diff --git a/client/API.md b/client/API.md index 82b2ba81..0b1b616e 100644 --- a/client/API.md +++ b/client/API.md @@ -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>) 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)` diff --git a/client/src/client.ts b/client/src/client.ts index 55b26bb0..9758c100 100644 --- a/client/src/client.ts +++ b/client/src/client.ts @@ -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] }[RequestName] } -const MANAGER_EVENTS: Array> = ['error', 'reconnect', 'reconnect_attempt', 'reconnect_error', 'reconnect_failed']; +const MANAGER_EVENTS: Array> = ['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]?: Array> + [E in EventName]?: Array> } = { }; _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[]) { - this.on(i, this._handlers[i] as EventHandler); + for(let i of Object.keys(this._handlers) as EventName[]) { + this.on(i, this._handlers[i] as EventHandler); } setTimeout(() => { @@ -87,27 +89,27 @@ export default class Socket { }); } - on>(eventName: E, fn: EventHandler) { - let listeners = this._listeners[eventName] as Array> | undefined; + on>(eventName: E, fn: EventHandler) { + let listeners = this._listeners[eventName] as Array> | 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>(eventName: E, fn: EventHandler) { + once>(eventName: E, fn: EventHandler) { let handler = ((data: any) => { this.removeListener(eventName, handler); (fn as any)(data); - }) as EventHandler; + }) as EventHandler; this.on(eventName, handler); } - removeListener>(eventName: E, fn: EventHandler) { - const listeners = this._listeners[eventName] as Array> | undefined; + removeListener>(eventName: E, fn: EventHandler) { + const listeners = this._listeners[eventName] as Array> | 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]?: EventHandler + [E in EventName]?: EventHandler } = { 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) { + _receiveMultiple(obj?: MultipleEvents) { if (obj) { - for(const i of Object.keys(obj) as EventName[]) - (obj[i] as Array).forEach((it) => { this._simulateEvent(i, it as any); }); + for(const i of Object.keys(obj) as EventName[]) + (obj[i] as Array).forEach((it) => { this._simulateEvent(i, it as any); }); } } - _simulateEvent>(eventName: E, ...data: MapEvents[E]) { - const listeners = this._listeners[eventName] as Array> | undefined; + _simulateEvent>(eventName: E, ...data: SocketEvents[E]) { + const listeners = this._listeners[eventName] as Array> | undefined; if(listeners) { - listeners.forEach(function(listener: EventHandler) { + listeners.forEach(function(listener: EventHandler) { listener(...data); }); } diff --git a/frontend/app/leaflet/highlightableLayers.js b/frontend/app/leaflet/highlightableLayers.js index 78490157..3274192d 100644 --- a/frontend/app/leaflet/highlightableLayers.js +++ b/frontend/app/leaflet/highlightableLayers.js @@ -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 = { diff --git a/frontend/app/map/lines/lines.js b/frontend/app/map/lines/lines.js index c4d3d2e7..683a82bc 100644 --- a/frontend/app/map/lines/lines.js +++ b/frontend/app/map/lines/lines.js @@ -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 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; diff --git a/leaflet/example.html b/leaflet/example.html index 5cccd6cb..d665b017 100644 --- a/leaflet/example.html +++ b/leaflet/example.html @@ -41,12 +41,35 @@ value="Open map wqxygV4R506PlBlZ" onclick="client.setPadId('wqxygV4R506PlBlZ').catch(log('setPadId error'))" /> + + + + + + +
@@ -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(); diff --git a/leaflet/package-lock.json b/leaflet/package-lock.json index a676c698..b6b78f02 100644 --- a/leaflet/package-lock.json +++ b/leaflet/package-lock.json @@ -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", diff --git a/leaflet/package.json b/leaflet/package.json index 8f8933ae..7dc63495 100644 --- a/leaflet/package.json +++ b/leaflet/package.json @@ -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" }, diff --git a/leaflet/src/bbox-handler.ts b/leaflet/src/bbox-handler.ts index 391a25ff..5151677d 100644 --- a/leaflet/src/bbox-handler.ts +++ b/leaflet/src/bbox-handler.ts @@ -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 = (name, data) => { + handleEmit: EventHandler = (name, data) => { if (["setPadId", "setRoute"].includes(name)) { this.updateBbox(); } diff --git a/leaflet/src/index.ts b/leaflet/src/index.ts index 398d0433..142fbb5e 100644 --- a/leaflet/src/index.ts +++ b/leaflet/src/index.ts @@ -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, diff --git a/leaflet/src/lines/lines-layer.ts b/leaflet/src/lines/lines-layer.ts new file mode 100644 index 00000000..67a4e3dd --- /dev/null +++ b/leaflet/src/lines/lines-layer.ts @@ -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> = {}; + highlightedLinesIds = new Set(); + + 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) { + 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) { + 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 = { + 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]; + } + +} \ No newline at end of file diff --git a/leaflet/src/lines/route-layer.ts b/leaflet/src/lines/route-layer.ts new file mode 100644 index 00000000..31383306 --- /dev/null +++ b/leaflet/src/lines/route-layer.ts @@ -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) { + super.setStyle(style); + + this.updateDraggableStyle(); + + return this; + } + +} \ No newline at end of file diff --git a/leaflet/src/markers/marker-layer.ts b/leaflet/src/markers/marker-layer.ts index bf5b9bac..f6b4b20a 100644 --- a/leaflet/src/markers/marker-layer.ts +++ b/leaflet/src/markers/marker-layer.ts @@ -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 & Pick; padding?: number; highlight?: boolean; raised?: boolean; diff --git a/leaflet/src/markers/markers-layer.ts b/leaflet/src/markers/markers-layer.ts index 35c12c6d..210e26cf 100644 --- a/leaflet/src/markers/markers-layer.ts +++ b/leaflet/src/markers/markers-layer.ts @@ -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 }); } diff --git a/leaflet/src/search/search-result-geojson.ts b/leaflet/src/search/search-result-geojson.ts new file mode 100644 index 00000000..93d06256 --- /dev/null +++ b/leaflet/src/search/search-result-geojson.ts @@ -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.'); + } + } + +} \ No newline at end of file diff --git a/leaflet/src/search/search-results-layer.ts b/leaflet/src/search/search-results-layer.ts new file mode 100644 index 00000000..18a09ad2 --- /dev/null +++ b/leaflet/src/search/search-results-layer.ts @@ -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(); + + 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) { + 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); + } + } + } + +} diff --git a/leaflet/src/type-fixup.ts b/leaflet/src/type-fixup.ts index 69ebe445..30cfa83f 100644 --- a/leaflet/src/type-fixup.ts +++ b/leaflet/src/type-fixup.ts @@ -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

{ + // Cannot override this properly + //constructor(geojson?: Array | geojson.GeoJSON, options?: GeoJSONOptions

); + //addData(geojson: Array | geojson.GeoJSON): this; + } + export const Hash: any; } \ No newline at end of file diff --git a/leaflet/src/utils/leaflet.ts b/leaflet/src/utils/leaflet.ts index f1a4b4f9..0ca849a2 100644 --- a/leaflet/src/utils/leaflet.ts +++ b/leaflet/src/utils/leaflet.ts @@ -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 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} @@ -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; +} \ No newline at end of file diff --git a/leaflet/src/utils/utils.ts b/leaflet/src/utils/utils.ts index fd311936..a1909635 100644 --- a/leaflet/src/utils/utils.ts +++ b/leaflet/src/utils/utils.ts @@ -1,3 +1,5 @@ +import { TrackPoints } from "facilmap-client"; + const LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; const LENGTH = 12; diff --git a/types/src/events.ts b/types/src/events.ts index b0bff57a..dc5e0a53 100644 --- a/types/src/events.ts +++ b/types/src/events.ts @@ -27,7 +27,6 @@ export interface MapEvents { type: [Type]; deleteType: [ObjectWithId]; history: [HistoryEntry]; - emit: { [eventName in RequestName]: [eventName, RequestData] }[RequestName]; } export type EventName> = keyof Events & string;