import './style.css'; import {Map, View} from 'ol'; import Feature from 'ol/Feature'; import GeoJSON from 'ol/format/GeoJSON'; import Overlay from 'ol/Overlay'; 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} 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(); async function main() { 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'}), stroke: new Stroke({ color: 'white', width: 2, }), }) }) } // // Route // 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)); } 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' }); vectorSourceTrackedPoints.addFeature(vanFeature); const trackedPointsLayer = new VectorLayer({ source: vectorSourceTrackedPoints, style: styles.iconVan }); // // Legacy routes // 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); // // 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 isRecentTimestamp (tst) { // newer than 2 hours ago? return (tst * 1000) > (Date.now() - 2*60*60*1000); } 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; if (!isRecentTimestamp(item.tst)) 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); } main();