2023-08-04 19:08:16 -04:00
|
|
|
|
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';
|
2023-08-06 08:03:22 -04:00
|
|
|
|
import {gpx_parse} from './gpx.js';
|
2023-08-04 19:08:16 -04:00
|
|
|
|
|
|
|
|
|
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];
|
|
|
|
|
|
2023-09-17 09:22:29 -04:00
|
|
|
|
const k_store = {
|
|
|
|
|
'🦞': 15,
|
|
|
|
|
'🛶': 10,
|
|
|
|
|
'🏠': 10,
|
|
|
|
|
};
|
|
|
|
|
|
2023-08-04 19:08:16 -04:00
|
|
|
|
class GgAppElement extends LitElement {
|
|
|
|
|
static get properties() {
|
|
|
|
|
return {
|
|
|
|
|
user: {type: Object},
|
|
|
|
|
strava: {type: Object},
|
|
|
|
|
activities: {type: Array},
|
|
|
|
|
activity: {type: Object},
|
|
|
|
|
world: {type: Object},
|
2023-09-17 09:22:29 -04:00
|
|
|
|
whoami: {type: String},
|
2023-08-04 19:08:16 -04:00
|
|
|
|
status: {type: Object},
|
2023-09-04 15:21:00 -04:00
|
|
|
|
tab: {type: String},
|
|
|
|
|
url: {type: String},
|
|
|
|
|
currency: {type: Number},
|
2023-09-17 09:22:29 -04:00
|
|
|
|
to_build: {type: String},
|
2023-08-04 19:08:16 -04:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
this.activities = [];
|
|
|
|
|
this.activity = {};
|
|
|
|
|
this.loaded_activities = [];
|
2023-09-17 09:22:29 -04:00
|
|
|
|
this.placed_emojis = [];
|
2023-08-04 19:08:16 -04:00
|
|
|
|
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;
|
2023-09-04 15:21:00 -04:00
|
|
|
|
this.focus = undefined;
|
2023-08-04 19:08:16 -04:00
|
|
|
|
this.status = undefined;
|
2023-09-04 15:21:00 -04:00
|
|
|
|
this.tab = 'map';
|
2023-08-04 19:08:16 -04:00
|
|
|
|
this.load().catch(function(e) {
|
|
|
|
|
console.log('load error', e);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async load() {
|
|
|
|
|
console.log('load');
|
|
|
|
|
this.user = await tfrpc.rpc.getUser();
|
2023-09-04 15:21:00 -04:00
|
|
|
|
this.url = (await tfrpc.rpc.url()).split('?')[0];
|
2023-08-04 19:08:16 -04:00
|
|
|
|
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();
|
2023-09-17 09:22:29 -04:00
|
|
|
|
if (this.whoami && this.activities?.length) {
|
2023-08-04 19:08:16 -04:00
|
|
|
|
await this.sync_activities();
|
|
|
|
|
}
|
|
|
|
|
await this.get_activities_from_ssb();
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-04 14:01:06 -04:00
|
|
|
|
/* https://gist.github.com/jcouyang/632709f30e12a7879a73e9e132c0d56b?permalink_comment_id=3591045#gistcomment-3591045 */
|
|
|
|
|
async promise_all(promises, max_concurrent) {
|
|
|
|
|
let index = 0;
|
|
|
|
|
let results = [];
|
|
|
|
|
async function exec_thread() {
|
|
|
|
|
while (index < promises.length) {
|
|
|
|
|
const current = index++;
|
|
|
|
|
results[current] = await promises[current];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const threads = [];
|
|
|
|
|
for (let thread = 0; thread < max_concurrent; thread++) {
|
|
|
|
|
threads.push(exec_thread());
|
|
|
|
|
}
|
|
|
|
|
await Promise.all(threads);
|
|
|
|
|
return results;
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-04 19:08:16 -04:00
|
|
|
|
async get_activities_from_ssb() {
|
|
|
|
|
this.status = {text: 'loading activities'};
|
2023-08-12 14:37:15 -04:00
|
|
|
|
this.loaded_activities = [];
|
2023-09-04 15:21:00 -04:00
|
|
|
|
let rows = await tfrpc.rpc.query(`
|
|
|
|
|
SELECT messages.author, json_extract(mention.value, '$.link') AS blob_id
|
2023-08-04 19:08:16 -04:00
|
|
|
|
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
|
|
|
|
|
`, []);
|
2023-09-04 14:01:06 -04:00
|
|
|
|
this.status = {text: 'loading activity data'};
|
2023-09-04 15:21:00 -04:00
|
|
|
|
let authors = rows.map(x => x.author);
|
|
|
|
|
let blobs = await this.promise_all(rows.map(x => tfrpc.rpc.get_blob(x.blob_id)), 8);
|
2023-09-04 14:01:06 -04:00
|
|
|
|
this.status = {text: 'processing activity data'};
|
2023-09-04 15:21:00 -04:00
|
|
|
|
for (let [index, blob] of blobs.entries()) {
|
|
|
|
|
let activity;
|
2023-08-06 08:03:22 -04:00
|
|
|
|
try {
|
2023-09-04 15:21:00 -04:00
|
|
|
|
activity = JSON.parse(blob);
|
2023-08-06 08:03:22 -04:00
|
|
|
|
} catch {
|
2023-09-04 15:21:00 -04:00
|
|
|
|
activity = gpx_parse(blob);
|
|
|
|
|
}
|
|
|
|
|
if (activity) {
|
|
|
|
|
activity.author = authors[index];
|
|
|
|
|
this.loaded_activities.push(activity);
|
2023-08-06 08:03:22 -04:00
|
|
|
|
}
|
2023-08-04 19:08:16 -04:00
|
|
|
|
}
|
2023-09-04 15:21:00 -04:00
|
|
|
|
this.status = {text: 'calculating balance'};
|
|
|
|
|
rows = await tfrpc.rpc.query(`
|
|
|
|
|
SELECT count(*) AS currency FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-activity'
|
2023-09-17 09:22:29 -04:00
|
|
|
|
`, [this.whoami]);
|
|
|
|
|
let currency = rows[0].currency;
|
|
|
|
|
rows = await tfrpc.rpc.query(`
|
|
|
|
|
SELECT SUM(json_extract(content, '$.cost')) AS cost FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-place'
|
|
|
|
|
`, [this.whoami]);
|
|
|
|
|
let spent = rows[0].cost;
|
|
|
|
|
this.currency = currency - spent;
|
|
|
|
|
this.status = {text: 'getting placed emojis'};
|
|
|
|
|
rows = await tfrpc.rpc.query(`
|
|
|
|
|
SELECT messages.content
|
|
|
|
|
FROM messages_fts('"gg-place"')
|
|
|
|
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
|
|
|
|
WHERE json_extract(messages.content, '$.type') = 'gg-place'
|
|
|
|
|
ORDER BY messages.timestamp
|
|
|
|
|
`);
|
|
|
|
|
for (let row of rows) {
|
|
|
|
|
console.log(row.content);
|
|
|
|
|
let content = JSON.parse(row.content);
|
|
|
|
|
this.placed_emojis.push({
|
|
|
|
|
position: content.position,
|
|
|
|
|
emoji: content.emoji,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
console.log(this.placed_emojis);
|
2023-08-04 19:08:16 -04:00
|
|
|
|
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
|
2023-09-17 09:22:29 -04:00
|
|
|
|
`, [this.whoami, JSON.stringify(ids)]);
|
2023-08-04 19:08:16 -04:00
|
|
|
|
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',
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
};
|
2023-09-17 09:22:29 -04:00
|
|
|
|
await tfrpc.rpc.appendMessage(this.whoami, message);
|
2023-08-04 19:08:16 -04:00
|
|
|
|
}
|
|
|
|
|
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) {
|
2023-09-17 09:22:29 -04:00
|
|
|
|
this.whoami = await tfrpc.rpc.createIdentity();
|
|
|
|
|
if (this.whoami) {
|
|
|
|
|
await tfrpc.rpc.appendMessage(this.whoami, {
|
2023-08-04 19:08:16 -04:00
|
|
|
|
type: 'gg-player',
|
|
|
|
|
active: true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
players.sort();
|
2023-09-17 09:22:29 -04:00
|
|
|
|
this.whoami = players[0];
|
2023-08-04 19:08:16 -04:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async update_credentials() {
|
|
|
|
|
let name = this.user?.credentials?.session?.name;
|
|
|
|
|
if (!name) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let shared = await tfrpc.rpc.sharedDatabaseGet(name);
|
|
|
|
|
if (shared) {
|
|
|
|
|
await tfrpc.rpc.databaseSet('strava', shared);
|
|
|
|
|
await tfrpc.rpc.sharedDatabaseRemove(name);
|
|
|
|
|
}
|
|
|
|
|
this.strava = JSON.parse(await tfrpc.rpc.databaseGet('strava') || '{}');
|
|
|
|
|
if (new Date().valueOf() / 1000 > this.strava.expires_at) {
|
|
|
|
|
console.log('this looks expired', new Date().valueOf() / 1000, '>', this.strava.expires_at);
|
|
|
|
|
let x = await tfrpc.rpc.refresh_token(this.strava);
|
|
|
|
|
if (x) {
|
|
|
|
|
this.strava = x;
|
|
|
|
|
await tfrpc.rpc.databaseSet('strava', JSON.stringify(x));
|
|
|
|
|
} else {
|
|
|
|
|
this.strava = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async update_activities() {
|
|
|
|
|
if (this?.strava?.access_token) {
|
|
|
|
|
let response = await fetch('https://www.strava.com/api/v3/athlete/activities', {
|
|
|
|
|
headers: {
|
|
|
|
|
'Authorization': `Bearer ${this.strava.access_token}`,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
this.activities = await response.json();
|
|
|
|
|
this.activities.sort((a, b) => (a.id - b.id));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
color_to_emoji(color) {
|
|
|
|
|
const k_map = [
|
|
|
|
|
[k_color_snow, '⬜'],
|
|
|
|
|
[k_color_ice, '🟦'],
|
|
|
|
|
[k_color_water, '🟦'],
|
|
|
|
|
[k_color_dirt, '🟫'],
|
|
|
|
|
[k_color_pavement, '⬛'],
|
|
|
|
|
[k_color_grass, '🟩'],
|
|
|
|
|
[k_color_default, '🟧'],
|
|
|
|
|
];
|
|
|
|
|
for (let m of k_map) {
|
|
|
|
|
if (m[0][0] == color[0] &&
|
|
|
|
|
m[0][1] == color[1] &&
|
|
|
|
|
m[0][2] == color[2] &&
|
|
|
|
|
m[0][3] == color[3]) {
|
|
|
|
|
return m[1];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-06 08:03:22 -04:00
|
|
|
|
activity_bounds(activity) {
|
|
|
|
|
let min_lat = Number.MAX_VALUE;
|
|
|
|
|
let min_lon = Number.MAX_VALUE;
|
|
|
|
|
let max_lat = -Number.MAX_VALUE;
|
|
|
|
|
let max_lon = -Number.MAX_VALUE;
|
|
|
|
|
if (activity?.map?.polyline) {
|
|
|
|
|
for (let pt of polyline.decode(activity.map.polyline)) {
|
|
|
|
|
min_lat = Math.min(min_lat, pt[0]);
|
|
|
|
|
min_lon = Math.min(min_lon, pt[1]);
|
|
|
|
|
max_lat = Math.max(max_lat, pt[0]);
|
|
|
|
|
max_lon = Math.max(max_lon, pt[1]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (activity?.segments) {
|
|
|
|
|
for (let segment of activity.segments) {
|
|
|
|
|
for (let pt of segment) {
|
|
|
|
|
min_lat = Math.min(min_lat, pt.lat);
|
|
|
|
|
min_lon = Math.min(min_lon, pt.lon);
|
|
|
|
|
max_lat = Math.max(max_lat, pt.lat);
|
|
|
|
|
max_lon = Math.max(max_lon, pt.lon);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
min: {
|
|
|
|
|
lat: min_lat,
|
|
|
|
|
lng: min_lon,
|
|
|
|
|
},
|
|
|
|
|
max: {
|
|
|
|
|
lat: max_lat,
|
|
|
|
|
lng: max_lon,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-04 13:44:05 -04:00
|
|
|
|
on_click(event) {
|
|
|
|
|
let popup = L.popup()
|
|
|
|
|
.setLatLng(event.latlng)
|
|
|
|
|
.setContent(`
|
2023-09-13 19:01:15 -04:00
|
|
|
|
<div><a target="_top" href="https://www.google.com/maps/search/?api=1&query=${event.latlng.lat},${event.latlng.lng}">${event.latlng.lat}, ${event.latlng.lng}</a></div>
|
2023-09-04 13:44:05 -04:00
|
|
|
|
`)
|
|
|
|
|
.openOn(this.leaflet);
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-17 09:22:29 -04:00
|
|
|
|
async build() {
|
|
|
|
|
if (this.popup) {
|
|
|
|
|
this.popup.remove();
|
|
|
|
|
}
|
|
|
|
|
if (!this.marker) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let latlng = this.marker.getLatLng();
|
|
|
|
|
|
|
|
|
|
let cost = k_store[this.to_build];
|
|
|
|
|
if (cost > this.currency) {
|
|
|
|
|
alert('Insufficient funds.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let message = {
|
|
|
|
|
type: 'gg-place',
|
|
|
|
|
position: {lat: latlng.lat, lng: latlng.lng},
|
|
|
|
|
emoji: this.to_build,
|
|
|
|
|
cost: cost,
|
|
|
|
|
};
|
|
|
|
|
let id = await tfrpc.rpc.appendMessage(this.whoami, message);
|
|
|
|
|
this.marker.remove();
|
|
|
|
|
this.placed_emojis.push({
|
|
|
|
|
position: {lat: latlng.lat, lng: latlng.lng},
|
|
|
|
|
emoji: this.to_build,
|
|
|
|
|
});
|
|
|
|
|
this.currency -= cost;
|
|
|
|
|
return this.update_map();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
on_marker_click(event) {
|
|
|
|
|
this.popup = L.popup()
|
|
|
|
|
.setLatLng(event.latlng)
|
|
|
|
|
.setContent(`
|
|
|
|
|
${this.to_build} (-${k_store[this.to_build]}) <input type="button" value="Build" onclick="document.getElementById('ggapp').build()"></input>
|
|
|
|
|
`)
|
|
|
|
|
.openOn(this.leaflet);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
on_mouse_down(event) {
|
|
|
|
|
if (this.marker) {
|
|
|
|
|
this.marker.remove();
|
|
|
|
|
this.marker = undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.to_build) {
|
|
|
|
|
this.marker = L.marker(event.latlng, {icon: L.divIcon({className: 'build-icon'}), draggable: true}).addTo(this.leaflet);
|
|
|
|
|
this.marker.on({click: this.on_marker_click.bind(this)});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-04 19:08:16 -04:00
|
|
|
|
async update_map() {
|
2023-09-04 15:21:00 -04:00
|
|
|
|
let map = this.shadowRoot.getElementById('map');
|
|
|
|
|
if (!map || !this.loaded_activities.length) {
|
|
|
|
|
this.leaflet = undefined;
|
|
|
|
|
this.grid_layer = undefined;
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-08-04 19:08:16 -04:00
|
|
|
|
if (!this.leaflet) {
|
2023-09-04 15:21:00 -04:00
|
|
|
|
this.leaflet = L.map(map, {attributionControl: false, maxZoom: 16, bounceAtZoomLimits: false});
|
2023-09-04 15:26:48 -04:00
|
|
|
|
this.leaflet.on({contextmenu: this.on_click.bind(this)});
|
2023-09-17 09:22:29 -04:00
|
|
|
|
this.leaflet.on({click: this.on_mouse_down.bind(this)});
|
2023-08-04 19:08:16 -04:00
|
|
|
|
}
|
|
|
|
|
let self = this;
|
|
|
|
|
let grid_layer = L.GridLayer.extend({
|
|
|
|
|
createTile: function(coords) {
|
|
|
|
|
var tile = L.DomUtil.create('canvas', 'leaflet-tile');
|
|
|
|
|
var size = this.getTileSize();
|
|
|
|
|
tile.width = size.x;
|
|
|
|
|
tile.height = size.y;
|
|
|
|
|
var context = tile.getContext('2d');
|
|
|
|
|
context.font = '10pt sans';
|
|
|
|
|
let bounds = this._tileCoordsToBounds(coords);
|
|
|
|
|
let degrees = 360.0 / (2 ** coords.z);
|
|
|
|
|
let ul = bounds.getNorthWest();
|
|
|
|
|
let lr = bounds.getSouthEast();
|
|
|
|
|
|
|
|
|
|
let mini = document.createElement('canvas');
|
|
|
|
|
mini.width = Math.floor(size.x / 16.0);
|
|
|
|
|
mini.height = Math.floor(size.y / 16.0);
|
|
|
|
|
let mini_context = mini.getContext('2d');
|
|
|
|
|
let image_data = context.getImageData(0, 0, mini.width, mini.height);
|
|
|
|
|
for (let activity of self.loaded_activities) {
|
|
|
|
|
self.draw_activity_to_tile(image_data, mini.width, mini.height, ul, lr, activity);
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-09-17 09:22:29 -04:00
|
|
|
|
for (let placed of self.placed_emojis) {
|
|
|
|
|
let position = self.leaflet.options.crs.latLngToPoint(placed.position, coords.z);
|
|
|
|
|
let tile_x = Math.floor(position.x / size.x);
|
|
|
|
|
let tile_y = Math.floor(position.y / size.y);
|
|
|
|
|
position.x = position.x - tile_x * size.x;
|
|
|
|
|
position.y = position.y - tile_y * size.y;
|
|
|
|
|
if (tile_x == coords.x && tile_y == coords.y) {
|
|
|
|
|
context.fillText(placed.emoji, position.x, position.y + 10);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-04 19:08:16 -04:00
|
|
|
|
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) {
|
2023-08-06 08:03:22 -04:00
|
|
|
|
let bounds = this.activity_bounds(activity);
|
|
|
|
|
this.min_lat = Math.min(this.min_lat, bounds.min.lat);
|
|
|
|
|
this.min_lon = Math.min(this.min_lon, bounds.min.lng);
|
|
|
|
|
this.max_lat = Math.max(this.max_lat, bounds.max.lat);
|
|
|
|
|
this.max_lon = Math.max(this.max_lon, bounds.max.lng);
|
2023-08-04 19:08:16 -04:00
|
|
|
|
}
|
2023-09-04 15:21:00 -04:00
|
|
|
|
if (this.focus) {
|
|
|
|
|
this.leaflet.fitBounds([
|
|
|
|
|
this.focus.min,
|
|
|
|
|
this.focus.max,
|
|
|
|
|
]);
|
|
|
|
|
this.focus = undefined;
|
|
|
|
|
} else {
|
|
|
|
|
this.leaflet.fitBounds([
|
|
|
|
|
[this.min_lat, this.min_lon],
|
|
|
|
|
[this.max_lat, this.max_lon],
|
|
|
|
|
]);
|
|
|
|
|
}
|
2023-08-04 19:08:16 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
2023-08-06 08:03:22 -04:00
|
|
|
|
if (activity?.map?.polyline) {
|
|
|
|
|
let last;
|
|
|
|
|
for (let pt of polyline.decode(activity.map.polyline)) {
|
|
|
|
|
let px = [
|
|
|
|
|
Math.floor(width * (pt[1] - ul.lng) / (lr.lng - ul.lng)),
|
|
|
|
|
Math.floor(height * (pt[0] - ul.lat) / (lr.lat - ul.lat)),
|
|
|
|
|
];
|
|
|
|
|
if (last) {
|
|
|
|
|
this.line(image_data, last[0], last[1], px[0], px[1], color);
|
|
|
|
|
}
|
|
|
|
|
last = px;
|
2023-08-04 19:08:16 -04:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-06 08:03:22 -04:00
|
|
|
|
if (activity?.segments) {
|
|
|
|
|
for (let segment of activity.segments) {
|
|
|
|
|
let last;
|
|
|
|
|
for (let pt of segment) {
|
|
|
|
|
let px = [
|
|
|
|
|
Math.floor(width * (pt.lon - ul.lng) / (lr.lng - ul.lng)),
|
|
|
|
|
Math.floor(height * (pt.lat - ul.lat) / (lr.lat - ul.lat)),
|
|
|
|
|
];
|
|
|
|
|
if (last) {
|
|
|
|
|
this.line(image_data, last[0], last[1], px[0], px[1], color);
|
|
|
|
|
}
|
|
|
|
|
last = px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async on_upload(event) {
|
|
|
|
|
try {
|
2023-08-12 14:37:15 -04:00
|
|
|
|
let file = event.srcElement.files[0];
|
|
|
|
|
let xml = await file.text();
|
|
|
|
|
let gpx = gpx_parse(xml);
|
|
|
|
|
let blob_id = await tfrpc.rpc.store_blob(xml);
|
|
|
|
|
console.log('blob_id = ', blob_id);
|
|
|
|
|
console.log(gpx);
|
|
|
|
|
let message = {
|
|
|
|
|
type: 'gg-activity',
|
|
|
|
|
mentions: [
|
|
|
|
|
{
|
|
|
|
|
link: `https://${gpx.link}/activity/${gpx.time}`,
|
|
|
|
|
name: 'activity_url',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
link: blob_id,
|
|
|
|
|
name: 'activity_data',
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
};
|
2023-09-17 09:22:29 -04:00
|
|
|
|
console.log('id =', this.whoami, 'message = ', message);
|
|
|
|
|
let id = await tfrpc.rpc.appendMessage(this.whoami, message);
|
2023-08-06 08:03:22 -04:00
|
|
|
|
console.log('appended message', id);
|
2023-08-12 14:37:15 -04:00
|
|
|
|
alert('Activity uploaded.');
|
|
|
|
|
await this.get_activities_from_ssb();
|
2023-08-06 08:03:22 -04:00
|
|
|
|
} catch (e) {
|
2023-08-12 14:37:15 -04:00
|
|
|
|
alert(`Error: ${JSON.stringify(e, null, 2)}`);
|
2023-08-06 08:03:22 -04:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
upload() {
|
|
|
|
|
let input = document.createElement('input');
|
|
|
|
|
input.type = 'file';
|
2023-08-12 14:37:15 -04:00
|
|
|
|
input.onchange = (event) => this.on_upload(event);
|
2023-08-06 08:03:22 -04:00
|
|
|
|
input.click();
|
2023-08-04 19:08:16 -04:00
|
|
|
|
}
|
|
|
|
|
|
2023-09-04 15:21:00 -04:00
|
|
|
|
updated() {
|
|
|
|
|
this.update_map();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
focus_map(activity) {
|
|
|
|
|
let bounds = this.activity_bounds(activity);
|
|
|
|
|
if (bounds.min.lat < bounds.max.lat &&
|
|
|
|
|
bounds.min.lng < bounds.max.lng) {
|
|
|
|
|
this.tab = 'map';
|
|
|
|
|
this.focus = bounds;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render_news() {
|
|
|
|
|
return html`
|
|
|
|
|
<ul>
|
|
|
|
|
${this.loaded_activities.map(x => html`
|
|
|
|
|
<li style="cursor: pointer" @click=${() => this.focus_map(x)}>${x.author} ${x.name ?? x.time}</li>
|
|
|
|
|
`)}
|
|
|
|
|
</ul>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-17 09:22:29 -04:00
|
|
|
|
render_store_item(item) {
|
|
|
|
|
let [emoji, cost] = item;
|
|
|
|
|
return html`
|
|
|
|
|
<div>
|
|
|
|
|
<input type="button" value="${emoji}" @click=${() => this.to_build = emoji}></input> ${cost} ${emoji == this.to_build ? '<-- Will be built next' : undefined}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-04 15:21:00 -04:00
|
|
|
|
render_store() {
|
|
|
|
|
return html`
|
|
|
|
|
<h2>Store</h2>
|
|
|
|
|
<div><b>Your balance:</b> ${this.currency}</div>
|
2023-09-17 09:22:29 -04:00
|
|
|
|
${Object.entries(k_store).map(this.render_store_item.bind(this))}
|
2023-09-04 15:21:00 -04:00
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-04 19:08:16 -04:00
|
|
|
|
render() {
|
2023-09-04 15:21:00 -04:00
|
|
|
|
let header;
|
2023-08-04 19:08:16 -04:00
|
|
|
|
if (!this.user?.credentials?.session?.name) {
|
2023-09-04 15:21:00 -04:00
|
|
|
|
header = html`<div style="flex: 1 0">Please <a target="_top" href="/login?return=${this.url}">login</a> to Tilde Friends, first.</div>`;
|
|
|
|
|
} else if (!this.strava?.access_token) {
|
2023-08-04 19:08:16 -04:00
|
|
|
|
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}`;
|
2023-09-04 15:21:00 -04:00
|
|
|
|
header = html`
|
|
|
|
|
<div style="flex: 1 0; display: flex; flex-direction: row; align-items: center; gap: 1em; width: 100%">
|
2023-08-12 14:37:15 -04:00
|
|
|
|
<div style="flex: 1 1">Please <a target="_top" href=${strava_url}>login</a> to Strava.</div>
|
2023-09-17 09:22:29 -04:00
|
|
|
|
<span style="font-size: xx-small; flex: 1 1; word-break: break-all">${this.whoami}</span>
|
2023-08-12 14:37:15 -04:00
|
|
|
|
<input type="button" value="📁" @click=${this.upload}></input>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
2023-09-04 15:21:00 -04:00
|
|
|
|
} else {
|
|
|
|
|
header = html`
|
|
|
|
|
<div>
|
|
|
|
|
<div style="flex: 1 0; display: flex; flex-direction: row; align-items: center; gap: 1em; width: 100%">
|
|
|
|
|
<h1>Welcome, ${this.user.credentials.session.name}</h1>
|
2023-09-17 09:22:29 -04:00
|
|
|
|
<span style="font-size: xx-small; flex: 1 1; word-break: break-all">${this.whoami}</span>
|
2023-09-04 15:21:00 -04:00
|
|
|
|
<input type="button" value="📁" @click=${this.upload}></input>
|
|
|
|
|
</div>
|
|
|
|
|
<h3 ?hidden=${!this.status?.text}>${this.status?.text} <progress ?hidden=${!this.status?.max} value=${this.status?.value} max=${this.status?.max}>${this.status?.value}</progress></h3>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let navigation = html`
|
|
|
|
|
<style>
|
|
|
|
|
#navigation input[type="button"] {
|
|
|
|
|
min-width: 3em;
|
|
|
|
|
min-height: 3em;
|
|
|
|
|
flex: 1 0;
|
|
|
|
|
font-size: large;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
<div id="navigation" style="display: flex; flex-direction: row">
|
|
|
|
|
<input type="button" id="button_map" @click=${() => this.tab = 'map'} value="🗺️Map"></input>
|
|
|
|
|
<input type="button" id="button_news" @click=${() => this.tab = 'news'} value="🏃News"></input>
|
|
|
|
|
<input type="button" id="button_friends" @click=${() => this.tab = 'friends'} value="👫Friends"></input>
|
|
|
|
|
<input type="button" id="button_store" @click=${() => this.tab = 'store'} value="🏗️Store"></input>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
let content;
|
|
|
|
|
switch (this.tab) {
|
|
|
|
|
case 'map':
|
|
|
|
|
content = html`<div id="map" style="width: 100%; height: 100%"></div>`;
|
|
|
|
|
break;
|
|
|
|
|
case 'news':
|
|
|
|
|
content = this.render_news();
|
|
|
|
|
break;
|
|
|
|
|
case 'friends':
|
|
|
|
|
content = html`<div>Friends</div>`;
|
|
|
|
|
break;
|
|
|
|
|
case 'store':
|
|
|
|
|
content = this.render_store();
|
|
|
|
|
break;
|
2023-08-04 19:08:16 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return html`
|
2023-09-17 09:22:29 -04:00
|
|
|
|
<style>
|
|
|
|
|
.build-icon::before {
|
|
|
|
|
content: '📍';
|
|
|
|
|
border: 2px solid red;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
2023-09-04 15:21:00 -04:00
|
|
|
|
<link rel="stylesheet" href="leaflet.css"/>
|
|
|
|
|
<div style="width: 100%; height: 100%; display: flex; flex-direction: column">
|
|
|
|
|
${header}
|
|
|
|
|
<div style="flex: 1 0; overflow: scroll">${content}</div>
|
|
|
|
|
${navigation}
|
2023-08-06 08:03:22 -04:00
|
|
|
|
</div>
|
2023-08-04 19:08:16 -04:00
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
customElements.define('gg-app', GgAppElement);
|