import L from 'leaflet'; import '@bagage/leaflet.restoreview/leaflet.restoreview.js'; import 'leaflet-search'; import 'leaflet-layervisibility'; // import glify from 'leaflet.glify'; const VehicleIcon = L.Icon.extend({ options: { shadowUrl: null, iconSize: [38, 95], iconAnchor: [22, 94], popupAnchor: [-3, -76] } }); const VehicleTypes = { 'bus': 'Autobusy', 'tram': 'Električky', 'trolleybus': 'Trolejbusy', 'stop': 'Zastávky', }; export default class MhdMapClient { constructor(cont) { this.ws = null; this.wsMessages = {}; this.cont = cont; this.mapCont = cont.querySelector('#map'); this.map = null; this.markers = {}; this.markerLayers = {}; this.baseMapLayer = null; this.layerControl = null; this.searchControl = null; this.searchableLayer = null; this.currentLinePoints = null; } async start() { this.startMap(); const loc = document.location; this.wsUrl = 'ws' + (loc.protocol === 'https:' ? 's' : '')+ '://' + loc.host + loc.pathname + '/ws'; await this.connectWs(); setInterval(() => { if (this.ws === null) { this.connectWs(); } }, 10000); } startMap() { this.map = L.map(this.mapCont, { center: [48.1530667, 17.1033202], zoom: 13, minZoom: 11, maxZoom: 19, zoomDelta: 0.5, zoomSnap: 0.5, doubleClickZoom: false, preferCanvas: true, }); this.baseMapLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap', maxZoom: 19 }).addTo(this.map); this.georefLayer = L.tileLayer('https://maps.georeferencer.com/georeferences/e886f898-04e0-4479-8cd3-bbe4ae7537b3/2022-06-22T09:24:26.012877Z/map/{z}/{x}/{y}.png?key=c0WCCJEkXmpgh7TxHg2U', { maxZoom: 17 }); this.layerControl = L.control.layers({ "OpenStreetMaps": this.baseMapLayer, "MHD mapa elek": this.georefLayer, }, {}).addTo(this.map); this.searchableLayer = L.layerGroup(); this.searchControl = new L.Control.Search({ layer: this.searchableLayer, buildTip: (text, val) => { const veh = val.layer?.options?.data?.vehicle; if (veh) { let lineNumber = veh.lineNumber || '?'; return ` ${text} @ ${lineNumber} `; } else { return `${text}`; } // return text; }, marker: false, moveToLocation: (latlng, title, map) => { map.setView(latlng.layer.getLatLng(), map.getZoom()); latlng.layer.fire('click'); } })/*.addTo(this.map);*/ this.map.addControl(this.searchControl); /*this.searchControl.on('search:locationfound', function(e) { console.log(e); if(e.layer._popup) e.layer.openPopup(); }).*/ this.map.on('popupopen', this._popupOpenEvent.bind(this)); this.map.on('popupclose', this._popupCloseEvent.bind(this)); this.map.restoreView(); this.map.on('click', event => { if (!window.over || !event.originalEvent.ctrlKey && !event.originalEvent.shiftKey) return; const clicked = this.map.mouseEventToLatLng(event.originalEvent); const bounds = window.over.getBounds(); if (event.originalEvent.ctrlKey) { window.over.setBounds([ bounds.getSouthWest(), clicked ]); } else if (event.originalEvent.shiftKey) { window.over.setBounds([ clicked, bounds.getNorthEast() ]); } console.log(window.over.getBounds()); }); this.map.on('baselayerchange', layer => { if (layer.layer === this.georefLayer) { this.markerLayers['bus'].hide(); } else { this.markerLayers['bus'].show(); } }); // proj. EPSG:3857 /* window.over = L.imageOverlay('/map_min.jpg', [[48.078703, 17.01291], [48.229636, 17.226939]], { opacity: 0.7, }).addTo(this.map); */ // this.updateTest(); this.currentLinePoints = new L.LayerGroup().addTo(this.map); this.currentLinePointsRenderer = L.canvas({ padding: 0.5 }).addTo(this.map); this.currentVehicleTrace = new L.LayerGroup().addTo(this.map); this.currentVehicleTraceRenderer = L.canvas({ padding: 0.5 }).addTo(this.map); this.currentVehicleTraceRenderer._container.style.pointerEvents = 'none'; //this.currentLinePointsRenderer = L.canvas({ padding: 0.5 }); } updateVehicles(vehicles) { let markersByType = {}; for (let vehId in vehicles) { const veh = vehicles[vehId]; if (this.markers.hasOwnProperty(vehId)) { let marker = this.markers[vehId]; if (veh.gpsLatitude < -90) { // invalid position -- vehicle left? marker._icon && L.DomUtil.addClass(marker._icon, 'veh-left'); } else { marker._icon && L.DomUtil.removeClass(marker._icon, 'veh-notinservice'); marker.setLatLng([veh.gpsLatitude, veh.gpsLongitude]); } } else { let iconClass = 'veh-icon mhd-linenr'; if (veh.type) { iconClass += ' veh-type-' + veh.type; } if (!veh.lineNumber) { iconClass += ' veh-notinservice'; } let popupHtml = `#${veh.vehicleNumber}`; if (veh.img && veh.imhdinfo) { popupHtml += ` `; } let lineNumber = veh.lineNumber || '?'; const newMarker = new L.Marker([veh.gpsLatitude, veh.gpsLongitude], { icon: new L.DivIcon({ className: iconClass, html: `${lineNumber}`, iconSize: null }), title: `${vehId}`, // TODO: Kinda hack, would be better to extend L.Marker instead. data: { vehicle: veh, } }) .bindPopup(popupHtml) .addTo(this.map); this.markers[vehId] = newMarker; // this.searchableLayer.addLayer(newMarker); if (veh.type) { markersByType[veh.type] ??= []; markersByType[veh.type].push(newMarker); } } } for (let type in markersByType) { if (this.markerLayers.hasOwnProperty(type)) { markersByType[type].forEach(el => this.markerLayers[type].addLayer(el)); } else { this.markerLayers[type] = L.layerGroup(markersByType[type]).addTo(this.map); this.searchableLayer.addLayer(this.markerLayers[type]); this.layerControl.addOverlay(this.markerLayers[type], VehicleTypes[type] ?? type); } } //this.markerLayers[] } updateStops(stops) { if (this.markerLayers.hasOwnProperty('stop')) return; const markers = []; for (let stop of stops) { let popupHtml = ` ${stop.name}
stationId: ${stop.stationId}
stationStopId: ${stop.stationStopId}
tag: ${stop.tag}
`; const newMarker = L.circleMarker([stop.gpsLat, stop.gpsLon], { icon: new L.DivIcon({ className: 'stop-icon', html: `${stop.name}`, iconSize: null, }), title: `${stop.name}`, // TODO: Kinda hack, would be better to extend L.Marker instead. data: { stop: stop, }, renderer: this.currentLinePointsRenderer, color: '#0F95D8', opacity: 1, fill: true, radius: 5, }) .bindPopup(popupHtml) .addTo(this.map); markers.push(newMarker); } if (this.markerLayers.hasOwnProperty('stop')) { this.markerLayers['stop'].clearLayers(); } this.markerLayers['stop'] = L.layerGroup(markers).addTo(this.map); this.searchableLayer.addLayer(this.markerLayers['stop']); this.layerControl.addOverlay(this.markerLayers['stop'], VehicleTypes['stop']); } async drawVehicleTrace(vehId) { this.currentVehicleTrace.clearLayers(); if (vehId === null) return; let vehPos = await this.sendWs('requestVehicleTrace', { vehicle: vehId, count: 1000, }); vehPos = vehPos.map(row => [row.lat, row.long]); let trace = vehPos.slice(0, 10); trace = this._splitArray(trace, row => row[0] < -90 || row[1] < -180); let points = new L.LayerGroup(); vehPos.forEach(point => points.addLayer(L.circleMarker(point, { renderer: this.currentVehicleTraceRenderer, color: '#ff00ff', opacity: 0.7, fill: false, radius: 2, }))); this.currentVehicleTrace.addLayer(points); this.currentVehicleTrace.addLayer(L.polyline(trace, { color: 'red', renderer: this.currentVehicleTraceRenderer, })); } async drawLinePoints(line) { this.currentLinePoints.clearLayers(); if (line === null) return; let trace = await this.sendWs('requestLineTrace', { line: line, count: 10000, }); /* this.currentLinePoints = L.glify.points({ this.map, data: pointsOrGeoJson });*/ trace = trace.map(row => [row.lat, row.long]); let points = new L.LayerGroup(); trace.forEach(point => points.addLayer(L.circleMarker(point, { renderer: this.currentLinePointsRenderer, color: '#0000ff', fill: false, radius: 3, }))); this.currentLinePoints.addLayer(points); //trace = trace.map(row => [row.lat, row.long]); //this.currentTrace = L.polyline(trace, { color: 'red' }).addTo(this.map); } async connectWs() { if (this.ws) { await this.ws.terminate(); } return new Promise ((resolve, reject) => { try { this.ws = new WebSocket(this.wsUrl); this.ws.addEventListener('open', event => { resolve(this.ws); }); this.ws.addEventListener('message', event => { const obj = JSON.parse(event.data); if (obj.action) { this.handleAction(obj.action, obj.data); } }); this.ws.addEventListener('close', event => { this.ws = null; }); } catch (e) { this.ws = null; reject(e); } }) } handleAction(action, data) { if (['updateVehicles', 'updateStops'].includes(action)) { return this[action](data); } else if (action === 'response') { if (this.wsMessages.hasOwnProperty(data.id)) { this.wsMessages[data.id](data.msg); delete this.wsMessages[data.id]; } else { console.error('Got a response to something we didn\'t ask for: ', data); } return true; } console.error('Unknown action requested by the server: ', action, data); } async sendWs(action, data) { if (!this.ws) return null; let msgid = Math.random().toString(36).slice(2, 14); return new Promise ((resolve, reject) => { this.wsMessages[msgid] = resolve; this.ws.send(JSON.stringify({ id: msgid, action, msg: data, })); }); } _popupOpenEvent(event) { L.DomUtil.addClass(this.map._container,'popup-open'); let icon = event?.popup?._source?._icon; if (icon) { this._hadMarkerOpen = icon; L.DomUtil.addClass(icon,'marker-popup-open'); } let veh = event?.popup?._source?.options?.data?.vehicle; if (veh) { L.DomUtil.addClass(this.map._container, 'drawing-trace'); // the user has clicked a vehicle this.drawLinePoints(veh.lineNumber); this.drawVehicleTrace(veh.vehicleNumber); } } _popupCloseEvent(event) { L.DomUtil.removeClass(this.map._container,'popup-open'); L.DomUtil.removeClass(this.map._container, 'drawing-trace'); if (this._hadMarkerOpen) { L.DomUtil.removeClass(this._hadMarkerOpen,'marker-popup-open'); this._hadMarkerOpen = null; } this.drawVehicleTrace(null); this.drawLinePoints(null); } _splitArray(arr, predicate) { const ret = []; let arrNow = []; for (let el of arr) { if (predicate(el)) { ret.push(arrNow); arrNow = []; } else { arrNow.push(el); } } ret.push(arrNow); return ret; } }