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:
parent
5e2dfff148
commit
09c7c8ac64
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`,
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user