Attempt to support .gpx files.

git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4389 ed5197a5-7fde-0310-b194-c3ffbd925b24
This commit is contained in:
Cory McWilliams 2023-08-06 12:03:22 +00:00
parent e6fd33b969
commit 39927e75f2
2 changed files with 198 additions and 18 deletions

81
apps/gg/gpx.js Normal file
View File

@ -0,0 +1,81 @@
function xml_parse(xml) {
let result;
let path = [];
let tag_begin;
let text_begin;
for (let i = 0; i < xml.length; i++) {
let c = xml.charAt(i);
if (!tag_begin && c == '<') {
if (i > text_begin && path.length) {
let value = xml.substring(text_begin, i);
if (!/^\s*$/.test(value)) {
path[path.length - 1].value = value;
}
}
tag_begin = i + 1;
} else if (tag_begin && c == '>') {
let tag = xml.substring(tag_begin, i).trim();
if (tag.startsWith('?') && tag.endsWith('?')) {
/* Ignore directives. */
} else if (tag.startsWith('/')) {
path.pop();
} else {
let parts = tag.split(' ');
let attributes = {};
for (let j = 1; j < parts.length; j++) {
let eq = parts[j].indexOf('=');
let value = parts[j].substring(eq + 1);
if (value.startsWith('"') && value.endsWith('"')) {
value = value.substring(1, value.length - 1);
}
attributes[parts[j].substring(0, eq)] = value;
}
let next = {name: parts[0], children: [], attributes: attributes};
if (path.length) {
path[path.length - 1].children.push(next);
} else {
result = next;
}
if (!tag.endsWith('/')) {
path.push(next);
}
}
tag_begin = undefined;
text_begin = i + 1;
}
}
return result;
}
function* xml_each(node, name) {
for (let child of node.children) {
if (child.name == name) {
yield child;
}
}
}
export function gpx_parse(xml) {
let result = {segments: []};
let tree = xml_parse(xml);
if (tree?.name == 'gpx') {
for (let trk of xml_each(tree, 'trk')) {
for (let trkseg of xml_each(trk, 'trkseg')) {
let segment = [];
for (let trkpt of xml_each(trkseg, 'trkpt')) {
segment.push({lat: parseFloat(trkpt.attributes.lat), lon: parseFloat(trkpt.attributes.lon)});
}
result.segments.push(segment);
}
}
}
for (let metadata of xml_each(tree, 'metadata')) {
for (let link of xml_each(metadata, 'link')) {
result.link = link.attributes.href;
}
for (let time of xml_each(metadata, 'time')) {
result.time = time.value;
}
}
return result;
}

View File

@ -1,6 +1,7 @@
import {LitElement, html, unsafeHTML, css, guard, until} from './lit-all.min.js'; import {LitElement, html, unsafeHTML, css, guard, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import * as polyline from './polyline.js'; import * as polyline from './polyline.js';
import {gpx_parse} from './gpx.js';
const k_client_id = '28276'; const k_client_id = '28276';
const k_redirect_url = 'https://tildefriends.net/~cory/gg/login'; const k_redirect_url = 'https://tildefriends.net/~cory/gg/login';
@ -75,7 +76,12 @@ class GgAppElement extends LitElement {
`, []); `, []);
for (let [index, row] of blob_ids.entries()) { for (let [index, row] of blob_ids.entries()) {
this.status = {text: 'loading activity data', value: index, max: blob_ids.length}; this.status = {text: 'loading activity data', value: index, max: blob_ids.length};
this.loaded_activities.push(JSON.parse(await tfrpc.rpc.get_blob(row.blob_id))); let blob = await tfrpc.rpc.get_blob(row.blob_id);
try {
this.loaded_activities.push(JSON.parse(blob));
} catch {
this.loaded_activities.push(gpx_parse(blob));
}
} }
this.status = undefined; this.status = undefined;
this.update_map(); this.update_map();
@ -208,6 +214,41 @@ class GgAppElement extends LitElement {
} }
} }
activity_bounds(activity) {
let min_lat = Number.MAX_VALUE;
let min_lon = Number.MAX_VALUE;
let max_lat = -Number.MAX_VALUE;
let max_lon = -Number.MAX_VALUE;
if (activity?.map?.polyline) {
for (let pt of polyline.decode(activity.map.polyline)) {
min_lat = Math.min(min_lat, pt[0]);
min_lon = Math.min(min_lon, pt[1]);
max_lat = Math.max(max_lat, pt[0]);
max_lon = Math.max(max_lon, pt[1]);
}
}
if (activity?.segments) {
for (let segment of activity.segments) {
for (let pt of segment) {
min_lat = Math.min(min_lat, pt.lat);
min_lon = Math.min(min_lon, pt.lon);
max_lat = Math.max(max_lat, pt.lat);
max_lon = Math.max(max_lon, pt.lon);
}
}
}
return {
min: {
lat: min_lat,
lng: min_lon,
},
max: {
lat: max_lat,
lng: max_lon,
},
};
}
async update_map() { async update_map() {
if (!this.leaflet) { if (!this.leaflet) {
this.leaflet = L.map('map', {attributionControl: false, maxZoom: 16, bounceAtZoomLimits: false}); this.leaflet = L.map('map', {attributionControl: false, maxZoom: 16, bounceAtZoomLimits: false});
@ -257,12 +298,11 @@ class GgAppElement extends LitElement {
this.grid_layer.addTo(this.leaflet); this.grid_layer.addTo(this.leaflet);
} }
for (let activity of this.loaded_activities) { for (let activity of this.loaded_activities) {
for (let pt of polyline.decode(activity.map.polyline)) { let bounds = this.activity_bounds(activity);
this.min_lat = Math.min(this.min_lat, pt[0]); this.min_lat = Math.min(this.min_lat, bounds.min.lat);
this.min_lon = Math.min(this.min_lon, pt[1]); this.min_lon = Math.min(this.min_lon, bounds.min.lng);
this.max_lat = Math.max(this.max_lat, pt[0]); this.max_lat = Math.max(this.max_lat, bounds.max.lat);
this.max_lon = Math.max(this.max_lon, pt[1]); this.max_lon = Math.max(this.max_lon, bounds.max.lng);
}
} }
this.leaflet.fitBounds([ this.leaflet.fitBounds([
[this.min_lat, this.min_lon], [this.min_lat, this.min_lon],
@ -392,6 +432,7 @@ class GgAppElement extends LitElement {
draw_activity_to_tile(image_data, width, height, ul, lr, activity) { draw_activity_to_tile(image_data, width, height, ul, lr, activity) {
let color = this.activity_to_color(activity); let color = this.activity_to_color(activity);
if (activity?.map?.polyline) {
let last; let last;
for (let pt of polyline.decode(activity.map.polyline)) { for (let pt of polyline.decode(activity.map.polyline)) {
let px = [ let px = [
@ -404,6 +445,58 @@ class GgAppElement extends LitElement {
last = px; last = px;
} }
} }
if (activity?.segments) {
console.log('have a gpx');
for (let segment of activity.segments) {
let last;
for (let pt of segment) {
let px = [
Math.floor(width * (pt.lon - ul.lng) / (lr.lng - ul.lng)),
Math.floor(height * (pt.lat - ul.lat) / (lr.lat - ul.lat)),
];
if (last) {
this.line(image_data, last[0], last[1], px[0], px[1], color);
}
last = px;
}
}
}
}
async on_upload(event) {
let file = event.srcElement.files[0];
let xml = await file.text();
let gpx = gpx_parse(xml);
let blob_id = await tfrpc.rpc.store_blob(xml);
console.log('blob_id = ', blob_id);
let message = {
type: 'gg-activity',
mentions: [
{
link: `https://${gpx.link}/activity/${gpx.time}`,
name: 'activity_url',
},
{
link: blob_id,
name: 'activity_data',
}
],
};
console.log('message = ', message);
try {
let id = await tfrpc.rpc.appendMessage(this.id, message);
console.log('appended message', id);
} catch (e) {
console.log('augh', e);
}
}
upload() {
let input = document.createElement('input');
input.type = 'file';
input.onchange = this.on_upload;
input.click();
}
render() { render() {
if (!this.user?.credentials?.session?.name) { if (!this.user?.credentials?.session?.name) {
@ -415,8 +508,14 @@ class GgAppElement extends LitElement {
} }
return html` return html`
<h1>Welcome, ${this.user.credentials.session.name} <span style="font-size: xx-small">${this.id}</span></h1> <div>
<h1>
Welcome, ${this.user.credentials.session.name}
<span style="font-size: xx-small">${this.id}</span>
<input type="button" value="📁" @click=${this.upload}></input>
</h1>
<h3>${this.status?.text} <progress ?hidden=${!this.status?.max} value=${this.status?.value} max=${this.status?.max}>${this.status?.value}</progress></h3> <h3>${this.status?.text} <progress ?hidden=${!this.status?.max} value=${this.status?.value} max=${this.status?.max}>${this.status?.value}</progress></h3>
</div>
`; `;
} }
} }