pull/147/head
Candid Dauth 2021-02-13 22:56:10 +01:00
rodzic e857d6872c
commit 282d3afd8f
20 zmienionych plików z 601 dodań i 341 usunięć

Wyświetl plik

@ -351,7 +351,7 @@ Search for markers and lines inside the map.
* `data` (object): An object with the following properties:
* `query` (string): The query string
* _returns_ (Promise<Array<[Marker](#marker-1)|[Line](#line-1)>>) An array of (stripped down) marker and line objects.
The objects only contain the `id`, `name`, `typeId`, ``lat`/`lon` (for markers), `left`/`top`/`right`/`bottom` (for
The objects only contain the `id`, `name`, `typeId`, `lat`/`lon` (for markers), `left`/`top`/`right`/`bottom` (for
lines) properties, plus an additional `kind` property that is either `"marker"` or `"line"`.
### `getRoute(data)`

Wyświetl plik

@ -7,24 +7,26 @@ import {
TrackPoint, Type, TypeCreate, TypeUpdate, View, ViewCreate, ViewUpdate, Writable
} from "facilmap-types";
declare module "facilmap-types/src/events" {
interface MapEvents {
connect: [];
disconnect: [string];
connect_error: [Error];
export interface SocketEvents extends MapEvents {
connect: [];
disconnect: [string];
connect_error: [Error];
error: [Error];
reconnect: [number];
reconnect_attempt: [number];
reconnect_error: [Error];
reconnect_failed: [];
error: [Error];
reconnect: [number];
reconnect_attempt: [number];
reconnect_error: [Error];
reconnect_failed: [];
loadStart: [],
loadEnd: []
}
loadStart: [],
loadEnd: [],
route: [RouteWithTrackPoints | undefined];
emit: { [eventName in RequestName]: [eventName, RequestData<eventName>] }[RequestName]
}
const MANAGER_EVENTS: Array<EventName<MapEvents>> = ['error', 'reconnect', 'reconnect_attempt', 'reconnect_error', 'reconnect_failed'];
const MANAGER_EVENTS: Array<EventName<SocketEvents>> = ['error', 'reconnect', 'reconnect_attempt', 'reconnect_error', 'reconnect_failed'];
export interface TrackPoints {
[idx: number]: TrackPoint;
@ -58,7 +60,7 @@ export default class Socket {
serverError: Error | undefined = undefined;
_listeners: {
[E in EventName<MapEvents>]?: Array<EventHandler<MapEvents, E>>
[E in EventName<SocketEvents>]?: Array<EventHandler<SocketEvents, E>>
} = { };
_listeningToHistory: boolean = false;
@ -75,8 +77,8 @@ export default class Socket {
const manager = new Manager(server, { forceNew: true });
this.socket = manager.socket("/");
for(let i of Object.keys(this._handlers) as EventName<MapEvents>[]) {
this.on(i, this._handlers[i] as EventHandler<MapEvents, typeof i>);
for(let i of Object.keys(this._handlers) as EventName<SocketEvents>[]) {
this.on(i, this._handlers[i] as EventHandler<SocketEvents, typeof i>);
}
setTimeout(() => {
@ -87,27 +89,27 @@ export default class Socket {
});
}
on<E extends EventName<MapEvents>>(eventName: E, fn: EventHandler<MapEvents, E>) {
let listeners = this._listeners[eventName] as Array<EventHandler<MapEvents, E>> | undefined;
on<E extends EventName<SocketEvents>>(eventName: E, fn: EventHandler<SocketEvents, E>) {
let listeners = this._listeners[eventName] as Array<EventHandler<SocketEvents, E>> | undefined;
if(!listeners) {
listeners = this._listeners[eventName] = [ ];
(MANAGER_EVENTS.includes(eventName) ? this.socket.io : this.socket)
.on(eventName, (...[data]: MapEvents[E]) => { this._simulateEvent(eventName as any, data); });
.on(eventName, (...[data]: SocketEvents[E]) => { this._simulateEvent(eventName as any, data); });
}
listeners.push(fn);
}
once<E extends EventName<MapEvents>>(eventName: E, fn: EventHandler<MapEvents, E>) {
once<E extends EventName<SocketEvents>>(eventName: E, fn: EventHandler<SocketEvents, E>) {
let handler = ((data: any) => {
this.removeListener(eventName, handler);
(fn as any)(data);
}) as EventHandler<MapEvents, E>;
}) as EventHandler<SocketEvents, E>;
this.on(eventName, handler);
}
removeListener<E extends EventName<MapEvents>>(eventName: E, fn: EventHandler<MapEvents, E>) {
const listeners = this._listeners[eventName] as Array<EventHandler<MapEvents, E>> | undefined;
removeListener<E extends EventName<SocketEvents>>(eventName: E, fn: EventHandler<SocketEvents, E>) {
const listeners = this._listeners[eventName] as Array<EventHandler<SocketEvents, E>> | undefined;
if(listeners) {
this._listeners[eventName] = listeners.filter((listener) => (listener !== fn)) as any;
}
@ -131,7 +133,7 @@ export default class Socket {
}
_handlers: {
[E in EventName<MapEvents>]?: EventHandler<MapEvents, E>
[E in EventName<SocketEvents>]?: EventHandler<SocketEvents, E>
} = {
padData: (data) => {
this.padData = data;
@ -345,6 +347,8 @@ export default class Socket {
...route,
trackPoints: this._mergeTrackPoints({}, route.trackPoints)
};
this._simulateEvent("route", this.route);
}
return this.route;
@ -353,6 +357,7 @@ export default class Socket {
clearRoute() {
this.route = undefined;
this._simulateEvent("route", undefined);
return this._emit("clearRoute");
}
@ -363,6 +368,8 @@ export default class Socket {
trackPoints: this._mergeTrackPoints({}, route.trackPoints)
};
this._simulateEvent("route", this.route);
return this.route;
});
}
@ -416,17 +423,17 @@ export default class Socket {
});
}
_receiveMultiple(obj?: MultipleEvents<MapEvents>) {
_receiveMultiple(obj?: MultipleEvents<SocketEvents>) {
if (obj) {
for(const i of Object.keys(obj) as EventName<MapEvents>[])
(obj[i] as Array<MapEvents[typeof i][0]>).forEach((it) => { this._simulateEvent(i, it as any); });
for(const i of Object.keys(obj) as EventName<SocketEvents>[])
(obj[i] as Array<SocketEvents[typeof i][0]>).forEach((it) => { this._simulateEvent(i, it as any); });
}
}
_simulateEvent<E extends EventName<MapEvents>>(eventName: E, ...data: MapEvents[E]) {
const listeners = this._listeners[eventName] as Array<EventHandler<MapEvents, E>> | undefined;
_simulateEvent<E extends EventName<SocketEvents>>(eventName: E, ...data: SocketEvents[E]) {
const listeners = this._listeners[eventName] as Array<EventHandler<SocketEvents, E>> | undefined;
if(listeners) {
listeners.forEach(function(listener: EventHandler<MapEvents, E>) {
listeners.forEach(function(listener: EventHandler<SocketEvents, E>) {
listener(...data);
});
}

Wyświetl plik

@ -5,96 +5,7 @@ import ng from 'angular';
fm.app.factory("fmHighlightableLayers", function(fmUtils) {
class GeoJSON extends L.GeoJSON {
addData(geojson) {
var features = Array.isArray(geojson) ? geojson : geojson.features,
i, len, feature;
if (features) {
for (i = 0, len = features.length; i < len; i++) {
// only add this if geometry or geometries are set and not null
feature = features[i];
if (feature.geometries || feature.geometry || feature.features || feature.coordinates) {
this.addData(feature);
}
}
return this;
}
var options = this.options;
if (options.filter && !options.filter(geojson)) { return this; }
var layer = this.geometryToLayer(geojson, options);
if (!layer) {
return this;
}
layer.feature = L.GeoJSON.asFeature(geojson);
layer.defaultOptions = layer.options;
this.resetStyle(layer);
if (options.onEachFeature) {
options.onEachFeature(geojson, layer);
}
return this.addLayer(layer);
}
geometryToLayer(geojson, options) {
var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson,
coords = geometry ? geometry.coordinates : null,
layers = [],
_coordsToLatLng = options && options.coordsToLatLng || L.GeoJSON.coordsToLatLng,
latlng, latlngs, i, len;
if (!coords && !geometry) {
return null;
}
switch (geometry.type) {
case 'Point':
latlng = _coordsToLatLng(coords);
return new fmHighlightableLayers.Marker(latlng, this.options.markerOptions);
case 'MultiPoint':
for (i = 0, len = coords.length; i < len; i++) {
latlng = _coordsToLatLng(coords[i]);
layers.push(new fmHighlightableLayers.Marker(latlng, this.options.markerOptions));
}
return new FeatureGroup(layers);
case 'LineString':
case 'MultiLineString':
latlngs = L.GeoJSON.coordsToLatLngs(coords, geometry.type === 'LineString' ? 0 : 1, _coordsToLatLng);
return new fmHighlightableLayers.Polyline(latlngs, this.options);
case 'Polygon':
case 'MultiPolygon':
latlngs = L.GeoJSON.coordsToLatLngs(coords, geometry.type === 'Polygon' ? 1 : 2, _coordsToLatLng);
return new fmHighlightableLayers.Polygon(latlngs, this.options);
case 'GeometryCollection':
for (i = 0, len = geometry.geometries.length; i < len; i++) {
var layer = this.geometryToLayer({
geometry: geometry.geometries[i],
type: 'Feature',
properties: geojson.properties
}, options);
if (layer) {
layers.push(layer);
}
}
return new L.FeatureGroup(layers);
default:
throw new Error('Invalid GeoJSON object.');
}
}
}
let fmHighlightableLayers = {

Wyświetl plik

@ -18,34 +18,6 @@ fm.app.factory("fmMapLines", function(fmUtils, $uibModal, $compile, $timeout, $r
linePopupBaseScope.client = map.client;
linePopupBaseScope.className = css.className;
map.client.on("line", function(data) {
setTimeout(function() { // trackPoints needs to be copied over
if((!map.client._editingLineId || data.id != map.client._editingLineId) && map.client.filterFunc(map.client.lines[data.id]))
linesUi._addLine(map.client.lines[data.id]);
}, 0);
});
map.client.on("deleteLine", function(data) {
linesUi._deleteLine(data);
});
map.client.on("linePoints", function(data) {
setTimeout(function() {
if((!map.client._editingLineId || data.id != map.client._editingLineId) && map.client.filterFunc(map.client.lines[data.id]))
linesUi._addLine(map.client.lines[data.id]);
}, 0);
});
map.client.on("filter", function() {
for(var i in map.client.lines) {
var show = (!map.client._editingLineId || i != map.client._editingLineId) && map.client.filterFunc(map.client.lines[i]);
if(linesById[i] && !show)
linesUi._deleteLine(map.client.lines[i]);
else if(!linesById[i] && show)
linesUi._addLine(map.client.lines[i]);
}
});
map.mapEvents.$on("showObject", async (event, id, zoom) => {
let m = id.match(/^l(\d+)$/);
if(m) {
@ -74,60 +46,8 @@ fm.app.factory("fmMapLines", function(fmUtils, $uibModal, $compile, $timeout, $r
var linesUi = {
_addLine: function(line, _doNotRerenderPopup) {
var trackPoints = [ ];
var p = line.trackPoints || [ ];
for(var i=0; i<p.length; i++) {
if(p[i] != null)
trackPoints.push(L.latLng(p[i].lat, p[i].lon));
}
if(trackPoints.length < 2)
return linesUi._deleteLine(line);
if(!linesById[line.id]) {
linesById[line.id] = (new fmHighlightableLayers.Polyline([ ])).addTo(map.map);
if(line.id != null) { // We don't want a popup for lines that we are drawing right now
linesById[line.id]
.on("click", function(e) {
linesUi.showLineInfoBox(map.client.lines[line.id]);
}.fmWrapApply($rootScope))
.bindTooltip("", $.extend({}, map.tooltipOptions, { sticky: true, offset: [ 20, 0 ] }))
.on("tooltipopen", function(e) {
linesById[line.id].setTooltipContent(fmUtils.quoteHtml(map.client.lines[line.id].name)).openTooltip(e.latlng);
});
}
}
var style = {
color : '#'+line.colour,
width : line.width
};
if(line.id == null) // We are drawing a line
style.highlight = true;
// Two points that are both outside of the viewport should not be connected, as the piece in between
// has not been received.
let splitLatLngs = fmUtils.disconnectSegmentsOutsideViewport(trackPoints, map.map.getBounds());
linesById[line.id].setLatLngs(splitLatLngs).setStyle(style);
if(line.id != null && openLine && line.id == openLine.id && !_doNotRerenderPopup)
linesUi.showLineInfoBox(line);
},
_deleteLine: function(line) {
if(line.id != null && openLine && line.id == openLine.id) {
openLine.hide();
openLine = null;
}
var lineObj = linesById[line.id];
if(!lineObj)
return;
lineObj.removeFrom(map.map);
delete linesById[line.id];
},
getZoomDestination(line) {
let bounds = fmUtils.fmToLeafletBbox(line);

Wyświetl plik

@ -19,74 +19,6 @@ fmUtils.copyToClipboard = function(text) {
c.destroy();
};
fmUtils.temporaryDragMarker = function(map, line, colour, callback, additionalOptions) {
// This marker is shown when we hover the line. It enables us to create new markers.
// It is a huge one (a normal marker with 5000 px or so transparency around it, so that we can be
// sure that the mouse is over it and dragging it will work smoothly.
let temporaryHoverMarker;
let lastPos = null;
function update() {
if(lastPos) {
const pointOnLine = fmUtils.getClosestPointOnLine(map, line._latlngs[0], lastPos);
const distance = map.latLngToContainerPoint(pointOnLine).distanceTo(map.latLngToContainerPoint(lastPos));
if(distance > line.options.weight / 2)
lastPos = null;
else {
temporaryHoverMarker.setLatLng(pointOnLine);
if(!temporaryHoverMarker._map)
temporaryHoverMarker.addTo(map);
}
}
if(!lastPos && temporaryHoverMarker._map)
temporaryHoverMarker.remove();
}
function _move(e) {
lastPos = map.mouseEventToLatLng(e.originalEvent);
update();
}
function _out(e) {
lastPos = null;
setTimeout(update, 0); // Delay in case there is a mouseover event over the marker following
}
line.on("mouseover", _move).on("mousemove", _move).on("mouseout", _out);
function makeTemporaryHoverMarker() {
temporaryHoverMarker = L.marker([0,0], Object.assign({
icon: fmUtils.createMarkerIcon(colour, 35, null, null, 1000),
draggable: true,
rise: true
}, additionalOptions)).once("dragstart", function() {
temporaryHoverMarker.once("dragend", function() {
// We have to replace the huge icon with the regular one at the end of the dragging, otherwise
// the dragging gets interrupted
this.setIcon(fmUtils.createMarkerIcon(colour));
}, temporaryHoverMarker);
callback(temporaryHoverMarker);
makeTemporaryHoverMarker();
})
.on("mouseover", _move).on("mousemove", _move).on("mouseout", _out)
.on("click", (e) => {
// Forward to the line to make it possible to click it again
line.fire("click", e);
});
}
makeTemporaryHoverMarker();
return function() {
line.off("mouseover", _move).off("mousemove", _move).off("mouseout", _out);
temporaryHoverMarker.remove();
};
};
fmUtils.onLongMouseDown = function(map, callback) {
var mouseDownTimeout, pos;

Wyświetl plik

@ -41,12 +41,35 @@
value="Open map wqxygV4R506PlBlZ"
onclick="client.setPadId('wqxygV4R506PlBlZ').catch(log('setPadId error'))"
/>
<a href="http://localhost:40829/wqxygV4R506PlBlZ" target="_blank"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg=="></a>
<input
type="button"
value="Toggle filter name ~= &quot;test&quot;"
onclick="map.setFmFilter(map.fmFilter ? undefined : 'name ~= &quot;test&quot;')"
/>
<input
type="button"
value="Berlin to Hamburg"
onclick="client.setRoute({ routePoints: [{ lat: 52.51704, lon: 13.38886 }, { lat: 53.55034, lon: 10.00065 }], mode: 'car' })"
/>
<input
type="button"
value="Clear route"
onclick="client.clearRoute()"
/>
<input
type="button"
value="Search for &quot;Berlin&quot;"
onclick="client.find({ query: 'Berlin' }).then((res) => { searchResultsLayer.setResults(res) }).catch((err) => { console.error(err); })"
/>
<input
type="button"
value="Clear search"
onclick="searchResultsLayer.setResults([]);"
/>
</div>
<div id="map"></div>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
@ -68,13 +91,35 @@
const markersLayer = new L.FacilMap.MarkersLayer(client).addTo(map)
.on("click", (e) => {
markersLayer.setHighlightedMarkers(new Set([e.layer.options.marker.id]));
markersLayer.setHighlightedMarkers(new Set([e.layer.marker.id]));
linesLayer.setHighlightedLines(new Set());
searchResultsLayer.setHighlightedResults(new Set());
});
const linesLayer = new L.FacilMap.LinesLayer(client).addTo(map)
.on("click", (e) => {
L.DomEvent.stopPropagation(e);
markersLayer.setHighlightedMarkers(new Set());
linesLayer.setHighlightedLines(new Set([e.layer.line.id]));
searchResultsLayer.setHighlightedResults(new Set());
});
map.on("click", () => {
markersLayer.setHighlightedMarkers(new Set());
linesLayer.setHighlightedLines(new Set());
searchResultsLayer.setHighlightedResults(new Set());
});
const routeLayer = new L.FacilMap.RouteLayer(client, { raised: true }).addTo(map);
const searchResultsLayer = new L.FacilMap.SearchResultsLayer().addTo(map)
.on("click", (e) => {
L.DomEvent.stopPropagation(e);
markersLayer.setHighlightedMarkers(new Set());
linesLayer.setHighlightedLines(new Set());
searchResultsLayer.setHighlightedResults(new Set([ e.layer._fmSearchResult ]));
});
const hashHandler = new L.FacilMap.HashHandler(map, client).enable();
</script>
</body>

26
leaflet/package-lock.json wygenerowano
Wyświetl plik

@ -5971,14 +5971,22 @@
"integrity": "sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw=="
},
"leaflet-auto-graticule": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/leaflet-auto-graticule/-/leaflet-auto-graticule-1.0.6.tgz",
"integrity": "sha512-A4CL5MmI0B8aiWINULzUEWZadRoPvTzYwI5an/kHfXvNNaut2z5HN+RSPwvRx0Lp824epHTbETXGbxHikpBWUQ=="
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/leaflet-auto-graticule/-/leaflet-auto-graticule-1.0.7.tgz",
"integrity": "sha512-GZYMh62MqZ1WZv5D4zx0xR/7LFpf1lzmSwc/gYrc/8CeQtks8kL9E95K4DhE7MC3LsNf9Oa22rj+MBexkR0TFw=="
},
"leaflet-draggable-lines": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/leaflet-draggable-lines/-/leaflet-draggable-lines-1.0.5.tgz",
"integrity": "sha512-pJzrAG7HkVcsHl/NZF2VK7h0SviMMrMf20x7RkR/cimF2mv5zxGvfi3n0yoWsqUM4OhUGjqqez3v5f9JgZUnZA==",
"requires": {
"leaflet-geometryutil": "^0.9.3"
}
},
"leaflet-freie-tonne": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/leaflet-freie-tonne/-/leaflet-freie-tonne-1.0.1.tgz",
"integrity": "sha512-hRpIJWJbu6D2miAPBOQc1fZPrtlawXOameHxZt+uYEcq3JKKAeQ6WiG9GsQmsWkuxIT6PWlmrebi6+CgIkPc+A=="
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/leaflet-freie-tonne/-/leaflet-freie-tonne-1.0.3.tgz",
"integrity": "sha512-SF/LMSW4zwOYib/9+X6mAwZBxNnffWO8fVvBkjraTlr7e41zkCsE+7W4UVvJu2GXsVqIzx9NgZBBi5dvCDOWug=="
},
"leaflet-geometryutil": {
"version": "0.9.3",
@ -5994,9 +6002,9 @@
"integrity": "sha1-w2xxg0fFJDAztXy0uuomEZ2CxwE="
},
"leaflet-highlightable-layers": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/leaflet-highlightable-layers/-/leaflet-highlightable-layers-1.0.1.tgz",
"integrity": "sha512-E/dtm5iscM5HQSNkyfy8Xh4AvFJNrCzBJoNqkc7x6MgI1pGdjBxn4RTCP2jLkg1Lt2WMF8HATc2Y1w4zSMw4sA=="
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/leaflet-highlightable-layers/-/leaflet-highlightable-layers-1.0.7.tgz",
"integrity": "sha512-/TIuwKnCQudPUOpTz7QO/8qjsNkcWIvHgPvTa5xoGbex8EsAhyxVPU70qurfeq63+zqQQFzIYV6b1Vu165A6Lw=="
},
"leaflet.markercluster": {
"version": "1.4.1",

Wyświetl plik

@ -33,11 +33,12 @@
"facilmap-types": "2.7.0",
"filtrex": "^2.1.0",
"leaflet": "^1.7.1",
"leaflet-auto-graticule": "^1.0.6",
"leaflet-freie-tonne": "^1.0.1",
"leaflet-auto-graticule": "^1.0.7",
"leaflet-draggable-lines": "^1.0.5",
"leaflet-freie-tonne": "^1.0.3",
"leaflet-geometryutil": "^0.9.3",
"leaflet-hash": "^0.2.1",
"leaflet-highlightable-layers": "^1.0.1",
"leaflet-highlightable-layers": "^1.0.7",
"leaflet.markercluster": "^1.4.1",
"lodash": "^4.17.20"
},

Wyświetl plik

@ -1,5 +1,5 @@
import Socket from "facilmap-client";
import { EventHandler, MapEvents } from "facilmap-types";
import Socket, { SocketEvents } from "facilmap-client";
import { EventHandler } from "facilmap-types";
import { Handler, Map } from "leaflet";
import { leafletToFmBbox } from "./utils/leaflet";
@ -22,7 +22,7 @@ export default class BboxHandler extends Handler {
}
}
handleEmit: EventHandler<MapEvents, "emit"> = (name, data) => {
handleEmit: EventHandler<SocketEvents, "emit"> = (name, data) => {
if (["setPadId", "setRoute"].includes(name)) {
this.updateBbox();
}

Wyświetl plik

@ -13,15 +13,23 @@ import MarkerCluster from "./markers/marker-cluster";
import MarkerLayer from "./markers/marker-layer";
import MarkersLayer from "./markers/markers-layer";
import HashHandler from "./views/hash";
import LinesLayer from "./lines/lines-layer";
import RouteLayer from "./lines/route-layer";
import SearchResultGeoJSON from "./search/search-result-geojson";
import SearchResultsLayer from "./search/search-results-layer";
const FacilMap = {
BboxHandler,
ClickListener,
HashHandler,
Layers,
LinesLayer,
MarkerCluster,
MarkerLayer,
MarkersLayer,
RouteLayer,
SearchResultGeoJSON,
SearchResultsLayer,
Socket,
Utils: {
leaflet: leafletUtils,

Wyświetl plik

@ -0,0 +1,146 @@
import Socket, { TrackPoints } from "facilmap-client";
import { ID, Line, LinePointsEvent, ObjectWithId } from "facilmap-types";
import { FeatureGroup, LayerOptions, Map, PolylineOptions } from "leaflet";
import { HighlightableLayerOptions, HighlightablePolyline } from "leaflet-highlightable-layers";
import { disconnectSegmentsOutsideViewport, tooltipOptions, trackPointsToLatLngArray } from "../utils/leaflet";
import { quoteHtml } from "../utils/utils";
interface LinesLayerOptions extends LayerOptions {
}
export default class LinesLayer extends FeatureGroup {
options!: LayerOptions;
client: Socket;
linesById: Record<string, InstanceType<typeof HighlightablePolyline>> = {};
highlightedLinesIds = new Set<ID>();
constructor(client: Socket, options?: LinesLayerOptions) {
super([], options);
this.client = client;
}
onAdd(map: Map) {
super.onAdd(map);
this.client.on("line", this.handleLine);
this.client.on("linePoints", this.handleLinePoints);
this.client.on("deleteLine", this.handleDeleteLine);
map.on("fmFilter", this.handleFilter);
return this;
}
onRemove(map: Map) {
super.onRemove(map);
this.client.removeListener("line", this.handleLine);
this.client.removeListener("linePoints", this.handleLinePoints);
this.client.removeListener("deleteLine", this.handleDeleteLine);
map.off("fmFilter", this.handleFilter);
return this;
}
handleLine = (line: Line) => {
if(this._map.fmFilterFunc(line))
this._addLine(line);
};
handleLinePoints = (event: LinePointsEvent) => {
const line = this.client.lines[event.id];
if(line && this._map.fmFilterFunc(line))
this._addLine(line);
};
handleDeleteLine = (data: ObjectWithId) => {
this._deleteLine(data);
};
handleFilter = () => {
for(const i of Object.keys(this.client.lines) as any as Array<keyof Socket['lines']>) {
const show = this._map.fmFilterFunc(this.client.lines[i]);
if(this.linesById[i] && !show)
this._deleteLine(this.client.lines[i]);
else if(!this.linesById[i] && show)
this._addLine(this.client.lines[i]);
}
};
highlightLine(id: ID) {
this.highlightedLinesIds.add(id);
if (this.client.lines[id])
this.handleLine(this.client.lines[id]);
}
unhighlightLine(id: ID) {
this.highlightedLinesIds.delete(id);
if (this.client.lines[id])
this.handleLine(this.client.lines[id]);
}
setHighlightedLines(ids: Set<ID>) {
for (const id of this.highlightedLinesIds) {
if (!ids.has(id))
this.unhighlightLine(id);
}
for (const id of ids) {
if (!this.highlightedLinesIds.has(id))
this.highlightLine(id);
}
}
_addLine(line: Line & { trackPoints?: TrackPoints }) {
const trackPoints = trackPointsToLatLngArray(line.trackPoints);
if(trackPoints.length < 2) {
this._deleteLine(line);
return;
}
if(!this.linesById[line.id]) {
this.linesById[line.id] = new HighlightablePolyline([ ]);
this.addLayer(this.linesById[line.id]);
if(line.id != null) { // We don't want a popup for lines that we are drawing right now
this.linesById[line.id]
.bindTooltip("", { ...tooltipOptions, sticky: true, offset: [ 20, 0 ] })
.on("tooltipopen", () => {
this.linesById[line.id].setTooltipContent(quoteHtml(this.client.lines[line.id].name));
});
}
}
const style: HighlightableLayerOptions<PolylineOptions> = {
color: '#'+line.colour,
weight: line.width,
opacity: 0.35
} as any;
if(line.id == null || this.highlightedLinesIds.has(line.id)) {
Object.assign(style, {
raised: true,
opacity: 1
});
}
// Two points that are both outside of the viewport should not be connected, as the piece in between
// has not been received.
let splitLatLngs = disconnectSegmentsOutsideViewport(trackPoints, this._map.getBounds());
(this.linesById[line.id] as any).line = line;
this.linesById[line.id].setLatLngs(splitLatLngs).setStyle(style);
}
_deleteLine(line: ObjectWithId) {
if(!this.linesById[line.id])
return;
this.removeLayer(this.linesById[line.id]);
delete this.linesById[line.id];
}
}

Wyświetl plik

@ -0,0 +1,104 @@
import Socket from "facilmap-client";
import { Map, PathOptions, PolylineOptions } from "leaflet";
import { HighlightableLayerOptions, HighlightablePolyline } from "leaflet-highlightable-layers";
import { trackPointsToLatLngArray } from "../utils/leaflet";
import DraggableLines from "leaflet-draggable-lines";
interface RouteLayerOptions extends PolylineOptions {
}
export default class RouteLayer extends HighlightablePolyline {
realOptions!: RouteLayerOptions;
client: Socket;
draggable?: DraggableLines;
constructor(client: Socket, options?: RouteLayerOptions) {
super([], options);
this.client = client;
}
onAdd(map: Map) {
super.onAdd(map);
this.draggable = new DraggableLines(map, { enableForLayer: false });
this.draggable.enable();
this.draggable.on("dragend remove insert", this.handleDrag);
this.updateDraggableStyle();
this.client.on("route", this.handleRoute);
this.client.on("routePoints", this.handleRoutePoints);
this.updateLine(true);
return this;
}
onRemove(map: Map) {
super.onRemove(map);
this.client.removeListener("route", this.handleRoute);
this.client.removeListener("routePoints", this.handleRoutePoints);
this.draggable!.off("dragend remove insert", this.handleDrag);
this.draggable!.disableForLayer(this);
this.draggable!.disable();
return this;
}
handleDrag = () => {
this.updateRoute();
};
handleRoute = () => {
this.updateLine(true);
};
handleRoutePoints = () => {
this.updateLine(false);
};
updateRoute() {
if (this.client.route) {
this.client.setRoute({
...this.client.route,
routePoints: this.getDraggableLinesRoutePoints()!.map((p) => ({ lat: p.lat, lon: p.lng }))
});
}
}
updateLine(updateRoutePoints: boolean) {
if (this.client.route) {
if (updateRoutePoints)
this.setDraggableLinesRoutePoints(this.client.route.routePoints.map((p) => [p.lat, p.lon]));
const trackPoints = trackPointsToLatLngArray(this.client.route.trackPoints);
this.setLatLngs(trackPoints);
this.draggable!.enableForLayer(this);
} else {
this.setLatLngs([]);
this.draggable!.disableForLayer(this);
}
}
updateDraggableStyle() {
if (this.draggable) {
Object.assign(this.draggable.options, {
dragMarkerOptions: () => ({ pane: "fm-raised-marker" }),
tempMarkerOptions: () => ({ pane: "fm-raised-marker" }),
plusTempMarkerOptions: () => ({ pane: "fm-raised-marker" })
});
this.draggable.redraw();
}
}
setStyle(style: HighlightableLayerOptions<PathOptions>) {
super.setStyle(style);
this.updateDraggableStyle();
return this;
}
}

Wyświetl plik

@ -1,4 +1,4 @@
import { Colour, Marker, Shape, Symbol } from "facilmap-types";
import { Marker } from "facilmap-types";
import L, { LatLngExpression, LeafletMouseEvent, Map, Marker as LeafletMarker, MarkerOptions } from "leaflet";
import { createMarkerIcon } from "../utils/icons";
import { setLayerPane } from "leaflet-highlightable-layers";
@ -9,7 +9,7 @@ Map.addInitHook(function (this: Map) {
});
export interface MarkerLayerOptions extends MarkerOptions {
marker?: Marker;
marker?: Partial<Marker> & Pick<Marker, 'colour' | 'size' | 'symbol' | 'shape'>;
padding?: number;
highlight?: boolean;
raised?: boolean;

Wyświetl plik

@ -1,6 +1,6 @@
import Socket from 'facilmap-client';
import { ID, Marker, ObjectWithId } from 'facilmap-types';
import { FeatureGroup, LayerOptions, Map } from 'leaflet';
import { Map } from 'leaflet';
import { tooltipOptions } from '../utils/leaflet';
import { quoteHtml } from '../utils/utils';
import MarkerCluster, { MarkerClusterOptions } from './marker-cluster';
@ -107,11 +107,16 @@ export default class MarkersLayer extends MarkerCluster {
});
}
(this.markersById[marker.id] as any).marker = marker;
const highlight = this.highlightedMarkerIds.has(marker.id);
this.markersById[marker.id]
.setLatLng([ marker.lat, marker.lon ])
.setStyle({
marker,
highlight: this.highlightedMarkerIds.has(marker.id)
highlight,
raised: highlight
});
}

Wyświetl plik

@ -0,0 +1,88 @@
import { GeoJSON, Geometry, Feature } from "geojson";
import { FeatureGroup, GeoJSON as GeoJSONLayer, GeoJSONOptions, Layer } from "leaflet";
import { HighlightablePolygon, HighlightablePolyline } from "leaflet-highlightable-layers";
import MarkerLayer, { MarkerLayerOptions } from "../markers/marker-layer";
interface SearchResultGeoJSONOptions extends GeoJSONOptions {
marker?: MarkerLayerOptions['marker'];
highlight?: boolean;
raised?: boolean;
}
export default class SearchResultGeoJSON extends GeoJSONLayer {
options!: SearchResultGeoJSONOptions;
constructor(geojson: GeoJSON, options?: SearchResultGeoJSONOptions) {
super(geojson, options);
}
addData(geojson: GeoJSON) {
// GeoJSON.addData() does not support specifying a custom geometryToLayer function. Thus we are replicating its functionality here.
if (Array.isArray(geojson) || 'features' in geojson) {
for (const feature of Array.isArray(geojson) ? geojson : geojson.features) {
if (feature.geometries || feature.geometry || feature.features || feature.coordinates) {
this.addData(feature);
}
}
return this;
}
if (this.options.filter && !this.options.filter(geojson as any))
return this;
const layer = this.geometryToLayer(geojson);
if (!layer)
return this;
(layer as any).feature = GeoJSONLayer.asFeature(geojson);
(layer as any).defaultOptions = layer.options;
this.resetStyle(layer);
if (this.options.onEachFeature)
this.options.onEachFeature(geojson as any, layer);
return this.addLayer(layer);
}
geometryToLayer(geojson: Geometry | Feature): Layer | undefined {
const geometry = geojson.type === 'Feature' ? geojson.geometry : geojson;
const _coordsToLatLng = this.options.coordsToLatLng || GeoJSONLayer.coordsToLatLng;
if (!geometry)
return;
switch (geometry.type) {
case 'Point':
return new MarkerLayer(_coordsToLatLng(geometry.coordinates as any), { marker: this.options.marker, raised: this.options.raised, highlight: this.options.highlight });
case 'MultiPoint':
return new FeatureGroup(geometry.coordinates.map((coords) => (
new MarkerLayer(_coordsToLatLng(coords as any), { marker: this.options.marker, raised: this.options.raised, highlight: this.options.highlight })
)));
case 'LineString':
case 'MultiLineString':
return new HighlightablePolyline(GeoJSONLayer.coordsToLatLngs(geometry.coordinates, geometry.type === 'LineString' ? 0 : 1, _coordsToLatLng), { raised: this.options.raised, opacity: this.options.highlight ? 1 : 0.35 });
case 'Polygon':
case 'MultiPolygon':
return new HighlightablePolygon(GeoJSONLayer.coordsToLatLngs(geometry.coordinates, geometry.type === 'Polygon' ? 1 : 2, _coordsToLatLng), { raised: this.options.raised, opacity: this.options.highlight ? 1 : 0.35 });
case 'GeometryCollection':
return new FeatureGroup(geometry.geometries.map((g) => (
this.geometryToLayer({
geometry: g,
type: 'Feature',
properties: (geojson as any).properties
})
)).filter((l) => l) as Layer[]);
default:
throw new Error('Invalid GeoJSON object.');
}
}
}

Wyświetl plik

@ -0,0 +1,114 @@
import { SearchResult } from "facilmap-types";
import { FeatureGroup, Layer, LayerOptions } from "leaflet";
import MarkerLayer from "../markers/marker-layer";
import { tooltipOptions } from "../utils/leaflet";
import SearchResultGeoJSON from "./search-result-geojson";
declare module "leaflet" {
interface Layer {
_fmSearchResult?: SearchResult;
}
}
const searchMarkerColour = "000000";
const searchMarkerSize = 35;
interface SearchResultsLayerOptions extends LayerOptions {
}
export default class SearchResultsLayer extends FeatureGroup {
options!: SearchResultsLayerOptions;
highlightedResults = new Set<SearchResult>();
constructor(results?: SearchResult[], options?: SearchResultsLayerOptions) {
super([], options);
if (results)
this.setResults(results);
}
highlightResult(result: SearchResult) {
this.highlightedResults.add(result);
this.redrawResult(result);
}
unhighlightResult(result: SearchResult) {
this.highlightedResults.delete(result);
this.redrawResult(result);
}
setHighlightedResults(results: Set<SearchResult>) {
for (const result of this.highlightedResults) {
if (!results.has(result))
this.unhighlightResult(result);
}
for (const result of results) {
if (!this.highlightedResults.has(result))
this.highlightResult(result);
}
}
redrawResult(result: SearchResult) {
for (const layer of this.getLayers().filter((layer) => layer._fmSearchResult === result)) {
this.removeLayer(layer);
}
for (const layer of this.resultToLayers(result)) {
this.addLayer(layer);
}
}
resultToLayers(result: SearchResult) {
const layers: Layer[] = [];
const highlight = this.highlightedResults.has(result);
if(!result.lat || !result.lon || (result.geojson && result.geojson.type != "Point")) { // If the geojson is just a point, we already render our own marker
const layer = new SearchResultGeoJSON(result.geojson!, {
raised: highlight,
highlight,
marker: {
colour: searchMarkerColour,
size: searchMarkerSize,
symbol: result.icon || '',
shape: ''
}
}).bindTooltip(result.display_name, { ...tooltipOptions, sticky: true, offset: [ 20, 0 ] })
layer._fmSearchResult = result;
layer.eachLayer((l) => {
l._fmSearchResult = result;
});
layers.push(layer);
}
if(result.lat != null && result.lon != null) {
const marker = new MarkerLayer([ result.lat, result.lon ], {
raised: highlight,
highlight,
marker: {
colour: searchMarkerColour,
size: searchMarkerSize,
symbol: result.icon || '',
shape: ''
}
}).bindTooltip(result.display_name, { ...tooltipOptions, offset: [ 20, 0 ] })
marker._fmSearchResult = result;
layers.push(marker);
}
return layers;
}
setResults(results: SearchResult[]) {
this.clearLayers();
for (const result of results) {
for (const layer of this.resultToLayers(result)) {
this.addLayer(layer);
}
}
}
}

Wyświetl plik

@ -1,4 +1,5 @@
import { GridLayerOptions, Layer, Map } from "leaflet";
import geojson from "geojson";
declare module "leaflet" {
interface LayerOptions {
@ -28,5 +29,11 @@ declare module "leaflet" {
options: MarkerClusterGroupOptions;
}
interface GeoJSON<P = any> {
// Cannot override this properly
//constructor(geojson?: Array<geojson.Feature> | geojson.GeoJSON, options?: GeoJSONOptions<P>);
//addData(geojson: Array<geojson.Feature> | geojson.GeoJSON): this;
}
export const Hash: any;
}

Wyświetl plik

@ -1,3 +1,4 @@
import { TrackPoints } from 'facilmap-client';
import { Bbox, BboxWithZoom } from 'facilmap-types';
import L, { LatLng, LatLngBounds, Map, TooltipOptions } from 'leaflet';
import 'leaflet-geometryutil';
@ -26,56 +27,6 @@ export function fmToLeafletBbox(bbox: Bbox): LatLngBounds {
return L.latLngBounds(L.latLng(bbox.bottom, bbox.left), L.latLng(bbox.top, bbox.right));
}
export function getClosestPointOnLine(map: Map, trackPoints: LatLng[], point: LatLng): LatLng {
const index = getClosestIndexOnLine(map, trackPoints, point);
const before = trackPoints[Math.floor(index)];
const after = trackPoints[Math.ceil(index)];
const percentage = index - Math.floor(index);
return L.latLng(before.lat + percentage * (after.lat - before.lat), before.lng + percentage * (after.lng - before.lng));
}
export function getClosestIndexOnLine(map: Map, trackPoints: LatLng[], point: LatLng, startI?: number): number {
let dist = Infinity;
let idx = null;
for(let i=(startI || 0); i<trackPoints.length-1; i++) {
const thisDist = L.GeometryUtil.distanceSegment(map, point, trackPoints[i], trackPoints[i+1]);
if(thisDist < dist) {
dist = thisDist;
idx = i;
}
}
if(idx == null)
return trackPoints.length;
const closestPointOnSegment = L.GeometryUtil.closestOnSegment(map, point, trackPoints[idx], trackPoints[idx+1]);
idx += L.GeometryUtil.distance(map, closestPointOnSegment, trackPoints[idx]) / L.GeometryUtil.distance(map, trackPoints[idx], trackPoints[idx+1]);
return idx;
}
export function getIndexOnLine(map: Map, trackPoints: LatLng[], routePoints: LatLng[], point: LatLng): number {
if(routePoints.length == 0)
return 0;
const idxs: number[] = [ ];
for(let i=0; i<routePoints.length; i++) {
idxs.push(getClosestIndexOnLine(map, trackPoints, routePoints[i], Math.floor(idxs[i-1])));
}
const pointIdx = getClosestIndexOnLine(map, trackPoints, point);
if(pointIdx == 0)
return 0;
for(let i=0; i<idxs.length; i++) {
if(idxs[i] > pointIdx)
return i;
}
return idxs.length;
}
/**
* Takes an array of track points and splits it up where two points in a row are outside of the given bbox.
* @param trackPoints {Array<L.LatLng>}
@ -122,3 +73,15 @@ export function pointsEqual(latLng1: LatLng, latLng2: LatLng, map: Map, zoom?: n
return map.project(latLng1, zoom).distanceTo(map.project(latLng2, zoom)) < 1;
}
export function trackPointsToLatLngArray(trackPoints: TrackPoints | undefined): LatLng[] {
const result: LatLng[] = [];
if (trackPoints) {
for (let i = 0; i < trackPoints.length; i++) {
if (trackPoints[i]) {
result.push(new LatLng(trackPoints[i]!.lat, trackPoints[i]!.lon, trackPoints[i]!.ele));
}
}
}
return result;
}

Wyświetl plik

@ -1,3 +1,5 @@
import { TrackPoints } from "facilmap-client";
const LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const LENGTH = 12;

Wyświetl plik

@ -27,7 +27,6 @@ export interface MapEvents {
type: [Type];
deleteType: [ObjectWithId];
history: [HistoryEntry];
emit: { [eventName in RequestName]: [eventName, RequestData<eventName>] }[RequestName];
}
export type EventName<Events extends Record<keyof Events, any[]>> = keyof Events & string;