Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8c0aefbd6
|
||
|
|
9c18cc19b7
|
||
|
|
1dc218ca8a
|
||
|
|
bfd9b4fdf6
|
||
|
|
67707b7ded
|
||
|
|
9c96037e32
|
||
|
|
fc4c63b519
|
||
|
|
fe1c9f6300
|
||
|
|
0d66bc1266
|
||
|
|
ba3982d9a6
|
||
|
|
f2cc9a9783
|
||
|
|
0c1aeebdf7
|
||
|
|
3324a57206
|
||
|
|
cf8e6882f8
|
||
|
|
6c4959deda
|
182553
data/legacy-route.json
Normal file
182553
data/legacy-route.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
11
dist/assets/index.098b1da4.js
vendored
11
dist/assets/index.098b1da4.js
vendored
File diff suppressed because one or more lines are too long
11
dist/assets/index.8fd3a327.js
vendored
Normal file
11
dist/assets/index.8fd3a327.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
@@ -6,11 +6,13 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Road2Bitcoin Live Map</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css">
|
||||
<script type="module" crossorigin src="/assets/index.098b1da4.js"></script>
|
||||
<script defer data-domain="r2b22.kip.pe" src="https://plausible.io/js/plausible.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index.8fd3a327.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index.eed9f443.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"><div id="popup"></div></div>
|
||||
<div id="people"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Road2Bitcoin Live Map</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css">
|
||||
<script defer data-domain="r2b22.kip.pe" src="https://plausible.io/js/plausible.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"><div id="popup"></div></div>
|
||||
<div id="people"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script type="module" src="./main.js"></script>
|
||||
</body>
|
||||
|
||||
487
main.js
487
main.js
@@ -7,263 +7,282 @@ import Point from 'ol/geom/Point';
|
||||
import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer';
|
||||
import {Circle as CircleStyle, Fill, Icon, Stroke, Style} from 'ol/style';
|
||||
import {OSM, Vector as VectorSource} from 'ol/source';
|
||||
import {useGeographic, fromLonLat} from 'ol/proj';
|
||||
import geojsonRoute from './geo/route.json'
|
||||
import geojsonPOI from './geo/poi.json'
|
||||
import {useGeographic} from 'ol/proj';
|
||||
import geojsonRoute from './data/r2b22-route.json'
|
||||
import geojsonPOI from './data/r2b22-poi.json';
|
||||
import geojsonLegacy from './data/legacy-route.json';
|
||||
|
||||
useGeographic();
|
||||
|
||||
const styles = {
|
||||
lineOrange: new Style({
|
||||
stroke: new Stroke({
|
||||
color: '#FF9900',
|
||||
// lineDash: [8],
|
||||
width: 5,
|
||||
}),
|
||||
}),
|
||||
lineGrey: new Style({
|
||||
stroke: new Stroke({
|
||||
color: '#555555',
|
||||
// lineDash: [8],
|
||||
width: 5,
|
||||
}),
|
||||
}),
|
||||
iconStop: new Style({
|
||||
image: new Icon({
|
||||
anchor: [0.5, 46],
|
||||
anchorXUnits: 'fraction',
|
||||
anchorYUnits: 'pixels',
|
||||
src: '/img/icon.png',
|
||||
}),
|
||||
}),
|
||||
iconVan: new Style({
|
||||
image: new Icon({
|
||||
anchor: [0.5, 16],
|
||||
anchorXUnits: 'fraction',
|
||||
anchorYUnits: 'pixels',
|
||||
src: '/img/van-100px.png',
|
||||
}),
|
||||
}),
|
||||
circleBlack: new Style({
|
||||
image: new CircleStyle({
|
||||
radius: 7,
|
||||
fill: new Fill({color: '#FF9900'}),
|
||||
async function main() {
|
||||
const styles = {
|
||||
lineOrange: new Style({
|
||||
stroke: new Stroke({
|
||||
color: 'white',
|
||||
width: 2,
|
||||
color: '#FF9900',
|
||||
// lineDash: [8],
|
||||
width: 5,
|
||||
}),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// Route
|
||||
//
|
||||
|
||||
const lastStageFinished = 1;
|
||||
const stagesCompleted = geojsonRoute.features.slice(0, lastStageFinished);
|
||||
const stagesAhead = geojsonRoute.features.slice(lastStageFinished);
|
||||
const vectorSourceStagesCompleted = new VectorSource();
|
||||
const vectorSourceStagesAhead = new VectorSource();
|
||||
|
||||
for (const stage of stagesCompleted) {
|
||||
vectorSourceStagesCompleted.addFeature(new GeoJSON().readFeature(stage));
|
||||
}
|
||||
for (const stage of stagesAhead) {
|
||||
vectorSourceStagesAhead.addFeature(new GeoJSON().readFeature(stage));
|
||||
}
|
||||
|
||||
const stagesCompletedLayer = new VectorLayer({
|
||||
source: vectorSourceStagesCompleted,
|
||||
style: styles.lineOrange
|
||||
});
|
||||
|
||||
const stagesAheadLayer = new VectorLayer({
|
||||
source: vectorSourceStagesAhead,
|
||||
style: styles.lineGrey
|
||||
});
|
||||
|
||||
//
|
||||
// Points of Interest
|
||||
//
|
||||
|
||||
const vectorSourcePOI = new VectorSource({
|
||||
features: new GeoJSON().readFeatures(geojsonPOI),
|
||||
});
|
||||
|
||||
const poiLayer = new VectorLayer({
|
||||
source: vectorSourcePOI,
|
||||
style: styles.circleBlack,
|
||||
});
|
||||
|
||||
const vectorSourceTrackedPoints = new VectorSource();
|
||||
|
||||
const vanFeature= new Feature({
|
||||
geometry: new Point([8.918618, 44.407408]),
|
||||
name: 'Support Van',
|
||||
trackable: true
|
||||
});
|
||||
|
||||
vectorSourceTrackedPoints.addFeature(vanFeature);
|
||||
|
||||
const trackedPointsLayer = new VectorLayer({
|
||||
source: vectorSourceTrackedPoints,
|
||||
style: styles.iconVan
|
||||
});
|
||||
|
||||
//
|
||||
// Map initialization
|
||||
//
|
||||
|
||||
const view = new View({
|
||||
center: [10.6, 46.9],
|
||||
zoom: 6.6
|
||||
})
|
||||
|
||||
window.view = view;
|
||||
|
||||
const map = new Map({
|
||||
target: 'map',
|
||||
layers: [
|
||||
new TileLayer({
|
||||
source: new OSM()
|
||||
}),
|
||||
stagesCompletedLayer,
|
||||
stagesAheadLayer,
|
||||
poiLayer,
|
||||
trackedPointsLayer
|
||||
],
|
||||
view: view
|
||||
});
|
||||
|
||||
//
|
||||
// Center map on current/next stage
|
||||
//
|
||||
|
||||
setTimeout(() => {
|
||||
const nextStageFeature = new GeoJSON().readFeature(stagesAhead[0]);
|
||||
view.fit(nextStageFeature.getGeometry(), {
|
||||
maxZoom: 10,
|
||||
duration: 1000
|
||||
});
|
||||
}, 3000);
|
||||
|
||||
//
|
||||
// Popups
|
||||
//
|
||||
const popupEl = document.getElementById('popup');
|
||||
|
||||
const popup = new Overlay({
|
||||
element: popupEl,
|
||||
positioning: 'bottom-center',
|
||||
stopEvent: false,
|
||||
});
|
||||
map.addOverlay(popup);
|
||||
|
||||
let popover;
|
||||
function disposePopover() {
|
||||
if (popover) {
|
||||
popover.dispose();
|
||||
popover = undefined;
|
||||
lineGrey: new Style({
|
||||
stroke: new Stroke({
|
||||
color: '#555555',
|
||||
// lineDash: [8],
|
||||
width: 5,
|
||||
}),
|
||||
}),
|
||||
iconStop: new Style({
|
||||
image: new Icon({
|
||||
anchor: [0.5, 46],
|
||||
anchorXUnits: 'fraction',
|
||||
anchorYUnits: 'pixels',
|
||||
src: '/img/icon.png',
|
||||
}),
|
||||
}),
|
||||
iconVan: new Style({
|
||||
image: new Icon({
|
||||
anchor: [0.5, 16],
|
||||
anchorXUnits: 'fraction',
|
||||
anchorYUnits: 'pixels',
|
||||
src: '/img/van-100px.png',
|
||||
}),
|
||||
}),
|
||||
circleBlack: new Style({
|
||||
image: new CircleStyle({
|
||||
radius: 7,
|
||||
fill: new Fill({color: '#FF9900'}),
|
||||
stroke: new Stroke({
|
||||
color: 'white',
|
||||
width: 2,
|
||||
}),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function createPopoverHtml(feature) {
|
||||
const container = document.createElement('div');
|
||||
const title = document.createElement('div');
|
||||
title.textContent = feature.get('name');
|
||||
container.append(title);
|
||||
return container.innerHTML;
|
||||
// if (feature.get('trackable')) {
|
||||
// const linkParent = document.createElement('div');
|
||||
// const followLink = document.createElement('a');
|
||||
// followLink.textContent = 'Follow';
|
||||
// followLink.href = '#';
|
||||
// followLink.addEventListener('click', startFollowing(feature, followLink));
|
||||
// linkParent.append(followLink);
|
||||
// container.append(linkParent);
|
||||
// }
|
||||
}
|
||||
//
|
||||
// Route
|
||||
//
|
||||
|
||||
// display popup on click
|
||||
map.on('click', function (evt) {
|
||||
const feature = map.forEachFeatureAtPixel(evt.pixel, function (feature) {
|
||||
return feature;
|
||||
});
|
||||
disposePopover();
|
||||
if (!feature) {
|
||||
return;
|
||||
const tourStatus = await fetch('https://r2b22.kip.pe/status.json').then(res => res.json());
|
||||
|
||||
const lastStageFinished = tourStatus.lastStageFinished;
|
||||
const stagesCompleted = geojsonRoute.features.slice(0, lastStageFinished);
|
||||
const stagesAhead = geojsonRoute.features.slice(lastStageFinished);
|
||||
const vectorSourceStagesCompleted = new VectorSource();
|
||||
const vectorSourceStagesAhead = new VectorSource();
|
||||
|
||||
for (const stage of stagesCompleted) {
|
||||
vectorSourceStagesCompleted.addFeature(new GeoJSON().readFeature(stage));
|
||||
}
|
||||
popup.setPosition(evt.coordinate);
|
||||
popover = new bootstrap.Popover(popupEl, {
|
||||
placement: 'top',
|
||||
html: true,
|
||||
content: createPopoverHtml(feature)
|
||||
for (const stage of stagesAhead) {
|
||||
vectorSourceStagesAhead.addFeature(new GeoJSON().readFeature(stage));
|
||||
}
|
||||
|
||||
const stagesCompletedLayer = new VectorLayer({
|
||||
source: vectorSourceStagesCompleted,
|
||||
style: styles.lineOrange
|
||||
});
|
||||
popover.show();
|
||||
});
|
||||
|
||||
// change mouse cursor when over marker
|
||||
map.on('pointermove', function (evt) {
|
||||
map.getTargetElement().style.cursor = map.hasFeatureAtPixel(evt.pixel)
|
||||
? 'pointer'
|
||||
: '';
|
||||
});
|
||||
const stagesAheadLayer = new VectorLayer({
|
||||
source: vectorSourceStagesAhead,
|
||||
style: styles.lineGrey
|
||||
});
|
||||
|
||||
// Close the popup when the map is moved
|
||||
map.on('movestart', disposePopover);
|
||||
//
|
||||
// Points of Interest
|
||||
//
|
||||
|
||||
const vectorSourcePOI = new VectorSource({
|
||||
features: new GeoJSON().readFeatures(geojsonPOI),
|
||||
});
|
||||
|
||||
//
|
||||
// Tracking
|
||||
//
|
||||
const poiLayer = new VectorLayer({
|
||||
source: vectorSourcePOI,
|
||||
style: styles.circleBlack,
|
||||
});
|
||||
|
||||
const updateInterval = 5000;
|
||||
let followedFeature = vanFeature;
|
||||
let followedZoomed = false;
|
||||
// let followedFeature = null;
|
||||
const vectorSourceTrackedPoints = new VectorSource();
|
||||
|
||||
function startFollowing(feature, followLink) {
|
||||
followedFeature = feature;
|
||||
followLink.textContent = 'Stop following';
|
||||
// followLink.removeEventListener('click', startFollowing);
|
||||
followLink.addEventListener('click', stopFollowing(feature, followLink));
|
||||
}
|
||||
const vanFeature= new Feature({
|
||||
geometry: new Point([8.918618, 44.407408]),
|
||||
name: 'Support Van'
|
||||
});
|
||||
|
||||
function stopFollowing(feature, followLink) {
|
||||
followedFeature = null;
|
||||
followedZoomed = false;
|
||||
followLink.textContent = 'Stop following';
|
||||
// followLink.removeEventListener('click', stopFollowing);
|
||||
followLink.addEventListener('click', startFollowing(feature, followLink));
|
||||
}
|
||||
vectorSourceTrackedPoints.addFeature(vanFeature);
|
||||
|
||||
function updateData(startInterval=false) {
|
||||
fetch('https://r2b22.kip.pe/last.json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
const coords = [data.lon, data.lat];
|
||||
vanFeature.getGeometry().setCoordinates(coords);
|
||||
const trackedPointsLayer = new VectorLayer({
|
||||
source: vectorSourceTrackedPoints,
|
||||
style: styles.iconVan
|
||||
});
|
||||
|
||||
// let zoomLevel;
|
||||
// if (!followedZoomed) {
|
||||
// zoomLevel = 13;
|
||||
// followedZoomed = true;
|
||||
// }
|
||||
//
|
||||
// Legacy routes
|
||||
//
|
||||
|
||||
// if (followedFeature) {
|
||||
// view.animate({
|
||||
// center: followedFeature.getGeometry().getCoordinates(),
|
||||
// duration: 500,
|
||||
// zoom: zoomLevel
|
||||
// });
|
||||
// }
|
||||
const vectorSourceLegacy = new VectorSource();
|
||||
vectorSourceLegacy.addFeatures(new GeoJSON().readFeatures(geojsonLegacy));
|
||||
|
||||
const legacyLayer = new VectorLayer({
|
||||
source: vectorSourceLegacy,
|
||||
style: styles.lineOrange
|
||||
});
|
||||
|
||||
//
|
||||
// Map initialization
|
||||
//
|
||||
|
||||
const view = new View({
|
||||
center: [10.6, 46.9],
|
||||
zoom: 6.6
|
||||
})
|
||||
|
||||
window.view = view;
|
||||
|
||||
const map = new Map({
|
||||
target: 'map',
|
||||
layers: [
|
||||
new TileLayer({
|
||||
source: new OSM()
|
||||
}),
|
||||
stagesCompletedLayer,
|
||||
stagesAheadLayer,
|
||||
legacyLayer,
|
||||
poiLayer,
|
||||
trackedPointsLayer
|
||||
],
|
||||
view: view
|
||||
});
|
||||
|
||||
//
|
||||
// Center map on current/next stage
|
||||
//
|
||||
|
||||
setTimeout(() => {
|
||||
const nextStageFeature = new GeoJSON().readFeature(stagesAhead[0]);
|
||||
view.fit(nextStageFeature.getGeometry(), {
|
||||
maxZoom: 10,
|
||||
duration: 1000
|
||||
});
|
||||
}, 3000);
|
||||
|
||||
if (startInterval) {
|
||||
setInterval(updateData, updateInterval);
|
||||
//
|
||||
// Popups
|
||||
//
|
||||
const popupEl = document.getElementById('popup');
|
||||
|
||||
const popup = new Overlay({
|
||||
element: popupEl,
|
||||
positioning: 'bottom-center',
|
||||
stopEvent: false,
|
||||
});
|
||||
map.addOverlay(popup);
|
||||
|
||||
let popover;
|
||||
function disposePopover() {
|
||||
if (popover) {
|
||||
popover.dispose();
|
||||
popover = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function createPopoverHtml(feature) {
|
||||
const container = document.createElement('div');
|
||||
const title = document.createElement('div');
|
||||
title.textContent = feature.get('name');
|
||||
container.append(title);
|
||||
return container.innerHTML;
|
||||
}
|
||||
|
||||
// display popup on click
|
||||
map.on('click', function (evt) {
|
||||
const feature = map.forEachFeatureAtPixel(evt.pixel, function (feature) {
|
||||
return feature;
|
||||
});
|
||||
disposePopover();
|
||||
if (!feature) return;
|
||||
popup.setPosition(evt.coordinate);
|
||||
popover = new bootstrap.Popover(popupEl, {
|
||||
placement: 'top',
|
||||
html: true,
|
||||
content: createPopoverHtml(feature)
|
||||
});
|
||||
popover.show();
|
||||
});
|
||||
|
||||
// change mouse cursor when over marker
|
||||
map.on('pointermove', function (evt) {
|
||||
map.getTargetElement().style.cursor = map.hasFeatureAtPixel(evt.pixel)
|
||||
? 'pointer'
|
||||
: '';
|
||||
});
|
||||
|
||||
// Close the popup when the map is moved
|
||||
map.on('movestart', disposePopover);
|
||||
|
||||
//
|
||||
// Tracking
|
||||
//
|
||||
|
||||
const updateInterval = 10000;
|
||||
const peopleOverlays = {};
|
||||
|
||||
function createParticipantHTML (name) {
|
||||
if (document.getElementById(`user-${name}`)) return;
|
||||
const el = document.createElement('img');
|
||||
el.src = `https://r2b22.kip.pe/avatars/${name}.png`;
|
||||
el.id = `user-${name}`;
|
||||
el.style = 'width: 40px; height: 40px; border-radius: 20px; cursor: pointer';
|
||||
document.getElementById('people').append(el);
|
||||
}
|
||||
|
||||
function createParticipantOverlay (name) {
|
||||
if (peopleOverlays[name]) return;
|
||||
const overlayElement = new Overlay({
|
||||
stopEvent: false,
|
||||
positioning: 'center-center',
|
||||
element: document.getElementById(`user-${name}`)
|
||||
});
|
||||
peopleOverlays[name] = overlayElement;
|
||||
map.addOverlay(overlayElement);
|
||||
}
|
||||
|
||||
function updateData(startInterval=false) {
|
||||
fetch('https://r2b22.kip.pe/last.json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const vanData = data.find(i => i.name == 'satoshithevan');
|
||||
const vanCoords = [vanData.lon, vanData.lat];
|
||||
vanFeature.getGeometry().setCoordinates(vanCoords);
|
||||
|
||||
for (const item of data) {
|
||||
if (!tourStatus.participants.includes(item.name)) continue;
|
||||
createParticipantHTML(item.name);
|
||||
createParticipantOverlay(item.name);
|
||||
const overlay = peopleOverlays[item.name];
|
||||
overlay.setPosition([item.lon, item.lat]);
|
||||
|
||||
function clickHandler () {
|
||||
disposePopover();
|
||||
popup.setPosition([item.lon, item.lat]);
|
||||
popover = new bootstrap.Popover(popupEl, {
|
||||
placement: 'top',
|
||||
html: true,
|
||||
content: `Rider: ${item.name}`
|
||||
});
|
||||
popover.show();
|
||||
}
|
||||
|
||||
const avatarEl = document.getElementById(`user-${item.name}`);
|
||||
avatarEl.removeEventListener('click', clickHandler);
|
||||
avatarEl.addEventListener('click', clickHandler);
|
||||
}
|
||||
});
|
||||
|
||||
if (startInterval) {
|
||||
setInterval(updateData, updateInterval);
|
||||
}
|
||||
}
|
||||
|
||||
updateData(true);
|
||||
}
|
||||
|
||||
updateData(true);
|
||||
main();
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ol-vite",
|
||||
"version": "1.2.0",
|
||||
"version": "1.8.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ol-vite",
|
||||
"version": "1.2.0",
|
||||
"version": "1.8.0",
|
||||
"dependencies": {
|
||||
"ol": "latest"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "map",
|
||||
"version": "1.2.0",
|
||||
"version": "1.8.0",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
|
||||
Reference in New Issue
Block a user