Files
MHDMap/client/MhdMapClient.js
2022-06-28 13:02:50 +02:00

444 lines
14 KiB
JavaScript

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 `<a href="#" class="result-vehicle veh-type-${veh.type}">
<span><b>${text}</b> @ <span class="mhd-linenr veh-type-${veh.type}">${lineNumber}</span></span>
<img src="${document.location.pathname}/imhd/icons/${veh.img}.png" alt="" />
</a>`;
} else {
return `<a href="#">${text}</a>`;
}
// 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.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 = `<b>#${veh.vehicleNumber}</b>`;
if (veh.img && veh.imhdinfo) {
popupHtml += `
<a href="https://imhd.sk${veh.imhdinfo}" target="_blank">
<img class="veh-model" src="${document.location.pathname}/imhd/icons/${veh.img}.png" alt="" />
</a>`;
}
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) {
const markers = [];
for (let stop of stops) {
let popupHtml = `
<b>${stop.name}</b><br/>
stationId: ${stop.stationId}<br />
stationStopId: ${stop.stationStopId}<br />
tag: ${stop.tag}<br />
`;
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.currentVehicleTraceRenderer,
color: '#0F95D8',
opacity: 1,
fill: true,
radius: 5,
})
.bindPopup(popupHtml)
.addTo(this.map);
markers.push(newMarker);
}
console.log('stops', stops);
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;
}
}