forked from cory/tildefriends
		
	apps/gg doesn't belong here and isn't ready for prime time..
This commit is contained in:
		@@ -717,7 +717,7 @@ PACKAGE_DIRS := \
 | 
			
		||||
	deps/prettier/ \
 | 
			
		||||
	deps/lit/
 | 
			
		||||
 | 
			
		||||
RAW_FILES := $(filter-out apps/blog% apps/gg% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f))
 | 
			
		||||
RAW_FILES := $(filter-out apps/blog% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f))
 | 
			
		||||
 | 
			
		||||
out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug
 | 
			
		||||
out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release
 | 
			
		||||
@@ -855,7 +855,6 @@ dist: release-apk iosrelease-ipa
 | 
			
		||||
	@mkdir -p dist/ out/tildefriends-$(VERSION_NUMBER)
 | 
			
		||||
	@git archive main | tar -x -C out/tildefriends-$(VERSION_NUMBER)
 | 
			
		||||
	@tar \
 | 
			
		||||
		--exclude=apps/gg* \
 | 
			
		||||
		--exclude=apps/welcome* \
 | 
			
		||||
		--exclude=deps/libbacktrace/Isaac.Newton-Opticks.txt \
 | 
			
		||||
		--exclude=deps/libsodium/builds/msvc/vs* \
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "🗺",
 | 
			
		||||
	"previous": "&0XSp+xdQwVtQ88bXzvWdH15Ex63hv5zUKTa4zx7HBGM=.sha256"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,85 +0,0 @@
 | 
			
		||||
import * as tfrpc from '/tfrpc.js';
 | 
			
		||||
import * as strava from './strava.js';
 | 
			
		||||
 | 
			
		||||
let g_database;
 | 
			
		||||
let g_shared_database;
 | 
			
		||||
 | 
			
		||||
tfrpc.register(async function createIdentity() {
 | 
			
		||||
	return ssb.createIdentity();
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function appendMessage(id, message) {
 | 
			
		||||
	print('APPEND', JSON.stringify(message));
 | 
			
		||||
	return ssb.appendMessageWithIdentity(id, message);
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(function url() {
 | 
			
		||||
	return core.url;
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function getUser() {
 | 
			
		||||
	return core.user;
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(function getIdentities() {
 | 
			
		||||
	return ssb.getIdentities();
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function databaseGet(key) {
 | 
			
		||||
	return g_database ? g_database.get(key) : undefined;
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function databaseSet(key, value) {
 | 
			
		||||
	return g_database ? g_database.set(key, value) : undefined;
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function databaseRemove(key, value) {
 | 
			
		||||
	return g_database ? g_database.remove(key, value) : undefined;
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function sharedDatabaseGet(key) {
 | 
			
		||||
	return g_shared_database ? g_shared_database.get(key) : undefined;
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function sharedDatabaseSet(key, value) {
 | 
			
		||||
	return g_shared_database ? g_shared_database.set(key, value) : undefined;
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function sharedDatabaseRemove(key, value) {
 | 
			
		||||
	return g_shared_database ? g_shared_database.remove(key, value) : undefined;
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function query(sql, args) {
 | 
			
		||||
	let result = [];
 | 
			
		||||
	await ssb.sqlAsync(sql, args, function callback(row) {
 | 
			
		||||
		result.push(row);
 | 
			
		||||
	});
 | 
			
		||||
	return result;
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function store_blob(blob) {
 | 
			
		||||
	if (typeof blob == 'string') {
 | 
			
		||||
		blob = utf8Encode(blob);
 | 
			
		||||
	}
 | 
			
		||||
	if (Array.isArray(blob)) {
 | 
			
		||||
		blob = Uint8Array.from(blob);
 | 
			
		||||
	}
 | 
			
		||||
	return await ssb.blobStore(blob);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tfrpc.register(async function get_blob(id) {
 | 
			
		||||
	return utf8Decode(await ssb.blobGet(id));
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(strava.refresh_token);
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
	g_shared_database = await shared_database('state');
 | 
			
		||||
	if (core.user.credentials?.session?.name) {
 | 
			
		||||
		g_database = await database('state');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let attempt;
 | 
			
		||||
	if (core.user.credentials?.session?.name) {
 | 
			
		||||
		let shared_db = await shared_database('state');
 | 
			
		||||
		attempt = await shared_db.get(core.user.credentials.session.name);
 | 
			
		||||
	}
 | 
			
		||||
	app.setDocument(
 | 
			
		||||
		utf8Decode(getFile('index.html')).replace(
 | 
			
		||||
			'${data}',
 | 
			
		||||
			JSON.stringify({
 | 
			
		||||
				attempt: attempt,
 | 
			
		||||
				state: core.user?.credentials?.session?.name,
 | 
			
		||||
			})
 | 
			
		||||
		)
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main();
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,84 +0,0 @@
 | 
			
		||||
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;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,21 +0,0 @@
 | 
			
		||||
import * as strava from './strava.js';
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
	print('handler running');
 | 
			
		||||
	let r = await strava.authorization_code(request.query.code);
 | 
			
		||||
	print('state =', request.query.state);
 | 
			
		||||
	print('body = ', r.body);
 | 
			
		||||
	if (request.query.state && r.body) {
 | 
			
		||||
		let shared_db = await shared_database('state');
 | 
			
		||||
		await shared_db.set(request.query.state, utf8Decode(r.body));
 | 
			
		||||
	}
 | 
			
		||||
	await respond({
 | 
			
		||||
		data: r.body,
 | 
			
		||||
		content_type: 'text/plain',
 | 
			
		||||
		headers: {
 | 
			
		||||
			Location: 'https://tildefriends.net/~cory/gg/',
 | 
			
		||||
		},
 | 
			
		||||
		status_code: 307,
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
main();
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
<!doctype html>
 | 
			
		||||
<html style="width: 100%; height: 100%; margin: 0; padding: 0">
 | 
			
		||||
	<head>
 | 
			
		||||
		<script>
 | 
			
		||||
			window.litDisableBundleWarning = true;
 | 
			
		||||
		</script>
 | 
			
		||||
		<script>
 | 
			
		||||
			let g_data = ${data};
 | 
			
		||||
		</script>
 | 
			
		||||
		<script src="script.js" type="module"></script>
 | 
			
		||||
		<script src="leaflet.js"></script>
 | 
			
		||||
	</head>
 | 
			
		||||
	<body
 | 
			
		||||
		style="
 | 
			
		||||
			color: #fff;
 | 
			
		||||
			display: flex;
 | 
			
		||||
			flex-flow: column;
 | 
			
		||||
			height: 100%;
 | 
			
		||||
			width: 100%;
 | 
			
		||||
			margin: 0;
 | 
			
		||||
			padding: 0;
 | 
			
		||||
		"
 | 
			
		||||
	>
 | 
			
		||||
		<gg-app style="width: 100%; height: 100%" id="ggapp"></gg-app>
 | 
			
		||||
	</body>
 | 
			
		||||
</html>
 | 
			
		||||
@@ -1,661 +0,0 @@
 | 
			
		||||
/* required styles */
 | 
			
		||||
 | 
			
		||||
.leaflet-pane,
 | 
			
		||||
.leaflet-tile,
 | 
			
		||||
.leaflet-marker-icon,
 | 
			
		||||
.leaflet-marker-shadow,
 | 
			
		||||
.leaflet-tile-container,
 | 
			
		||||
.leaflet-pane > svg,
 | 
			
		||||
.leaflet-pane > canvas,
 | 
			
		||||
.leaflet-zoom-box,
 | 
			
		||||
.leaflet-image-layer,
 | 
			
		||||
.leaflet-layer {
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	left: 0;
 | 
			
		||||
	top: 0;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-container {
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-tile,
 | 
			
		||||
.leaflet-marker-icon,
 | 
			
		||||
.leaflet-marker-shadow {
 | 
			
		||||
	-webkit-user-select: none;
 | 
			
		||||
	   -moz-user-select: none;
 | 
			
		||||
	        user-select: none;
 | 
			
		||||
	  -webkit-user-drag: none;
 | 
			
		||||
	}
 | 
			
		||||
/* Prevents IE11 from highlighting tiles in blue */
 | 
			
		||||
.leaflet-tile::selection {
 | 
			
		||||
	background: transparent;
 | 
			
		||||
}
 | 
			
		||||
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
 | 
			
		||||
.leaflet-safari .leaflet-tile {
 | 
			
		||||
	image-rendering: -webkit-optimize-contrast;
 | 
			
		||||
	}
 | 
			
		||||
/* hack that prevents hw layers "stretching" when loading new tiles */
 | 
			
		||||
.leaflet-safari .leaflet-tile-container {
 | 
			
		||||
	width: 1600px;
 | 
			
		||||
	height: 1600px;
 | 
			
		||||
	-webkit-transform-origin: 0 0;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-marker-icon,
 | 
			
		||||
.leaflet-marker-shadow {
 | 
			
		||||
	display: block;
 | 
			
		||||
	}
 | 
			
		||||
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
 | 
			
		||||
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
 | 
			
		||||
.leaflet-container .leaflet-overlay-pane svg {
 | 
			
		||||
	max-width: none !important;
 | 
			
		||||
	max-height: none !important;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-container .leaflet-marker-pane img,
 | 
			
		||||
.leaflet-container .leaflet-shadow-pane img,
 | 
			
		||||
.leaflet-container .leaflet-tile-pane img,
 | 
			
		||||
.leaflet-container img.leaflet-image-layer,
 | 
			
		||||
.leaflet-container .leaflet-tile {
 | 
			
		||||
	max-width: none !important;
 | 
			
		||||
	max-height: none !important;
 | 
			
		||||
	width: auto;
 | 
			
		||||
	padding: 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
.leaflet-container img.leaflet-tile {
 | 
			
		||||
	/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
 | 
			
		||||
	mix-blend-mode: plus-lighter;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.leaflet-container.leaflet-touch-zoom {
 | 
			
		||||
	-ms-touch-action: pan-x pan-y;
 | 
			
		||||
	touch-action: pan-x pan-y;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-container.leaflet-touch-drag {
 | 
			
		||||
	-ms-touch-action: pinch-zoom;
 | 
			
		||||
	/* Fallback for FF which doesn't support pinch-zoom */
 | 
			
		||||
	touch-action: none;
 | 
			
		||||
	touch-action: pinch-zoom;
 | 
			
		||||
}
 | 
			
		||||
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
 | 
			
		||||
	-ms-touch-action: none;
 | 
			
		||||
	touch-action: none;
 | 
			
		||||
}
 | 
			
		||||
.leaflet-container {
 | 
			
		||||
	-webkit-tap-highlight-color: transparent;
 | 
			
		||||
}
 | 
			
		||||
.leaflet-container a {
 | 
			
		||||
	-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
 | 
			
		||||
}
 | 
			
		||||
.leaflet-tile {
 | 
			
		||||
	filter: inherit;
 | 
			
		||||
	visibility: hidden;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-tile-loaded {
 | 
			
		||||
	visibility: inherit;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-zoom-box {
 | 
			
		||||
	width: 0;
 | 
			
		||||
	height: 0;
 | 
			
		||||
	-moz-box-sizing: border-box;
 | 
			
		||||
	     box-sizing: border-box;
 | 
			
		||||
	z-index: 800;
 | 
			
		||||
	}
 | 
			
		||||
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
 | 
			
		||||
.leaflet-overlay-pane svg {
 | 
			
		||||
	-moz-user-select: none;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
.leaflet-pane         { z-index: 400; }
 | 
			
		||||
 | 
			
		||||
.leaflet-tile-pane    { z-index: 200; }
 | 
			
		||||
.leaflet-overlay-pane { z-index: 400; }
 | 
			
		||||
.leaflet-shadow-pane  { z-index: 500; }
 | 
			
		||||
.leaflet-marker-pane  { z-index: 600; }
 | 
			
		||||
.leaflet-tooltip-pane   { z-index: 650; }
 | 
			
		||||
.leaflet-popup-pane   { z-index: 700; }
 | 
			
		||||
 | 
			
		||||
.leaflet-map-pane canvas { z-index: 100; }
 | 
			
		||||
.leaflet-map-pane svg    { z-index: 200; }
 | 
			
		||||
 | 
			
		||||
.leaflet-vml-shape {
 | 
			
		||||
	width: 1px;
 | 
			
		||||
	height: 1px;
 | 
			
		||||
	}
 | 
			
		||||
.lvml {
 | 
			
		||||
	behavior: url(#default#VML);
 | 
			
		||||
	display: inline-block;
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* control positioning */
 | 
			
		||||
 | 
			
		||||
.leaflet-control {
 | 
			
		||||
	position: relative;
 | 
			
		||||
	z-index: 800;
 | 
			
		||||
	pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
 | 
			
		||||
	pointer-events: auto;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-top,
 | 
			
		||||
.leaflet-bottom {
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	z-index: 1000;
 | 
			
		||||
	pointer-events: none;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-top {
 | 
			
		||||
	top: 0;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-right {
 | 
			
		||||
	right: 0;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-bottom {
 | 
			
		||||
	bottom: 0;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-left {
 | 
			
		||||
	left: 0;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-control {
 | 
			
		||||
	float: left;
 | 
			
		||||
	clear: both;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-right .leaflet-control {
 | 
			
		||||
	float: right;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-top .leaflet-control {
 | 
			
		||||
	margin-top: 10px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-bottom .leaflet-control {
 | 
			
		||||
	margin-bottom: 10px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-left .leaflet-control {
 | 
			
		||||
	margin-left: 10px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-right .leaflet-control {
 | 
			
		||||
	margin-right: 10px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* zoom and fade animations */
 | 
			
		||||
 | 
			
		||||
.leaflet-fade-anim .leaflet-popup {
 | 
			
		||||
	opacity: 0;
 | 
			
		||||
	-webkit-transition: opacity 0.2s linear;
 | 
			
		||||
	   -moz-transition: opacity 0.2s linear;
 | 
			
		||||
	        transition: opacity 0.2s linear;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
 | 
			
		||||
	opacity: 1;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-zoom-animated {
 | 
			
		||||
	-webkit-transform-origin: 0 0;
 | 
			
		||||
	    -ms-transform-origin: 0 0;
 | 
			
		||||
	        transform-origin: 0 0;
 | 
			
		||||
	}
 | 
			
		||||
svg.leaflet-zoom-animated {
 | 
			
		||||
	will-change: transform;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.leaflet-zoom-anim .leaflet-zoom-animated {
 | 
			
		||||
	-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
 | 
			
		||||
	   -moz-transition:    -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
 | 
			
		||||
	        transition:         transform 0.25s cubic-bezier(0,0,0.25,1);
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-zoom-anim .leaflet-tile,
 | 
			
		||||
.leaflet-pan-anim .leaflet-tile {
 | 
			
		||||
	-webkit-transition: none;
 | 
			
		||||
	   -moz-transition: none;
 | 
			
		||||
	        transition: none;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
.leaflet-zoom-anim .leaflet-zoom-hide {
 | 
			
		||||
	visibility: hidden;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* cursors */
 | 
			
		||||
 | 
			
		||||
.leaflet-interactive {
 | 
			
		||||
	cursor: pointer;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-grab {
 | 
			
		||||
	cursor: -webkit-grab;
 | 
			
		||||
	cursor:    -moz-grab;
 | 
			
		||||
	cursor:         grab;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-crosshair,
 | 
			
		||||
.leaflet-crosshair .leaflet-interactive {
 | 
			
		||||
	cursor: crosshair;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-popup-pane,
 | 
			
		||||
.leaflet-control {
 | 
			
		||||
	cursor: auto;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-dragging .leaflet-grab,
 | 
			
		||||
.leaflet-dragging .leaflet-grab .leaflet-interactive,
 | 
			
		||||
.leaflet-dragging .leaflet-marker-draggable {
 | 
			
		||||
	cursor: move;
 | 
			
		||||
	cursor: -webkit-grabbing;
 | 
			
		||||
	cursor:    -moz-grabbing;
 | 
			
		||||
	cursor:         grabbing;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
/* marker & overlays interactivity */
 | 
			
		||||
.leaflet-marker-icon,
 | 
			
		||||
.leaflet-marker-shadow,
 | 
			
		||||
.leaflet-image-layer,
 | 
			
		||||
.leaflet-pane > svg path,
 | 
			
		||||
.leaflet-tile-container {
 | 
			
		||||
	pointer-events: none;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
.leaflet-marker-icon.leaflet-interactive,
 | 
			
		||||
.leaflet-image-layer.leaflet-interactive,
 | 
			
		||||
.leaflet-pane > svg path.leaflet-interactive,
 | 
			
		||||
svg.leaflet-image-layer.leaflet-interactive path {
 | 
			
		||||
	pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
 | 
			
		||||
	pointer-events: auto;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
/* visual tweaks */
 | 
			
		||||
 | 
			
		||||
.leaflet-container {
 | 
			
		||||
	background: #ddd;
 | 
			
		||||
	outline-offset: 1px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-container a {
 | 
			
		||||
	color: #0078A8;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-zoom-box {
 | 
			
		||||
	border: 2px dotted #38f;
 | 
			
		||||
	background: rgba(255,255,255,0.5);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* general typography */
 | 
			
		||||
.leaflet-container {
 | 
			
		||||
	font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
 | 
			
		||||
	font-size: 12px;
 | 
			
		||||
	font-size: 0.75rem;
 | 
			
		||||
	line-height: 1.5;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* general toolbar styles */
 | 
			
		||||
 | 
			
		||||
.leaflet-bar {
 | 
			
		||||
	box-shadow: 0 1px 5px rgba(0,0,0,0.65);
 | 
			
		||||
	border-radius: 4px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-bar a {
 | 
			
		||||
	background-color: #fff;
 | 
			
		||||
	border-bottom: 1px solid #ccc;
 | 
			
		||||
	width: 26px;
 | 
			
		||||
	height: 26px;
 | 
			
		||||
	line-height: 26px;
 | 
			
		||||
	display: block;
 | 
			
		||||
	text-align: center;
 | 
			
		||||
	text-decoration: none;
 | 
			
		||||
	color: black;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-bar a,
 | 
			
		||||
.leaflet-control-layers-toggle {
 | 
			
		||||
	background-position: 50% 50%;
 | 
			
		||||
	background-repeat: no-repeat;
 | 
			
		||||
	display: block;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-bar a:hover,
 | 
			
		||||
.leaflet-bar a:focus {
 | 
			
		||||
	background-color: #f4f4f4;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-bar a:first-child {
 | 
			
		||||
	border-top-left-radius: 4px;
 | 
			
		||||
	border-top-right-radius: 4px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-bar a:last-child {
 | 
			
		||||
	border-bottom-left-radius: 4px;
 | 
			
		||||
	border-bottom-right-radius: 4px;
 | 
			
		||||
	border-bottom: none;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-bar a.leaflet-disabled {
 | 
			
		||||
	cursor: default;
 | 
			
		||||
	background-color: #f4f4f4;
 | 
			
		||||
	color: #bbb;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
.leaflet-touch .leaflet-bar a {
 | 
			
		||||
	width: 30px;
 | 
			
		||||
	height: 30px;
 | 
			
		||||
	line-height: 30px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-touch .leaflet-bar a:first-child {
 | 
			
		||||
	border-top-left-radius: 2px;
 | 
			
		||||
	border-top-right-radius: 2px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-touch .leaflet-bar a:last-child {
 | 
			
		||||
	border-bottom-left-radius: 2px;
 | 
			
		||||
	border-bottom-right-radius: 2px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
/* zoom control */
 | 
			
		||||
 | 
			
		||||
.leaflet-control-zoom-in,
 | 
			
		||||
.leaflet-control-zoom-out {
 | 
			
		||||
	font: bold 18px 'Lucida Console', Monaco, monospace;
 | 
			
		||||
	text-indent: 1px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out  {
 | 
			
		||||
	font-size: 22px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* layers control */
 | 
			
		||||
 | 
			
		||||
.leaflet-control-layers {
 | 
			
		||||
	box-shadow: 0 1px 5px rgba(0,0,0,0.4);
 | 
			
		||||
	background: #fff;
 | 
			
		||||
	border-radius: 5px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-control-layers-toggle {
 | 
			
		||||
	background-image: url(images/layers.png);
 | 
			
		||||
	width: 36px;
 | 
			
		||||
	height: 36px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-retina .leaflet-control-layers-toggle {
 | 
			
		||||
	background-image: url(images/layers-2x.png);
 | 
			
		||||
	background-size: 26px 26px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-touch .leaflet-control-layers-toggle {
 | 
			
		||||
	width: 44px;
 | 
			
		||||
	height: 44px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-control-layers .leaflet-control-layers-list,
 | 
			
		||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
 | 
			
		||||
	display: none;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
 | 
			
		||||
	display: block;
 | 
			
		||||
	position: relative;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-control-layers-expanded {
 | 
			
		||||
	padding: 6px 10px 6px 6px;
 | 
			
		||||
	color: #333;
 | 
			
		||||
	background: #fff;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-control-layers-scrollbar {
 | 
			
		||||
	overflow-y: scroll;
 | 
			
		||||
	overflow-x: hidden;
 | 
			
		||||
	padding-right: 5px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-control-layers-selector {
 | 
			
		||||
	margin-top: 2px;
 | 
			
		||||
	position: relative;
 | 
			
		||||
	top: 1px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-control-layers label {
 | 
			
		||||
	display: block;
 | 
			
		||||
	font-size: 13px;
 | 
			
		||||
	font-size: 1.08333em;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-control-layers-separator {
 | 
			
		||||
	height: 0;
 | 
			
		||||
	border-top: 1px solid #ddd;
 | 
			
		||||
	margin: 5px -10px 5px -6px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
/* Default icon URLs */
 | 
			
		||||
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
 | 
			
		||||
	background-image: url(images/marker-icon.png);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* attribution and scale controls */
 | 
			
		||||
 | 
			
		||||
.leaflet-container .leaflet-control-attribution {
 | 
			
		||||
	background: #fff;
 | 
			
		||||
	background: rgba(255, 255, 255, 0.8);
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-control-attribution,
 | 
			
		||||
.leaflet-control-scale-line {
 | 
			
		||||
	padding: 0 5px;
 | 
			
		||||
	color: #333;
 | 
			
		||||
	line-height: 1.4;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-control-attribution a {
 | 
			
		||||
	text-decoration: none;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-control-attribution a:hover,
 | 
			
		||||
.leaflet-control-attribution a:focus {
 | 
			
		||||
	text-decoration: underline;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-attribution-flag {
 | 
			
		||||
	display: inline !important;
 | 
			
		||||
	vertical-align: baseline !important;
 | 
			
		||||
	width: 1em;
 | 
			
		||||
	height: 0.6669em;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-left .leaflet-control-scale {
 | 
			
		||||
	margin-left: 5px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-bottom .leaflet-control-scale {
 | 
			
		||||
	margin-bottom: 5px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-control-scale-line {
 | 
			
		||||
	border: 2px solid #777;
 | 
			
		||||
	border-top: none;
 | 
			
		||||
	line-height: 1.1;
 | 
			
		||||
	padding: 2px 5px 1px;
 | 
			
		||||
	white-space: nowrap;
 | 
			
		||||
	-moz-box-sizing: border-box;
 | 
			
		||||
	     box-sizing: border-box;
 | 
			
		||||
	background: rgba(255, 255, 255, 0.8);
 | 
			
		||||
	text-shadow: 1px 1px #fff;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-control-scale-line:not(:first-child) {
 | 
			
		||||
	border-top: 2px solid #777;
 | 
			
		||||
	border-bottom: none;
 | 
			
		||||
	margin-top: -2px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
 | 
			
		||||
	border-bottom: 2px solid #777;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
.leaflet-touch .leaflet-control-attribution,
 | 
			
		||||
.leaflet-touch .leaflet-control-layers,
 | 
			
		||||
.leaflet-touch .leaflet-bar {
 | 
			
		||||
	box-shadow: none;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-touch .leaflet-control-layers,
 | 
			
		||||
.leaflet-touch .leaflet-bar {
 | 
			
		||||
	border: 2px solid rgba(0,0,0,0.2);
 | 
			
		||||
	background-clip: padding-box;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* popup */
 | 
			
		||||
 | 
			
		||||
.leaflet-popup {
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	text-align: center;
 | 
			
		||||
	margin-bottom: 20px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-popup-content-wrapper {
 | 
			
		||||
	padding: 1px;
 | 
			
		||||
	text-align: left;
 | 
			
		||||
	border-radius: 12px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-popup-content {
 | 
			
		||||
	margin: 13px 24px 13px 20px;
 | 
			
		||||
	line-height: 1.3;
 | 
			
		||||
	font-size: 13px;
 | 
			
		||||
	font-size: 1.08333em;
 | 
			
		||||
	min-height: 1px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-popup-content p {
 | 
			
		||||
	margin: 17px 0;
 | 
			
		||||
	margin: 1.3em 0;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-popup-tip-container {
 | 
			
		||||
	width: 40px;
 | 
			
		||||
	height: 20px;
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	left: 50%;
 | 
			
		||||
	margin-top: -1px;
 | 
			
		||||
	margin-left: -20px;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
	pointer-events: none;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-popup-tip {
 | 
			
		||||
	width: 17px;
 | 
			
		||||
	height: 17px;
 | 
			
		||||
	padding: 1px;
 | 
			
		||||
 | 
			
		||||
	margin: -10px auto 0;
 | 
			
		||||
	pointer-events: auto;
 | 
			
		||||
 | 
			
		||||
	-webkit-transform: rotate(45deg);
 | 
			
		||||
	   -moz-transform: rotate(45deg);
 | 
			
		||||
	    -ms-transform: rotate(45deg);
 | 
			
		||||
	        transform: rotate(45deg);
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-popup-content-wrapper,
 | 
			
		||||
.leaflet-popup-tip {
 | 
			
		||||
	background: white;
 | 
			
		||||
	color: #333;
 | 
			
		||||
	box-shadow: 0 3px 14px rgba(0,0,0,0.4);
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-container a.leaflet-popup-close-button {
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	top: 0;
 | 
			
		||||
	right: 0;
 | 
			
		||||
	border: none;
 | 
			
		||||
	text-align: center;
 | 
			
		||||
	width: 24px;
 | 
			
		||||
	height: 24px;
 | 
			
		||||
	font: 16px/24px Tahoma, Verdana, sans-serif;
 | 
			
		||||
	color: #757575;
 | 
			
		||||
	text-decoration: none;
 | 
			
		||||
	background: transparent;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-container a.leaflet-popup-close-button:hover,
 | 
			
		||||
.leaflet-container a.leaflet-popup-close-button:focus {
 | 
			
		||||
	color: #585858;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-popup-scrolled {
 | 
			
		||||
	overflow: auto;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
.leaflet-oldie .leaflet-popup-content-wrapper {
 | 
			
		||||
	-ms-zoom: 1;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-oldie .leaflet-popup-tip {
 | 
			
		||||
	width: 24px;
 | 
			
		||||
	margin: 0 auto;
 | 
			
		||||
 | 
			
		||||
	-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
 | 
			
		||||
	filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
.leaflet-oldie .leaflet-control-zoom,
 | 
			
		||||
.leaflet-oldie .leaflet-control-layers,
 | 
			
		||||
.leaflet-oldie .leaflet-popup-content-wrapper,
 | 
			
		||||
.leaflet-oldie .leaflet-popup-tip {
 | 
			
		||||
	border: 1px solid #999;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* div icon */
 | 
			
		||||
 | 
			
		||||
.leaflet-div-icon {
 | 
			
		||||
	background: #fff;
 | 
			
		||||
	border: 1px solid #666;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* Tooltip */
 | 
			
		||||
/* Base styles for the element that has a tooltip */
 | 
			
		||||
.leaflet-tooltip {
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	padding: 6px;
 | 
			
		||||
	background-color: #fff;
 | 
			
		||||
	border: 1px solid #fff;
 | 
			
		||||
	border-radius: 3px;
 | 
			
		||||
	color: #222;
 | 
			
		||||
	white-space: nowrap;
 | 
			
		||||
	-webkit-user-select: none;
 | 
			
		||||
	-moz-user-select: none;
 | 
			
		||||
	-ms-user-select: none;
 | 
			
		||||
	user-select: none;
 | 
			
		||||
	pointer-events: none;
 | 
			
		||||
	box-shadow: 0 1px 3px rgba(0,0,0,0.4);
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-tooltip.leaflet-interactive {
 | 
			
		||||
	cursor: pointer;
 | 
			
		||||
	pointer-events: auto;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-tooltip-top:before,
 | 
			
		||||
.leaflet-tooltip-bottom:before,
 | 
			
		||||
.leaflet-tooltip-left:before,
 | 
			
		||||
.leaflet-tooltip-right:before {
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	pointer-events: none;
 | 
			
		||||
	border: 6px solid transparent;
 | 
			
		||||
	background: transparent;
 | 
			
		||||
	content: "";
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
/* Directions */
 | 
			
		||||
 | 
			
		||||
.leaflet-tooltip-bottom {
 | 
			
		||||
	margin-top: 6px;
 | 
			
		||||
}
 | 
			
		||||
.leaflet-tooltip-top {
 | 
			
		||||
	margin-top: -6px;
 | 
			
		||||
}
 | 
			
		||||
.leaflet-tooltip-bottom:before,
 | 
			
		||||
.leaflet-tooltip-top:before {
 | 
			
		||||
	left: 50%;
 | 
			
		||||
	margin-left: -6px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-tooltip-top:before {
 | 
			
		||||
	bottom: 0;
 | 
			
		||||
	margin-bottom: -12px;
 | 
			
		||||
	border-top-color: #fff;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-tooltip-bottom:before {
 | 
			
		||||
	top: 0;
 | 
			
		||||
	margin-top: -12px;
 | 
			
		||||
	margin-left: -6px;
 | 
			
		||||
	border-bottom-color: #fff;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-tooltip-left {
 | 
			
		||||
	margin-left: -6px;
 | 
			
		||||
}
 | 
			
		||||
.leaflet-tooltip-right {
 | 
			
		||||
	margin-left: 6px;
 | 
			
		||||
}
 | 
			
		||||
.leaflet-tooltip-left:before,
 | 
			
		||||
.leaflet-tooltip-right:before {
 | 
			
		||||
	top: 50%;
 | 
			
		||||
	margin-top: -6px;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-tooltip-left:before {
 | 
			
		||||
	right: 0;
 | 
			
		||||
	margin-right: -12px;
 | 
			
		||||
	border-left-color: #fff;
 | 
			
		||||
	}
 | 
			
		||||
.leaflet-tooltip-right:before {
 | 
			
		||||
	left: 0;
 | 
			
		||||
	margin-left: -12px;
 | 
			
		||||
	border-right-color: #fff;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
/* Printing */
 | 
			
		||||
 | 
			
		||||
@media print {
 | 
			
		||||
	/* Prevent printers from removing background-images of controls. */
 | 
			
		||||
	.leaflet-control {
 | 
			
		||||
		-webkit-print-color-adjust: exact;
 | 
			
		||||
		print-color-adjust: exact;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										120
									
								
								apps/gg/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										120
									
								
								apps/gg/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,162 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Based off of [the offical Google document](https://developers.google.com/maps/documentation/utilities/polylinealgorithm)
 | 
			
		||||
 *
 | 
			
		||||
 * Some parts from [this implementation](http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/PolylineEncoder.js)
 | 
			
		||||
 * by [Mark McClure](http://facstaff.unca.edu/mcmcclur/)
 | 
			
		||||
 *
 | 
			
		||||
 * @module polyline
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
var polyline = {};
 | 
			
		||||
 | 
			
		||||
function py2_round(value) {
 | 
			
		||||
	// Google's polyline algorithm uses the same rounding strategy as Python 2, which is different from JS for negative values
 | 
			
		||||
	return Math.floor(Math.abs(value) + 0.5) * (value >= 0 ? 1 : -1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function encode(current, previous, factor) {
 | 
			
		||||
	current = py2_round(current * factor);
 | 
			
		||||
	previous = py2_round(previous * factor);
 | 
			
		||||
	var coordinate = (current - previous) * 2;
 | 
			
		||||
	if (coordinate < 0) {
 | 
			
		||||
		coordinate = -coordinate - 1;
 | 
			
		||||
	}
 | 
			
		||||
	var output = '';
 | 
			
		||||
	while (coordinate >= 0x20) {
 | 
			
		||||
		output += String.fromCharCode((0x20 | (coordinate & 0x1f)) + 63);
 | 
			
		||||
		coordinate /= 32;
 | 
			
		||||
	}
 | 
			
		||||
	output += String.fromCharCode((coordinate | 0) + 63);
 | 
			
		||||
	return output;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Decodes to a [latitude, longitude] coordinates array.
 | 
			
		||||
 *
 | 
			
		||||
 * This is adapted from the implementation in Project-OSRM.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {String} str
 | 
			
		||||
 * @param {Number} precision
 | 
			
		||||
 * @returns {Array}
 | 
			
		||||
 *
 | 
			
		||||
 * @see https://github.com/Project-OSRM/osrm-frontend/blob/master/WebContent/routing/OSRM.RoutingGeometry.js
 | 
			
		||||
 */
 | 
			
		||||
polyline.decode = function (str, precision) {
 | 
			
		||||
	var index = 0,
 | 
			
		||||
		lat = 0,
 | 
			
		||||
		lng = 0,
 | 
			
		||||
		coordinates = [],
 | 
			
		||||
		shift = 0,
 | 
			
		||||
		result = 0,
 | 
			
		||||
		byte = null,
 | 
			
		||||
		latitude_change,
 | 
			
		||||
		longitude_change,
 | 
			
		||||
		factor = Math.pow(10, Number.isInteger(precision) ? precision : 5);
 | 
			
		||||
 | 
			
		||||
	// Coordinates have variable length when encoded, so just keep
 | 
			
		||||
	// track of whether we've hit the end of the string. In each
 | 
			
		||||
	// loop iteration, a single coordinate is decoded.
 | 
			
		||||
	while (index < str.length) {
 | 
			
		||||
		// Reset shift, result, and byte
 | 
			
		||||
		byte = null;
 | 
			
		||||
		shift = 1;
 | 
			
		||||
		result = 0;
 | 
			
		||||
 | 
			
		||||
		do {
 | 
			
		||||
			byte = str.charCodeAt(index++) - 63;
 | 
			
		||||
			result += (byte & 0x1f) * shift;
 | 
			
		||||
			shift *= 32;
 | 
			
		||||
		} while (byte >= 0x20);
 | 
			
		||||
 | 
			
		||||
		latitude_change = result & 1 ? (-result - 1) / 2 : result / 2;
 | 
			
		||||
 | 
			
		||||
		shift = 1;
 | 
			
		||||
		result = 0;
 | 
			
		||||
 | 
			
		||||
		do {
 | 
			
		||||
			byte = str.charCodeAt(index++) - 63;
 | 
			
		||||
			result += (byte & 0x1f) * shift;
 | 
			
		||||
			shift *= 32;
 | 
			
		||||
		} while (byte >= 0x20);
 | 
			
		||||
 | 
			
		||||
		longitude_change = result & 1 ? (-result - 1) / 2 : result / 2;
 | 
			
		||||
 | 
			
		||||
		lat += latitude_change;
 | 
			
		||||
		lng += longitude_change;
 | 
			
		||||
 | 
			
		||||
		coordinates.push([lat / factor, lng / factor]);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return coordinates;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Encodes the given [latitude, longitude] coordinates array.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Array.<Array.<Number>>} coordinates
 | 
			
		||||
 * @param {Number} precision
 | 
			
		||||
 * @returns {String}
 | 
			
		||||
 */
 | 
			
		||||
polyline.encode = function (coordinates, precision) {
 | 
			
		||||
	if (!coordinates.length) {
 | 
			
		||||
		return '';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var factor = Math.pow(10, Number.isInteger(precision) ? precision : 5),
 | 
			
		||||
		output =
 | 
			
		||||
			encode(coordinates[0][0], 0, factor) +
 | 
			
		||||
			encode(coordinates[0][1], 0, factor);
 | 
			
		||||
 | 
			
		||||
	for (var i = 1; i < coordinates.length; i++) {
 | 
			
		||||
		var a = coordinates[i],
 | 
			
		||||
			b = coordinates[i - 1];
 | 
			
		||||
		output += encode(a[0], b[0], factor);
 | 
			
		||||
		output += encode(a[1], b[1], factor);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return output;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function flipped(coords) {
 | 
			
		||||
	var flipped = [];
 | 
			
		||||
	for (var i = 0; i < coords.length; i++) {
 | 
			
		||||
		var coord = coords[i].slice();
 | 
			
		||||
		flipped.push([coord[1], coord[0]]);
 | 
			
		||||
	}
 | 
			
		||||
	return flipped;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Encodes a GeoJSON LineString feature/geometry.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Object} geojson
 | 
			
		||||
 * @param {Number} precision
 | 
			
		||||
 * @returns {String}
 | 
			
		||||
 */
 | 
			
		||||
polyline.fromGeoJSON = function (geojson, precision) {
 | 
			
		||||
	if (geojson && geojson.type === 'Feature') {
 | 
			
		||||
		geojson = geojson.geometry;
 | 
			
		||||
	}
 | 
			
		||||
	if (!geojson || geojson.type !== 'LineString') {
 | 
			
		||||
		throw new Error('Input must be a GeoJSON LineString');
 | 
			
		||||
	}
 | 
			
		||||
	return polyline.encode(flipped(geojson.coordinates), precision);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Decodes to a GeoJSON LineString geometry.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {String} str
 | 
			
		||||
 * @param {Number} precision
 | 
			
		||||
 * @returns {Object}
 | 
			
		||||
 */
 | 
			
		||||
polyline.toGeoJSON = function (str, precision) {
 | 
			
		||||
	var coords = polyline.decode(str, precision);
 | 
			
		||||
	return {
 | 
			
		||||
		type: 'LineString',
 | 
			
		||||
		coordinates: flipped(coords),
 | 
			
		||||
	};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
let polyline_decode = polyline.decode;
 | 
			
		||||
export {polyline_decode as decode};
 | 
			
		||||
@@ -1,909 +0,0 @@
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
const k_color_snow = [128, 128, 255, 255];
 | 
			
		||||
const k_color_ice = [160, 160, 255, 255];
 | 
			
		||||
const k_color_water = [0, 0, 255, 255];
 | 
			
		||||
const k_color_dirt = [128, 129, 130, 255];
 | 
			
		||||
const k_color_pavement = [32, 32, 32, 255];
 | 
			
		||||
const k_color_grass = [0, 255, 0, 255];
 | 
			
		||||
const k_color_default = [128, 128, 128, 255];
 | 
			
		||||
 | 
			
		||||
const k_store = {
 | 
			
		||||
	'🦞': 15,
 | 
			
		||||
	'🛶': 10,
 | 
			
		||||
	'🏠': 10,
 | 
			
		||||
	'⛰': 10,
 | 
			
		||||
	'🐠': 10,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const k_marker_snap = {x: 5, y: 4};
 | 
			
		||||
 | 
			
		||||
class GgAppElement extends LitElement {
 | 
			
		||||
	static get properties() {
 | 
			
		||||
		return {
 | 
			
		||||
			user: {type: Object},
 | 
			
		||||
			strava: {type: Object},
 | 
			
		||||
			activities: {type: Array},
 | 
			
		||||
			activity: {type: Object},
 | 
			
		||||
			world: {type: Object},
 | 
			
		||||
			whoami: {type: String},
 | 
			
		||||
			status: {type: Object},
 | 
			
		||||
			tab: {type: String},
 | 
			
		||||
			url: {type: String},
 | 
			
		||||
			currency: {type: Number},
 | 
			
		||||
			to_build: {type: String},
 | 
			
		||||
			emoji_of_the_day: {type: String},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	constructor() {
 | 
			
		||||
		super();
 | 
			
		||||
		this.activities = [];
 | 
			
		||||
		this.activity = {};
 | 
			
		||||
		this.loaded_activities = [];
 | 
			
		||||
		this.placed_emojis = [];
 | 
			
		||||
		this.strava = {};
 | 
			
		||||
		this.min_lat = Number.MAX_VALUE;
 | 
			
		||||
		this.min_lon = Number.MAX_VALUE;
 | 
			
		||||
		this.max_lat = -Number.MAX_VALUE;
 | 
			
		||||
		this.max_lon = -Number.MAX_VALUE;
 | 
			
		||||
		this.focus = undefined;
 | 
			
		||||
		this.status = undefined;
 | 
			
		||||
		this.tab = 'map';
 | 
			
		||||
		this.load().catch(function (e) {
 | 
			
		||||
			console.log('load error', e);
 | 
			
		||||
		});
 | 
			
		||||
		this.to_build = '🏠';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async load() {
 | 
			
		||||
		console.log('load');
 | 
			
		||||
		let emojis = await (await fetch('emojis.json')).json();
 | 
			
		||||
		emojis = Object.values(emojis)
 | 
			
		||||
			.map((x) => Object.values(x))
 | 
			
		||||
			.flat();
 | 
			
		||||
		let today = new Date();
 | 
			
		||||
		let date_index =
 | 
			
		||||
			today.getYear() * 356 + today.getMonth() * 31 + today.getDate();
 | 
			
		||||
		this.emoji_of_the_day = emojis[(date_index * 123457) % emojis.length];
 | 
			
		||||
		this.user = await tfrpc.rpc.getUser();
 | 
			
		||||
		this.url = (await tfrpc.rpc.url()).split('?')[0];
 | 
			
		||||
		try {
 | 
			
		||||
			await this.update_credentials();
 | 
			
		||||
		} catch (e) {
 | 
			
		||||
			console.log('update_credentials failed', e);
 | 
			
		||||
		}
 | 
			
		||||
		try {
 | 
			
		||||
			await this.update_activities();
 | 
			
		||||
		} catch (e) {
 | 
			
		||||
			console.log('update_activities failed', e);
 | 
			
		||||
		}
 | 
			
		||||
		await this.acquire_ssb_identity();
 | 
			
		||||
		if (this.whoami && this.activities?.length) {
 | 
			
		||||
			await this.sync_activities();
 | 
			
		||||
		}
 | 
			
		||||
		await this.get_activities_from_ssb();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/* https://gist.github.com/jcouyang/632709f30e12a7879a73e9e132c0d56b?permalink_comment_id=3591045#gistcomment-3591045 */
 | 
			
		||||
	async promise_all(promises, max_concurrent) {
 | 
			
		||||
		let index = 0;
 | 
			
		||||
		let results = [];
 | 
			
		||||
		async function exec_thread() {
 | 
			
		||||
			while (index < promises.length) {
 | 
			
		||||
				const current = index++;
 | 
			
		||||
				results[current] = await promises[current];
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		const threads = [];
 | 
			
		||||
		for (let thread = 0; thread < max_concurrent; thread++) {
 | 
			
		||||
			threads.push(exec_thread());
 | 
			
		||||
		}
 | 
			
		||||
		await Promise.all(threads);
 | 
			
		||||
		return results;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async get_activities_from_ssb() {
 | 
			
		||||
		this.status = {text: 'loading activities'};
 | 
			
		||||
		this.loaded_activities = [];
 | 
			
		||||
		let rows = await tfrpc.rpc.query(
 | 
			
		||||
			`
 | 
			
		||||
			SELECT messages.author, json_extract(mention.value, '$.link') AS blob_id
 | 
			
		||||
			FROM messages_fts('"gg-activity"')
 | 
			
		||||
			JOIN messages ON messages.rowid = messages_fts.rowid,
 | 
			
		||||
				json_each(messages.content, '$.mentions') as mention
 | 
			
		||||
			WHERE json_extract(messages.content, '$.type') = 'gg-activity' AND
 | 
			
		||||
			json_extract(mention.value, '$.name') = 'activity_data'
 | 
			
		||||
			ORDER BY messages.timestamp DESC
 | 
			
		||||
		`,
 | 
			
		||||
			[]
 | 
			
		||||
		);
 | 
			
		||||
		this.status = {text: 'loading activity data'};
 | 
			
		||||
		let authors = rows.map((x) => x.author);
 | 
			
		||||
		let blobs = await this.promise_all(
 | 
			
		||||
			rows.map((x) => tfrpc.rpc.get_blob(x.blob_id)),
 | 
			
		||||
			8
 | 
			
		||||
		);
 | 
			
		||||
		this.status = {text: 'processing activity data'};
 | 
			
		||||
		for (let [index, blob] of blobs.entries()) {
 | 
			
		||||
			let activity;
 | 
			
		||||
			try {
 | 
			
		||||
				activity = JSON.parse(blob);
 | 
			
		||||
			} catch {
 | 
			
		||||
				activity = gpx_parse(blob);
 | 
			
		||||
			}
 | 
			
		||||
			if (activity) {
 | 
			
		||||
				activity.author = authors[index];
 | 
			
		||||
				this.loaded_activities.push(activity);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		this.status = {text: 'calculating balance'};
 | 
			
		||||
		rows = await tfrpc.rpc.query(
 | 
			
		||||
			`
 | 
			
		||||
			SELECT count(*) AS currency FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-activity'
 | 
			
		||||
		`,
 | 
			
		||||
			[this.whoami]
 | 
			
		||||
		);
 | 
			
		||||
		let currency = rows[0].currency;
 | 
			
		||||
		rows = await tfrpc.rpc.query(
 | 
			
		||||
			`
 | 
			
		||||
			SELECT SUM(json_extract(content, '$.cost')) AS cost FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-place'
 | 
			
		||||
		`,
 | 
			
		||||
			[this.whoami]
 | 
			
		||||
		);
 | 
			
		||||
		let spent = rows[0].cost;
 | 
			
		||||
		this.currency = currency - spent;
 | 
			
		||||
		this.status = {text: 'getting placed emojis'};
 | 
			
		||||
		rows = await tfrpc.rpc.query(`
 | 
			
		||||
			SELECT messages.content
 | 
			
		||||
			FROM messages_fts('"gg-place"')
 | 
			
		||||
			JOIN messages ON messages.rowid = messages_fts.rowid
 | 
			
		||||
			WHERE json_extract(messages.content, '$.type') = 'gg-place'
 | 
			
		||||
			ORDER BY messages.timestamp
 | 
			
		||||
		`);
 | 
			
		||||
		for (let row of rows) {
 | 
			
		||||
			console.log(row.content);
 | 
			
		||||
			let content = JSON.parse(row.content);
 | 
			
		||||
			this.placed_emojis.push({
 | 
			
		||||
				position: content.position,
 | 
			
		||||
				emoji: content.emoji,
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		console.log(this.placed_emojis);
 | 
			
		||||
		this.status = undefined;
 | 
			
		||||
		this.update_map();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async sync_activities() {
 | 
			
		||||
		let ids = this.activities.map(
 | 
			
		||||
			(x) => `https://www.strava.com/activities/${x.id}`
 | 
			
		||||
		);
 | 
			
		||||
		let missing = await tfrpc.rpc.query(
 | 
			
		||||
			`
 | 
			
		||||
			WITH my_activities AS (
 | 
			
		||||
				SELECT json_extract(mention.value, '$.link') AS url
 | 
			
		||||
				FROM messages, json_each(messages.content, '$.mentions') AS mention
 | 
			
		||||
				WHERE
 | 
			
		||||
					author = ? AND
 | 
			
		||||
					json_extract(messages.content, '$.type') = 'gg-activity' AND
 | 
			
		||||
					json_extract(mention.value, '$.name') = 'activity_url')
 | 
			
		||||
			SELECT from_strava.value FROM json_each(?) AS from_strava
 | 
			
		||||
			LEFT OUTER JOIN my_activities ON from_strava.value = my_activities.url
 | 
			
		||||
			WHERE my_activities.url IS NULL
 | 
			
		||||
			`,
 | 
			
		||||
			[this.whoami, JSON.stringify(ids)]
 | 
			
		||||
		);
 | 
			
		||||
		console.log('missing = ', missing);
 | 
			
		||||
		for (let [index, row] of missing.entries()) {
 | 
			
		||||
			this.status = {
 | 
			
		||||
				text: 'syncing from strava',
 | 
			
		||||
				value: index,
 | 
			
		||||
				max: missing.length,
 | 
			
		||||
			};
 | 
			
		||||
			let url = row.value;
 | 
			
		||||
			let id = url.match(/.*\/(\d+)/)[1];
 | 
			
		||||
			let response = await fetch(
 | 
			
		||||
				`https://www.strava.com/api/v3/activities/${id}`,
 | 
			
		||||
				{
 | 
			
		||||
					headers: {
 | 
			
		||||
						Authorization: `Bearer ${this.strava.access_token}`,
 | 
			
		||||
					},
 | 
			
		||||
				}
 | 
			
		||||
			);
 | 
			
		||||
			let activity = await response.json();
 | 
			
		||||
			let blob_id = await tfrpc.rpc.store_blob(JSON.stringify(activity));
 | 
			
		||||
			let message = {
 | 
			
		||||
				type: 'gg-activity',
 | 
			
		||||
				mentions: [
 | 
			
		||||
					{
 | 
			
		||||
						link: url,
 | 
			
		||||
						name: 'activity_url',
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						link: blob_id,
 | 
			
		||||
						name: 'activity_data',
 | 
			
		||||
					},
 | 
			
		||||
				],
 | 
			
		||||
			};
 | 
			
		||||
			await tfrpc.rpc.appendMessage(this.whoami, message);
 | 
			
		||||
		}
 | 
			
		||||
		this.status = undefined;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async acquire_ssb_identity() {
 | 
			
		||||
		let user = await tfrpc.rpc.getUser();
 | 
			
		||||
		if (!user?.credentials?.session?.name) {
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		let ids = await tfrpc.rpc.getIdentities();
 | 
			
		||||
		let players = ids.length
 | 
			
		||||
			? (
 | 
			
		||||
					await tfrpc.rpc.query(
 | 
			
		||||
						`
 | 
			
		||||
			SELECT author FROM messages JOIN json_each(?) ON messages.author = json_each.value
 | 
			
		||||
			WHERE
 | 
			
		||||
				json_extract(messages.content, '$.type') = 'gg-player' AND
 | 
			
		||||
				json_extract(messages.content, '$.active')
 | 
			
		||||
			ORDER BY timestamp DESC limit 1
 | 
			
		||||
			`,
 | 
			
		||||
						[JSON.stringify(ids)]
 | 
			
		||||
					)
 | 
			
		||||
				).map((row) => row.author)
 | 
			
		||||
			: [];
 | 
			
		||||
		if (!players.length) {
 | 
			
		||||
			this.whoami = await tfrpc.rpc.createIdentity();
 | 
			
		||||
			if (this.whoami) {
 | 
			
		||||
				await tfrpc.rpc.appendMessage(this.whoami, {
 | 
			
		||||
					type: 'gg-player',
 | 
			
		||||
					active: true,
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			players.sort();
 | 
			
		||||
			this.whoami = players[0];
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async update_credentials() {
 | 
			
		||||
		let name = this.user?.credentials?.session?.name;
 | 
			
		||||
		if (!name) {
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		let shared = await tfrpc.rpc.sharedDatabaseGet(name);
 | 
			
		||||
		if (shared) {
 | 
			
		||||
			await tfrpc.rpc.databaseSet('strava', shared);
 | 
			
		||||
			await tfrpc.rpc.sharedDatabaseRemove(name);
 | 
			
		||||
		}
 | 
			
		||||
		this.strava = JSON.parse((await tfrpc.rpc.databaseGet('strava')) || '{}');
 | 
			
		||||
		if (new Date().valueOf() / 1000 > this.strava.expires_at) {
 | 
			
		||||
			console.log(
 | 
			
		||||
				'this looks expired',
 | 
			
		||||
				new Date().valueOf() / 1000,
 | 
			
		||||
				'>',
 | 
			
		||||
				this.strava.expires_at
 | 
			
		||||
			);
 | 
			
		||||
			let x = await tfrpc.rpc.refresh_token(this.strava);
 | 
			
		||||
			if (x) {
 | 
			
		||||
				this.strava = x;
 | 
			
		||||
				await tfrpc.rpc.databaseSet('strava', JSON.stringify(x));
 | 
			
		||||
			} else {
 | 
			
		||||
				this.strava = null;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async update_activities() {
 | 
			
		||||
		if (this?.strava?.access_token) {
 | 
			
		||||
			let response = await fetch(
 | 
			
		||||
				'https://www.strava.com/api/v3/athlete/activities',
 | 
			
		||||
				{
 | 
			
		||||
					headers: {
 | 
			
		||||
						Authorization: `Bearer ${this.strava.access_token}`,
 | 
			
		||||
					},
 | 
			
		||||
				}
 | 
			
		||||
			);
 | 
			
		||||
			this.activities = await response.json();
 | 
			
		||||
			this.activities.sort((a, b) => a.id - b.id);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	color_to_emoji(color) {
 | 
			
		||||
		const k_map = [
 | 
			
		||||
			[k_color_snow, '⬜'],
 | 
			
		||||
			[k_color_ice, '🟦'],
 | 
			
		||||
			[k_color_water, '🟦'],
 | 
			
		||||
			[k_color_dirt, '🟫'],
 | 
			
		||||
			[k_color_pavement, '⬛'],
 | 
			
		||||
			[k_color_grass, '🟩'],
 | 
			
		||||
			[k_color_default, '🟧'],
 | 
			
		||||
		];
 | 
			
		||||
		for (let m of k_map) {
 | 
			
		||||
			if (
 | 
			
		||||
				m[0][0] == color[0] &&
 | 
			
		||||
				m[0][1] == color[1] &&
 | 
			
		||||
				m[0][2] == color[2] &&
 | 
			
		||||
				m[0][3] == color[3]
 | 
			
		||||
			) {
 | 
			
		||||
				return m[1];
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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,
 | 
			
		||||
			},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	on_click(event) {
 | 
			
		||||
		let popup = L.popup()
 | 
			
		||||
			.setLatLng(event.latlng)
 | 
			
		||||
			.setContent(
 | 
			
		||||
				`
 | 
			
		||||
				<div><a target="_top" href="https://www.google.com/maps/search/?api=1&query=${event.latlng.lat},${event.latlng.lng}">${event.latlng.lat}, ${event.latlng.lng}</a></div>
 | 
			
		||||
			`
 | 
			
		||||
			)
 | 
			
		||||
			.openOn(this.leaflet);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async build() {
 | 
			
		||||
		if (this.popup) {
 | 
			
		||||
			this.popup.remove();
 | 
			
		||||
		}
 | 
			
		||||
		if (!this.marker) {
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		let latlng = this.marker.getLatLng();
 | 
			
		||||
 | 
			
		||||
		let cost = k_store[this.to_build];
 | 
			
		||||
		if (cost > this.currency) {
 | 
			
		||||
			alert('Insufficient funds.');
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		let message = {
 | 
			
		||||
			type: 'gg-place',
 | 
			
		||||
			position: {lat: latlng.lat, lng: latlng.lng},
 | 
			
		||||
			emoji: this.to_build,
 | 
			
		||||
			cost: cost,
 | 
			
		||||
		};
 | 
			
		||||
		let id = await tfrpc.rpc.appendMessage(this.whoami, message);
 | 
			
		||||
		this.marker.remove();
 | 
			
		||||
		this.placed_emojis.push({
 | 
			
		||||
			position: {lat: latlng.lat, lng: latlng.lng},
 | 
			
		||||
			emoji: this.to_build,
 | 
			
		||||
		});
 | 
			
		||||
		this.currency -= cost;
 | 
			
		||||
		return this.update_map();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	on_marker_click(event) {
 | 
			
		||||
		this.popup = L.popup()
 | 
			
		||||
			.setLatLng(event.latlng)
 | 
			
		||||
			.setContent(
 | 
			
		||||
				`
 | 
			
		||||
				${this.to_build} (-${k_store[this.to_build]}) <input type="button" value="Build" onclick="document.getElementById('ggapp').build()"></input>
 | 
			
		||||
			`
 | 
			
		||||
			)
 | 
			
		||||
			.openOn(this.leaflet);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	snap_to_grid(latlng, fudge, zoom) {
 | 
			
		||||
		let position = this.leaflet.options.crs.latLngToPoint(
 | 
			
		||||
			latlng,
 | 
			
		||||
			zoom ?? this.leaflet.getZoom()
 | 
			
		||||
		);
 | 
			
		||||
		position.x = Math.round(position.x / 16) * 16 + (fudge?.x ?? 0);
 | 
			
		||||
		position.y = Math.round(position.y / 16) * 16 + (fudge?.y ?? 0);
 | 
			
		||||
		position = this.leaflet.options.crs.pointToLatLng(
 | 
			
		||||
			position,
 | 
			
		||||
			zoom ?? this.leaflet.getZoom()
 | 
			
		||||
		);
 | 
			
		||||
		return position;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	on_marker_move(event) {
 | 
			
		||||
		if (!this.no_snap && this.marker) {
 | 
			
		||||
			this.no_snap = true;
 | 
			
		||||
			this.marker.setLatLng(
 | 
			
		||||
				this.snap_to_grid(this.marker.getLatLng(), k_marker_snap)
 | 
			
		||||
			);
 | 
			
		||||
			this.no_snap = false;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	on_zoom(event) {
 | 
			
		||||
		if (this.marker) {
 | 
			
		||||
			this.marker.setLatLng(
 | 
			
		||||
				this.snap_to_grid(this.marker.getLatLng(), k_marker_snap)
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	on_mouse_down(event) {
 | 
			
		||||
		if (this.marker) {
 | 
			
		||||
			this.marker.remove();
 | 
			
		||||
			this.marker = undefined;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (this.to_build) {
 | 
			
		||||
			this.marker = L.marker(this.snap_to_grid(event.latlng, k_marker_snap), {
 | 
			
		||||
				icon: L.divIcon({className: 'build-icon'}),
 | 
			
		||||
				draggable: true,
 | 
			
		||||
			}).addTo(this.leaflet);
 | 
			
		||||
			this.marker.on({click: this.on_marker_click.bind(this)});
 | 
			
		||||
			this.marker.on({drag: this.on_marker_move.bind(this)});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async update_map() {
 | 
			
		||||
		let map = this.shadowRoot.getElementById('map');
 | 
			
		||||
		if (!map || !this.loaded_activities.length) {
 | 
			
		||||
			this.leaflet = undefined;
 | 
			
		||||
			this.grid_layer = undefined;
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		if (!this.leaflet) {
 | 
			
		||||
			this.leaflet = L.map(map, {
 | 
			
		||||
				attributionControl: false,
 | 
			
		||||
				maxZoom: 16,
 | 
			
		||||
				bounceAtZoomLimits: false,
 | 
			
		||||
			});
 | 
			
		||||
			this.leaflet.on({contextmenu: this.on_click.bind(this)});
 | 
			
		||||
			this.leaflet.on({click: this.on_mouse_down.bind(this)});
 | 
			
		||||
			this.leaflet.on({zoom: this.on_zoom.bind(this)});
 | 
			
		||||
		}
 | 
			
		||||
		let self = this;
 | 
			
		||||
		let grid_layer = L.GridLayer.extend({
 | 
			
		||||
			createTile: function (coords) {
 | 
			
		||||
				var tile = L.DomUtil.create('canvas', 'leaflet-tile');
 | 
			
		||||
				var size = this.getTileSize();
 | 
			
		||||
				tile.width = size.x;
 | 
			
		||||
				tile.height = size.y;
 | 
			
		||||
				var context = tile.getContext('2d');
 | 
			
		||||
				context.font = '10pt sans';
 | 
			
		||||
				let bounds = this._tileCoordsToBounds(coords);
 | 
			
		||||
				let degrees = 360.0 / 2 ** coords.z;
 | 
			
		||||
				let ul = bounds.getNorthWest();
 | 
			
		||||
				let lr = bounds.getSouthEast();
 | 
			
		||||
 | 
			
		||||
				let mini = document.createElement('canvas');
 | 
			
		||||
				mini.width = Math.floor(size.x / 16.0);
 | 
			
		||||
				mini.height = Math.floor(size.y / 16.0);
 | 
			
		||||
				let mini_context = mini.getContext('2d');
 | 
			
		||||
				let image_data = context.getImageData(0, 0, mini.width, mini.height);
 | 
			
		||||
				for (let activity of self.loaded_activities) {
 | 
			
		||||
					self.draw_activity_to_tile(
 | 
			
		||||
						image_data,
 | 
			
		||||
						mini.width,
 | 
			
		||||
						mini.height,
 | 
			
		||||
						ul,
 | 
			
		||||
						lr,
 | 
			
		||||
						activity
 | 
			
		||||
					);
 | 
			
		||||
				}
 | 
			
		||||
				context.textAlign = 'left';
 | 
			
		||||
				context.textBaseline = 'bottom';
 | 
			
		||||
				for (let x = 0; x < mini.width; x++) {
 | 
			
		||||
					for (let y = 0; y < mini.height; y++) {
 | 
			
		||||
						let start = (y * mini.width + x) * 4;
 | 
			
		||||
						let pixel = self.color_to_emoji(
 | 
			
		||||
							image_data.data.slice(start, start + 4)
 | 
			
		||||
						);
 | 
			
		||||
						if (pixel) {
 | 
			
		||||
							//context.fillRect(x * size.x / mini.width, y * size.y / mini.height, size.x / mini.width, size.y / mini.height);
 | 
			
		||||
							context.fillText(
 | 
			
		||||
								pixel,
 | 
			
		||||
								(x * size.x) / mini.width,
 | 
			
		||||
								(y * size.y) / mini.height + mini.height
 | 
			
		||||
							);
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				for (let placed of self.placed_emojis) {
 | 
			
		||||
					let position = self.leaflet.options.crs.latLngToPoint(
 | 
			
		||||
						self.snap_to_grid(placed.position, undefined, coords.z),
 | 
			
		||||
						coords.z
 | 
			
		||||
					);
 | 
			
		||||
					let tile_x = Math.floor(position.x / size.x);
 | 
			
		||||
					let tile_y = Math.floor(position.y / size.y);
 | 
			
		||||
					position.x = position.x - tile_x * size.x;
 | 
			
		||||
					position.y = position.y - tile_y * size.y;
 | 
			
		||||
					if (tile_x == coords.x && tile_y == coords.y) {
 | 
			
		||||
						//context.fillRect(position.x, position.y, size.x / mini.width, size.y / mini.height);
 | 
			
		||||
						context.fillText(
 | 
			
		||||
							placed.emoji,
 | 
			
		||||
							position.x,
 | 
			
		||||
							position.y + mini.height
 | 
			
		||||
						);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				return tile;
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
		if (this.grid_layer) {
 | 
			
		||||
			this.grid_layer.redraw();
 | 
			
		||||
		} else {
 | 
			
		||||
			this.grid_layer = new grid_layer();
 | 
			
		||||
			this.grid_layer.addTo(this.leaflet);
 | 
			
		||||
		}
 | 
			
		||||
		for (let activity of this.loaded_activities) {
 | 
			
		||||
			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);
 | 
			
		||||
		}
 | 
			
		||||
		if (this.focus) {
 | 
			
		||||
			this.leaflet.fitBounds([this.focus.min, this.focus.max]);
 | 
			
		||||
			this.focus = undefined;
 | 
			
		||||
		} else {
 | 
			
		||||
			this.leaflet.fitBounds([
 | 
			
		||||
				[this.min_lat, this.min_lon],
 | 
			
		||||
				[this.max_lat, this.max_lon],
 | 
			
		||||
			]);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	activity_to_color(activity) {
 | 
			
		||||
		let color = [0, 0, 0, 255];
 | 
			
		||||
		switch (activity.sport_type) {
 | 
			
		||||
			/* Implies snow. */
 | 
			
		||||
			case 'AlpineSki':
 | 
			
		||||
			case 'BackcountrySki':
 | 
			
		||||
			case 'NordicSki':
 | 
			
		||||
			case 'Snowshoe':
 | 
			
		||||
			case 'Snowboard':
 | 
			
		||||
				color = k_color_snow;
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			/* Implies ice. */
 | 
			
		||||
			case 'IceSkate':
 | 
			
		||||
			case 'InlineSkate':
 | 
			
		||||
				color = k_color_ice;
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			/* Implies water. */
 | 
			
		||||
			case 'Canoeing':
 | 
			
		||||
			case 'Kayaking':
 | 
			
		||||
			case 'Kitesurf':
 | 
			
		||||
			case 'Rowing':
 | 
			
		||||
			case 'Sail':
 | 
			
		||||
			case 'StandUpPaddling':
 | 
			
		||||
			case 'Surfing':
 | 
			
		||||
			case 'Swim':
 | 
			
		||||
			case 'Windsurf':
 | 
			
		||||
				color = k_color_water;
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			/* Implies dirt. */
 | 
			
		||||
			case 'EMountainBikeRide':
 | 
			
		||||
			case 'Hike':
 | 
			
		||||
			case 'MountainBikeRide':
 | 
			
		||||
			case 'RockClimbing':
 | 
			
		||||
			case 'TrailRun':
 | 
			
		||||
				color = k_color_dirt;
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			/* Implies pavement. */
 | 
			
		||||
			case 'EBikeRide':
 | 
			
		||||
			case 'GravelRide':
 | 
			
		||||
			case 'Handcycle':
 | 
			
		||||
			case 'Ride':
 | 
			
		||||
			case 'RollerSki':
 | 
			
		||||
			case 'Run':
 | 
			
		||||
			case 'Skateboard':
 | 
			
		||||
			case 'Badminton':
 | 
			
		||||
			case 'Tennis':
 | 
			
		||||
			case 'Velomobile':
 | 
			
		||||
			case 'Walk':
 | 
			
		||||
			case 'Wheelchair':
 | 
			
		||||
				color = k_color_pavement;
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			/* Grass, maybe? */
 | 
			
		||||
			case 'Golf':
 | 
			
		||||
			case 'Soccer':
 | 
			
		||||
			case 'Squash':
 | 
			
		||||
				color = k_color_grass;
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			// Crossfit,
 | 
			
		||||
			// Elliptical
 | 
			
		||||
			// HighIntensityIntervalTraining
 | 
			
		||||
			// Pickleball
 | 
			
		||||
			// Pilates
 | 
			
		||||
			// Racquetball
 | 
			
		||||
			// StairStepper
 | 
			
		||||
			// TableTennis,
 | 
			
		||||
			// VirtualRide
 | 
			
		||||
			// VirtualRow
 | 
			
		||||
			// VirtualRun
 | 
			
		||||
			// WeightTraining
 | 
			
		||||
			// Workout
 | 
			
		||||
			// Yoga
 | 
			
		||||
			default:
 | 
			
		||||
				color = k_color_default;
 | 
			
		||||
		}
 | 
			
		||||
		return color;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	line(image_data, x0, y0, x1, y1, value) {
 | 
			
		||||
		/* <3 https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm */
 | 
			
		||||
		let dx = Math.abs(x1 - x0);
 | 
			
		||||
		let sx = x0 < x1 ? 1 : -1;
 | 
			
		||||
		let dy = -Math.abs(y1 - y0);
 | 
			
		||||
		let sy = y0 < y1 ? 1 : -1;
 | 
			
		||||
		let error = dx + dy;
 | 
			
		||||
		while (true) {
 | 
			
		||||
			if (
 | 
			
		||||
				x0 >= 0 &&
 | 
			
		||||
				y0 >= 0 &&
 | 
			
		||||
				x0 < image_data.width &&
 | 
			
		||||
				y0 < image_data.height
 | 
			
		||||
			) {
 | 
			
		||||
				let base = (y0 * image_data.width + x0) * 4;
 | 
			
		||||
				image_data.data[base + 0] = value[0];
 | 
			
		||||
				image_data.data[base + 1] = value[1];
 | 
			
		||||
				image_data.data[base + 2] = value[2];
 | 
			
		||||
				image_data.data[base + 3] = value[3];
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (x0 == x1 && y0 == y1) {
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			let e2 = 2 * error;
 | 
			
		||||
			if (e2 >= dy) {
 | 
			
		||||
				if (x0 == x1) {
 | 
			
		||||
					break;
 | 
			
		||||
				}
 | 
			
		||||
				error += dy;
 | 
			
		||||
				x0 = Math.round(x0 + sx);
 | 
			
		||||
			}
 | 
			
		||||
			if (e2 <= dx) {
 | 
			
		||||
				if (y0 == y1) {
 | 
			
		||||
					break;
 | 
			
		||||
				}
 | 
			
		||||
				error += dx;
 | 
			
		||||
				y0 = Math.round(y0 + sy);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	draw_activity_to_tile(image_data, width, height, ul, lr, activity) {
 | 
			
		||||
		let color = this.activity_to_color(activity);
 | 
			
		||||
		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;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if (activity?.segments) {
 | 
			
		||||
			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) {
 | 
			
		||||
		try {
 | 
			
		||||
			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);
 | 
			
		||||
			console.log(gpx);
 | 
			
		||||
			let message = {
 | 
			
		||||
				type: 'gg-activity',
 | 
			
		||||
				mentions: [
 | 
			
		||||
					{
 | 
			
		||||
						link: `https://${gpx.link}/activity/${gpx.time}`,
 | 
			
		||||
						name: 'activity_url',
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						link: blob_id,
 | 
			
		||||
						name: 'activity_data',
 | 
			
		||||
					},
 | 
			
		||||
				],
 | 
			
		||||
			};
 | 
			
		||||
			console.log('id =', this.whoami, 'message = ', message);
 | 
			
		||||
			let id = await tfrpc.rpc.appendMessage(this.whoami, message);
 | 
			
		||||
			console.log('appended message', id);
 | 
			
		||||
			alert('Activity uploaded.');
 | 
			
		||||
			await this.get_activities_from_ssb();
 | 
			
		||||
		} catch (e) {
 | 
			
		||||
			alert(`Error: ${JSON.stringify(e, null, 2)}`);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	upload() {
 | 
			
		||||
		let input = document.createElement('input');
 | 
			
		||||
		input.type = 'file';
 | 
			
		||||
		input.onchange = (event) => this.on_upload(event);
 | 
			
		||||
		input.click();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	updated() {
 | 
			
		||||
		this.update_map();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	focus_map(activity) {
 | 
			
		||||
		let bounds = this.activity_bounds(activity);
 | 
			
		||||
		if (bounds.min.lat < bounds.max.lat && bounds.min.lng < bounds.max.lng) {
 | 
			
		||||
			this.tab = 'map';
 | 
			
		||||
			this.focus = bounds;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_news() {
 | 
			
		||||
		return html`
 | 
			
		||||
			<ul>
 | 
			
		||||
				${this.loaded_activities.map(
 | 
			
		||||
					(x) => html`
 | 
			
		||||
						<li style="cursor: pointer" @click=${() => this.focus_map(x)}>
 | 
			
		||||
							${x.author} ${x.name ?? x.time}
 | 
			
		||||
						</li>
 | 
			
		||||
					`
 | 
			
		||||
				)}
 | 
			
		||||
			</ul>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_store_item(item) {
 | 
			
		||||
		let [emoji, cost] = item;
 | 
			
		||||
		return html`
 | 
			
		||||
			<div>
 | 
			
		||||
				<input type="button" value="${emoji}" @click=${() => (this.to_build = emoji)}></input> ${cost} ${emoji == this.to_build ? '<-- Will be built next' : undefined}
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_store() {
 | 
			
		||||
		let store = Object.assign({}, k_store);
 | 
			
		||||
		store[this.emoji_of_the_day] = 5;
 | 
			
		||||
		return html`
 | 
			
		||||
			<h2>Store</h2>
 | 
			
		||||
			<div><b>Your balance:</b> ${this.currency}</div>
 | 
			
		||||
			${Object.entries(store).map(this.render_store_item.bind(this))}
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		let header;
 | 
			
		||||
		if (!this.user?.credentials?.session?.name) {
 | 
			
		||||
			header = html`<div style="flex: 1 0">
 | 
			
		||||
				Please <a target="_top" href="/login?return=${this.url}">login</a> to
 | 
			
		||||
				Tilde Friends, first.
 | 
			
		||||
			</div>`;
 | 
			
		||||
		} else if (!this.strava?.access_token) {
 | 
			
		||||
			let strava_url = `https://www.strava.com/oauth/authorize?client_id=${k_client_id}&redirect_uri=${k_redirect_url}&response_type=code&approval_prompt=auto&scope=activity%3Aread&state=${g_data.state}`;
 | 
			
		||||
			header = html`
 | 
			
		||||
				<div style="flex: 1 0; display: flex; flex-direction: row; align-items: center; gap: 1em; width: 100%">
 | 
			
		||||
					<div style="flex: 1 1">Please <a target="_top" href=${strava_url}>login</a> to Strava.</div>
 | 
			
		||||
					<span style="font-size: xx-small; flex: 1 1; word-break: break-all">${this.whoami}</span>
 | 
			
		||||
					<input type="button" value="📁" @click=${this.upload}></input>
 | 
			
		||||
				</div>
 | 
			
		||||
			`;
 | 
			
		||||
		} else {
 | 
			
		||||
			header = html`
 | 
			
		||||
				<div>
 | 
			
		||||
					<div style="flex: 1 0; display: flex; flex-direction: row; align-items: center; gap: 1em; width: 100%">
 | 
			
		||||
						<h1>Welcome, ${this.user.credentials.session.name}</h1>
 | 
			
		||||
						<span style="font-size: xx-small; flex: 1 1; word-break: break-all">${this.whoami}</span>
 | 
			
		||||
						<input type="button" value="📁" @click=${this.upload}></input>
 | 
			
		||||
					</div>
 | 
			
		||||
					<h3 ?hidden=${!this.status?.text}>${this.status?.text} <progress ?hidden=${!this.status?.max} value=${this.status?.value} max=${this.status?.max}>${this.status?.value}</progress></h3>
 | 
			
		||||
				</div>
 | 
			
		||||
			`;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let navigation = html`
 | 
			
		||||
			<style>
 | 
			
		||||
				#navigation input[type="button"] {
 | 
			
		||||
					min-width: 3em;
 | 
			
		||||
					min-height: 3em;
 | 
			
		||||
					flex: 1 0;
 | 
			
		||||
					font-size: large;
 | 
			
		||||
				}
 | 
			
		||||
			</style>
 | 
			
		||||
			<div id="navigation" style="display: flex; flex-direction: row">
 | 
			
		||||
				<input type="button" id="button_map" @click=${() => (this.tab = 'map')} value="🗺️Map"></input>
 | 
			
		||||
				<input type="button" id="button_news" @click=${() => (this.tab = 'news')} value="🏃News"></input>
 | 
			
		||||
				<input type="button" id="button_friends" @click=${() => (this.tab = 'friends')} value="👫Friends"></input>
 | 
			
		||||
				<input type="button" id="button_store" @click=${() => (this.tab = 'store')} value="🏗️Store"></input>
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
 | 
			
		||||
		let content;
 | 
			
		||||
		switch (this.tab) {
 | 
			
		||||
			case 'map':
 | 
			
		||||
				content = html`<div id="map" style="width: 100%; height: 100%"></div>`;
 | 
			
		||||
				break;
 | 
			
		||||
			case 'news':
 | 
			
		||||
				content = this.render_news();
 | 
			
		||||
				break;
 | 
			
		||||
			case 'friends':
 | 
			
		||||
				content = html`<div>Friends</div>`;
 | 
			
		||||
				break;
 | 
			
		||||
			case 'store':
 | 
			
		||||
				content = this.render_store();
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return html`
 | 
			
		||||
			<style>
 | 
			
		||||
				.build-icon::before {
 | 
			
		||||
					content: '📍';
 | 
			
		||||
					border: 2px solid red;
 | 
			
		||||
				}
 | 
			
		||||
			</style>
 | 
			
		||||
			<link rel="stylesheet" href="leaflet.css" />
 | 
			
		||||
			<div
 | 
			
		||||
				style="width: 100%; height: 100%; display: flex; flex-direction: column"
 | 
			
		||||
			>
 | 
			
		||||
				${header}
 | 
			
		||||
				<div style="flex: 1 0; overflow: scroll">${content}</div>
 | 
			
		||||
				${navigation}
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
customElements.define('gg-app', GgAppElement);
 | 
			
		||||
@@ -1,20 +0,0 @@
 | 
			
		||||
const k_client_id = '28276';
 | 
			
		||||
const k_client_secret = '3123f1f5afe132d9731111066d1d17bdb22ef27e';
 | 
			
		||||
const k_access_token = 'f753e77764c26252bd2d80e7c5cc17ace51a8864';
 | 
			
		||||
const k_refresh_token = 'f58d8e1b5a3ec3bf96e681589d5014f9a294f5a4';
 | 
			
		||||
const k_redirect_url = 'https://tildefriends.net/~cory/gg/login';
 | 
			
		||||
 | 
			
		||||
export async function refresh_token(token) {
 | 
			
		||||
	let r = await fetch('https://www.strava.com/api/v3/oauth/token', {
 | 
			
		||||
		method: 'POST',
 | 
			
		||||
		body: `client_id=${k_client_id}&client_secret=${k_client_secret}&refresh_token=${token.refresh_token}&grant_type=refresh_token`,
 | 
			
		||||
	});
 | 
			
		||||
	return r?.body ? JSON.parse(utf8Decode(r.body)) : undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function authorization_code(code) {
 | 
			
		||||
	return await fetch('https://www.strava.com/api/v3/oauth/token', {
 | 
			
		||||
		method: 'POST',
 | 
			
		||||
		body: `client_id=${k_client_id}&client_secret=${k_client_secret}&code=${code}&grant_type=authorization_code`,
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user