From 39927e75f2416ab37d36a8097ec02c0651938eca Mon Sep 17 00:00:00 2001 From: Cory McWilliams Date: Sun, 6 Aug 2023 12:03:22 +0000 Subject: [PATCH] Attempt to support .gpx files. git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4389 ed5197a5-7fde-0310-b194-c3ffbd925b24 --- apps/gg/gpx.js | 81 ++++++++++++++++++++++++++++ apps/gg/script.js | 135 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 198 insertions(+), 18 deletions(-) create mode 100644 apps/gg/gpx.js diff --git a/apps/gg/gpx.js b/apps/gg/gpx.js new file mode 100644 index 00000000..93421d88 --- /dev/null +++ b/apps/gg/gpx.js @@ -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; +} \ No newline at end of file diff --git a/apps/gg/script.js b/apps/gg/script.js index 50f8540d..29ea7208 100644 --- a/apps/gg/script.js +++ b/apps/gg/script.js @@ -1,6 +1,7 @@ import {LitElement, html, unsafeHTML, css, guard, until} from './lit-all.min.js'; import * as tfrpc from '/static/tfrpc.js'; import * as polyline from './polyline.js'; +import {gpx_parse} from './gpx.js'; const k_client_id = '28276'; 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()) { 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.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() { if (!this.leaflet) { 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); } for (let activity of this.loaded_activities) { - for (let pt of polyline.decode(activity.map.polyline)) { - this.min_lat = Math.min(this.min_lat, pt[0]); - this.min_lon = Math.min(this.min_lon, pt[1]); - this.max_lat = Math.max(this.max_lat, pt[0]); - this.max_lon = Math.max(this.max_lon, pt[1]); - } + let bounds = this.activity_bounds(activity); + this.min_lat = Math.min(this.min_lat, bounds.min.lat); + this.min_lon = Math.min(this.min_lon, bounds.min.lng); + this.max_lat = Math.max(this.max_lat, bounds.max.lat); + this.max_lon = Math.max(this.max_lon, bounds.max.lng); } this.leaflet.fitBounds([ [this.min_lat, this.min_lon], @@ -392,17 +432,70 @@ class GgAppElement extends LitElement { draw_activity_to_tile(image_data, width, height, ul, lr, activity) { let color = this.activity_to_color(activity); - let last; - for (let pt of polyline.decode(activity.map.polyline)) { - let px = [ - Math.floor(width * (pt[1] - ul.lng) / (lr.lng - ul.lng)), - Math.floor(height * (pt[0] - ul.lat) / (lr.lat - ul.lat)), - ]; - if (last) { - this.line(image_data, last[0], last[1], px[0], px[1], color); + if (activity?.map?.polyline) { + let last; + for (let pt of polyline.decode(activity.map.polyline)) { + let px = [ + Math.floor(width * (pt[1] - ul.lng) / (lr.lng - ul.lng)), + Math.floor(height * (pt[0] - ul.lat) / (lr.lat - ul.lat)), + ]; + if (last) { + this.line(image_data, last[0], last[1], px[0], px[1], color); + } + 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() { @@ -415,8 +508,14 @@ class GgAppElement extends LitElement { } return html` -

Welcome, ${this.user.credentials.session.name} ${this.id}

-

${this.status?.text} ${this.status?.value}

+
+

+ Welcome, ${this.user.credentials.session.name} + ${this.id} + +

+

${this.status?.text} ${this.status?.value}

+
`; } }