forked from cory/tildefriends
		
	GPS game.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4380 ed5197a5-7fde-0310-b194-c3ffbd925b24
This commit is contained in:
		
							
								
								
									
										79
									
								
								apps/gg/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								apps/gg/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| 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) { | ||||
| 	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(); | ||||
							
								
								
									
										20
									
								
								apps/gg/handler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								apps/gg/handler.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| import * as strava from './strava.js'; | ||||
|  | ||||
| async function main() { | ||||
| 	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, r.body); | ||||
| 	} | ||||
| 	await respond({ | ||||
| 		data: r.body, | ||||
| 		content_type: 'text/plain', | ||||
| 		headers: { | ||||
| 			Location: 'https://tildefriends.net/~cory/gg/', | ||||
| 		}, | ||||
| 		status_code: 307, | ||||
| 	}); | ||||
| } | ||||
| main(); | ||||
							
								
								
									
										16
									
								
								apps/gg/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								apps/gg/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| <!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> | ||||
| 		<link rel="stylesheet" href="leaflet.css"/> | ||||
| 		<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="flex: 0 1 auto; overflow: scroll"></gg-app> | ||||
| 		<div id="map" style="flex: 1 0"></div> | ||||
| 	</body> | ||||
| </html> | ||||
							
								
								
									
										661
									
								
								apps/gg/leaflet.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										661
									
								
								apps/gg/leaflet.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,661 @@ | ||||
| /* 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; | ||||
| 		} | ||||
| 	} | ||||
							
								
								
									
										6
									
								
								apps/gg/leaflet.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								apps/gg/leaflet.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										126
									
								
								apps/gg/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								apps/gg/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								apps/gg/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/gg/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										158
									
								
								apps/gg/polyline.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								apps/gg/polyline.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,158 @@ | ||||
| /** | ||||
|  * 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 }; | ||||
							
								
								
									
										423
									
								
								apps/gg/script.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										423
									
								
								apps/gg/script.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,423 @@ | ||||
| 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'; | ||||
|  | ||||
| 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]; | ||||
|  | ||||
| class GgAppElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			user: {type: Object}, | ||||
| 			strava: {type: Object}, | ||||
| 			activities: {type: Array}, | ||||
| 			activity: {type: Object}, | ||||
| 			world: {type: Object}, | ||||
| 			id: {type: String}, | ||||
| 			status: {type: Object}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		this.activities = []; | ||||
| 		this.activity = {}; | ||||
| 		this.loaded_activities = []; | ||||
| 		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.status = undefined; | ||||
| 		this.load().catch(function(e) { | ||||
| 			console.log('load error', e); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	async load() { | ||||
| 		console.log('load'); | ||||
| 		this.user = await tfrpc.rpc.getUser(); | ||||
| 		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.id && this.activities?.length) { | ||||
| 			await this.sync_activities(); | ||||
| 		} | ||||
| 		await this.get_activities_from_ssb(); | ||||
| 	} | ||||
|  | ||||
| 	async get_activities_from_ssb() { | ||||
| 		this.status = {text: 'loading activities'}; | ||||
| 		let blob_ids = await tfrpc.rpc.query(` | ||||
| 			SELECT 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 | ||||
| 		`, []); | ||||
| 		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))); | ||||
| 		} | ||||
| 		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.id, 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.id, 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.id = await tfrpc.rpc.createIdentity(); | ||||
| 			if (this.id) { | ||||
| 				await tfrpc.rpc.appendMessage(this.id, { | ||||
| 					type: 'gg-player', | ||||
| 					active: true, | ||||
| 				}); | ||||
| 			} | ||||
| 		} else { | ||||
| 			players.sort(); | ||||
| 			this.id = players[0]; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async update_credentials() { | ||||
| 		let name = this.user?.credentials?.session?.name; | ||||
| 		if (!name) { | ||||
| 			return; | ||||
| 		} | ||||
| 		let shared = await tfrpc.rpc.sharedDatabaseGet(name); | ||||
| 		if (shared) { | ||||
| 			console.log('shared =', 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]; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async update_map() { | ||||
| 		if (!this.leaflet) { | ||||
| 			this.leaflet = L.map('map', {attributionControl: false, maxZoom: 16, bounceAtZoomLimits: false}); | ||||
| 		} | ||||
| 		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(); | ||||
| 				//context.fillText(JSON.stringify(coords), 0, 12); | ||||
| 				//context.fillText(`${Math.round(ul.lat * 100) / 100} ${Math.round(ul.lng * 100) / 100}`, 0, 24); | ||||
| 				//context.fillText(`${Math.round(lr.lat * 100) / 100} ${Math.round(lr.lng * 100) / 100}`, 0, 36); | ||||
|  | ||||
| 				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); | ||||
| 				} | ||||
| 				//mini_context.putImageData(image_data, 0, 0); | ||||
| 				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.fillText(pixel, x * size.x / mini.width, y * size.y / mini.height + 10); | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 				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) { | ||||
| 			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]); | ||||
| 			} | ||||
| 		} | ||||
| 		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); | ||||
| 		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; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		if (!this.user?.credentials?.session?.name) { | ||||
| 			return html`<div>Please <a target="_top" href="/login">login</a> to Tilde Friends, first.</div>`; | ||||
| 		} | ||||
| 		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}`; | ||||
| 			return html`<div>Please <a target="_top" href=${strava_url}>login</a> to Strava.</div>`; | ||||
| 		} | ||||
|  | ||||
| 		return html` | ||||
| 			<h1>Welcome, ${this.user.credentials.session.name} <span style="font-size: xx-small">${this.id}</span></h1> | ||||
| 			<h3>${this.status?.text} <progress ?hidden=${!this.status?.max} value=${this.status?.value} max=${this.status?.max}>${this.status?.value}</progress></h3> | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
| customElements.define('gg-app', GgAppElement); | ||||
							
								
								
									
										20
									
								
								apps/gg/strava.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								apps/gg/strava.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| 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