initial commit
This commit is contained in:
444
client/MhdMapClient.js
Normal file
444
client/MhdMapClient.js
Normal file
@@ -0,0 +1,444 @@
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
13
client/client.js
Normal file
13
client/client.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import MhdMapClient from "./MhdMapClient.js";
|
||||
|
||||
const DOMReady = function(callback) {
|
||||
document.readyState === "interactive" || document.readyState === "complete" ? callback() : document.addEventListener("DOMContentLoaded", callback);
|
||||
};
|
||||
|
||||
DOMReady(() => {
|
||||
const client = new MhdMapClient(document.getElementById('app'));
|
||||
client.start();
|
||||
})
|
||||
|
||||
|
||||
|
||||
21
client/index.html
Normal file
21
client/index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Real-time mapa MHD</title>
|
||||
|
||||
<link rel="stylesheet" href="../node_modules/leaflet/dist/leaflet.css">
|
||||
<link rel="stylesheet" href="../node_modules/leaflet-search/dist/leaflet-search.min.css">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
|
||||
<script src="client.js" type="module"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div id="app">
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
89
client/style.css
Normal file
89
client/style.css
Normal file
@@ -0,0 +1,89 @@
|
||||
/* @import '~leaflet/dist/leaflet.css'; */
|
||||
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
font-family: sans-serif;
|
||||
|
||||
--mhd-color-tram: #e31e24;
|
||||
--mhd-color-bus: #5071b9;
|
||||
--mhd-color-trolleybus: #0E562C;
|
||||
}
|
||||
html, body, main, #app, #map {
|
||||
height: 100%;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
#map .mhd-linenr {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 2px #00000080;
|
||||
padding: 0.3em;
|
||||
}
|
||||
|
||||
#map .veh-icon {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
padding: 0.3em;
|
||||
}
|
||||
|
||||
#map .mhd-linenr.veh-type-tram {
|
||||
background: var(--mhd-color-tram);
|
||||
}
|
||||
#map .mhd-linenr.veh-type-bus {
|
||||
background: var(--mhd-color-bus);
|
||||
}
|
||||
#map .mhd-linenr.veh-type-trolleybus {
|
||||
background: var(--mhd-color-trolleybus);
|
||||
}
|
||||
|
||||
#map .veh-icon.veh-notinservice {
|
||||
background: #000;
|
||||
}
|
||||
#map .veh-icon.veh-left {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#map .leaflet-popup-content {
|
||||
margin: 5px 10px 5px 10px;
|
||||
min-width: 130px;
|
||||
text-align: center;
|
||||
}
|
||||
#map .leaflet-popup-content img.veh-model {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
#map .search-tooltip {
|
||||
min-width: 175px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
max-height: calc(((1em * 1.3) + 6px) * 8);
|
||||
overflow-x: visible;
|
||||
}
|
||||
#map .search-tooltip > a {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
line-height: 1.3;
|
||||
}
|
||||
#map .search-tooltip .result-vehicle img {
|
||||
height: 1em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
#map .search-tooltip .result-vehicle .mhd-linenr {
|
||||
box-shadow: none;
|
||||
padding: 0.1em 0.4em;
|
||||
}
|
||||
#map.popup-open.drawing-trace .leaflet-marker-icon.veh-icon {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity .3s ease-in-out;
|
||||
}
|
||||
#map.popup-open.drawing-trace .leaflet-marker-icon.veh-icon.marker-popup-open {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
Reference in New Issue
Block a user