From c88fd70befa78369c3e812e4d620eaf273f8c06c Mon Sep 17 00:00:00 2001 From: Florian Bischof Date: Fri, 3 May 2024 14:00:42 +0200 Subject: [PATCH] Backport Pro changes into OSS (#1490) (patch) --- cypress/integration/line.spec.js | 33 +++++ leaflet-geoman.d.ts | 216 ++++++++++++++++++++++++++++- package.json | 2 +- pnpm-lock.yaml | 21 +-- src/assets/translations/en.json | 9 +- src/css/controls.css | 1 + src/js/Edit/L.PM.Edit.Line.js | 3 + src/js/Edit/L.PM.Edit.Rectangle.js | 19 ++- src/js/Mixins/MarkerLimits.js | 7 + src/js/Toolbar/L.Controls.js | 20 ++- src/js/Toolbar/L.PM.Toolbar.js | 11 +- src/js/helpers/turfHelper.js | 2 +- 12 files changed, 319 insertions(+), 25 deletions(-) diff --git a/cypress/integration/line.spec.js b/cypress/integration/line.spec.js index 345ac22c..74aad971 100644 --- a/cypress/integration/line.spec.js +++ b/cypress/integration/line.spec.js @@ -325,4 +325,37 @@ describe('Draw & Edit Line', () => { expect(hintLine.options.color).to.eql('red'); }); }); + + it('remove vertex marker from MarkerLimit Cache', () => { + cy.toolbarButton('polyline').click(); + + cy.get(mapSelector) + .click(120, 150) + .click(120, 100) + .click(300, 100) + .click(300, 200) + .click(120, 150); + + cy.toolbarButton('edit').click(); + + cy.hasVertexMarkers(4); + cy.hasMiddleMarkers(3); + + // rightclick on a vertex-marker to delete it + cy.get('.marker-icon:not(.marker-icon-middle)') + .eq(2) + .trigger('contextmenu'); + + cy.hasVertexMarkers(3); + cy.hasMiddleMarkers(2); + + cy.wait(20); + + cy.window().then(({ map }) => { + map.panBy([40, 40], { animate: false }); + }); + + cy.hasVertexMarkers(3); + cy.hasMiddleMarkers(2); + }); }); diff --git a/leaflet-geoman.d.ts b/leaflet-geoman.d.ts index 84c47ee7..30bf1eb8 100644 --- a/leaflet-geoman.d.ts +++ b/leaflet-geoman.d.ts @@ -439,6 +439,66 @@ declare module 'leaflet' { fn?: PM.GlobalRotateModeToggledEventHandler ): this; + /****************************************** + * + * TODO: Union MODE EVENTS ON MAP ONLY + * + ********************************************/ + + /** Fired when Union Mode is toggled. */ + on( + type: 'pm:globalunionmodetoggled', + fn: PM.GlobalUnionModeToggledEventHandler + ): this; + once( + type: 'pm:globalunionmodetoggled', + fn: PM.GlobalUnionModeToggledEventHandler + ): this; + off( + type: 'pm:globalunionmodetoggled', + fn?: PM.GlobalUnionModeToggledEventHandler + ): this; + + /** Fired when Union is executed. */ + on(type: 'pm:union', fn: PM.UnionEventHandler): this; + once(type: 'pm:union', fn: PM.UnionEventHandler): this; + off(type: 'pm:union', fn: PM.UnionEventHandler): this; + + /****************************************** + * + * TODO: Difference MODE EVENTS ON MAP ONLY + * + ********************************************/ + + /** Fired when Difference Mode is toggled. */ + on( + type: 'pm:globaldifferencemodetoggled', + fn: PM.GlobalDifferenceModeToggledEventHandler + ): this; + once( + type: 'pm:globaldifferencemodetoggled', + fn: PM.GlobalDifferenceModeToggledEventHandler + ): this; + off( + type: 'pm:globaldifferencemodetoggled', + fn?: PM.GlobalDifferenceModeToggledEventHandler + ): this; + + /** Fired when Difference is executed. */ + on(type: 'pm:difference', fn: PM.DifferenceEventHandler): this; + once(type: 'pm:difference', fn: PM.DifferenceEventHandler): this; + off(type: 'pm:difference', fn?: PM.DifferenceEventHandler): this; + + /** Fired when a layer is added to the selection. */ + on(type: 'pm:selectionadd', fn: PM.SelectionEventHandler): this; + once(type: 'pm:selectionadd', fn: PM.SelectionEventHandler): this; + off(type: 'pm:selectionadd', fn: PM.SelectionEventHandler): this; + + /** Fired when a layer is removed from the selection. */ + on(type: 'pm:selectionremove', fn: PM.SelectionEventHandler): this; + once(type: 'pm:selectionremove', fn: PM.SelectionEventHandler): this; + off(type: 'pm:selectionremove', fn: PM.SelectionEventHandler): this; + /****************************************** * * TODO: TRANSLATION EVENTS ON MAP ONLY @@ -479,7 +539,6 @@ declare module 'leaflet' { } namespace PM { - export const version: string; /** Supported shape names. 'ImageOverlay' is in Edit Mode only. Also accepts custom shape name. */ @@ -547,7 +606,11 @@ declare module 'leaflet' { PMDragMap, PMRemoveMap, PMCutMap, - PMRotateMap { + PMRotateMap, + PMScaleMap, + PMSelectionMap, + PMUnionMap, + PMDifferenceMap { Toolbar: PMMapToolbar; Keyboard: PMMapKeyboard; @@ -592,6 +655,8 @@ declare module 'leaflet' { finishCircle?: string; placeCircleMarker?: string; placeText?: string; + selectFirstLayerFor?: string; + selectSecondLayerFor?: string; }; actions?: { @@ -617,6 +682,9 @@ declare module 'leaflet' { drawTextButton?: string; scaleButton?: string; autoTracingButton?: string; + snapGuidesButton?: string; + unionButton?: string; + differenceButton?: string; }; measurements?: { @@ -629,7 +697,7 @@ declare module 'leaflet' { width?: string; coordinates?: string; coordinatesMarker?: string; - } + }; } type ACTION_NAMES = 'cancel' | 'removeLastVertex' | 'finish' | 'finishMode'; @@ -637,6 +705,7 @@ declare module 'leaflet' { class Action { text: string; onClick?: (e: any) => void; + title?: string; } type TOOLBAR_CONTROL_ORDER = @@ -652,6 +721,14 @@ declare module 'leaflet' { | 'removalMode' | 'rotateMode' | 'drawText' + | 'scaleMode' + | 'pinningOption' + | 'snappingOption' + | 'autoTracingOption' + | 'snapGuidesOption' + | 'spitalMode' + | 'unionMode' + | 'differenceMode' | string; interface PMMapToolbar { @@ -668,7 +745,7 @@ declare module 'leaflet' { ): void; /** Returns all of the active buttons */ - getButtons(): Record + getButtons(): Record; /** Returns the full button object or undefined if the name does not exist */ getButton(name: string): L.Control | undefined; @@ -677,7 +754,7 @@ declare module 'leaflet' { controlExists(name: string): boolean; /** Returns all of the custom, active buttons */ - getButtonsInBlock(name: string): Record + getButtonsInBlock(name: string): Record; /** Returns a Object with the positions for all blocks */ getBlockPositions(): BlockPositions; @@ -690,8 +767,8 @@ declare module 'leaflet' { copyInstance: string, options?: CustomControlOptions ): { - drawInstance: Draw, - control: L.Control + drawInstance: Draw; + control: L.Control; }; /** Change the actions of an existing button. */ @@ -808,6 +885,26 @@ declare module 'leaflet' { /** Defines in which panes the layers and helper vertices are created. Default: { vertexPane: 'markerPane', layerPane: 'overlayPane', markerPane: 'markerPane' } */ panes?: { vertexPane?: PANE; layerPane?: PANE; markerPane?: PANE }; + + /** Measurement options */ + measurements?: { + measurement?: boolean; + showTooltip?: boolean; + showTooltipOnHover?: boolean; + totalLength?: boolean; + segmentLength?: boolean; + area?: boolean; + radius?: boolean; + perimeter?: boolean; + height?: boolean; + width?: boolean; + coordinates?: boolean; + displayFormat?: 'metric' | 'imperial'; + }; + + autoTracing?: boolean; + + selectionLayerStyle?: L.PathOptions; } interface PMDrawMap { @@ -908,6 +1005,74 @@ declare module 'leaflet' { globalRotateModeEnabled(): boolean; } + interface PMScaleMap { + /** Enables global scale mode. */ + enableGlobalScaleMode(): void; + + /** Disables global scale mode. */ + disableGlobalScaleMode(): void; + + /** Toggles global scale mode. */ + toggleGlobalScaleMode(): void; + + /** Returns true if global scale mode is enabled. false when disabled. */ + globalScaleModeEnabled(): boolean; + } + + interface PMSelectionMap { + /** Enables global selection mode. Optional a filter can be added, which checks if the selection is allowed. */ + enableSelectionTool(filterFnc?: () => boolean): void; + + /** Disables global selection mode. */ + disableSelectionTool(): void; + + /** Returns true if global selection mode is enabled. false when disabled. */ + selectionToolEnabled(): boolean; + + /** Adds a layer to the selection. */ + addSelection(layer: L.Layer): void; + + /** Removes a layer from the selection. */ + removeSelection(layer: L.Layer): void; + + /** Returns selected layers. */ + getSelectedLayers(): L.Layer[]; + } + + interface PMUnionMap { + /** Enables global union mode. */ + enableGlobalUnionMode(): void; + + /** Disables global union mode. */ + disableGlobalUnionMode(): void; + + /** Toggles global union mode. */ + toggleGlobalUnionMode(): void; + + /** Returns true if global union mode is enabled. false when disabled. */ + globalUnionModeEnabled(): boolean; + + /** Unifies the two layers. */ + union(layer1: L.Layer, layer2: L.Layer): void; + } + + interface PMDifferenceMap { + /** Enables global difference mode. */ + enableGlobalDifferenceMode(): void; + + /** Disables global difference mode. */ + disableGlobalDifferenceMode(): void; + + /** Toggles global difference mode. */ + toggleGlobalDifferenceMode(): void; + + /** Returns true if global difference mode is enabled. false when disabled. */ + globalDifferenceModeEnabled(): boolean; + + /** Subtracts the second selected layer from the first selected layer. */ + difference(layer1: L.Layer, layer2: L.Layer): void; + } + interface PMRotateLayer { /** Enables rotate mode on the layer. */ enableRotate(): void; @@ -1614,6 +1779,43 @@ declare module 'leaflet' { map: L.Map; }) => void; + /** + * UNION MODE MAP EVENT HANDLERS + */ + export type GlobalUnionModeToggledEventHandler = (e: { + enabled: boolean; + map: L.Map; + }) => void; + + /** + * UNION EVENT HANDLERS + */ + export type UnionEventHandler = (e: { + resultLayer: L.Layer; + mergedLayers: L.Layer[]; + }) => void; + + /** + * DIFFERENCE MODE MAP EVENT HANDLERS + */ + export type GlobalDifferenceModeToggledEventHandler = (e: { + enabled: boolean; + map: L.Map; + }) => void; + + /** + * DIFFERENCE EVENT HANDLERS + */ + export type DifferenceEventHandler = (e: { + resultLayer: L.Layer; + subtractedLayers: L.Layer[]; + }) => void; + + /** + * SELECTION EVENT HANDLERS + */ + export type SelectionEventHandler = (e: { layer: L.Layer }) => void; + /** * TRANSLATION EVENT HANDLERS */ diff --git a/package.json b/package.json index b8a26eae..11ad063d 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@turf/line-intersect": "^6.5.0", "@turf/line-split": "^6.5.0", "lodash": "4.17.21", - "polygon-clipping": "0.15.3" + "polyclip-ts": "^0.16.5" }, "devDependencies": { "@types/leaflet": "^1.7.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb21f9a1..11ec4c9a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,9 @@ dependencies: lodash: specifier: 4.17.21 version: 4.17.21 - polygon-clipping: - specifier: 0.15.3 - version: 0.15.3 + polyclip-ts: + specifier: ^0.16.5 + version: 0.16.5 devDependencies: '@types/leaflet': @@ -866,6 +866,10 @@ packages: tweetnacl: 0.14.5 dev: true + /bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + dev: false + /blob-util@2.0.2: resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} dev: true @@ -2737,10 +2741,11 @@ packages: engines: {node: '>=0.10.0'} dev: true - /polygon-clipping@0.15.3: - resolution: {integrity: sha512-ho0Xx5DLkgxRx/+n4O74XyJ67DcyN3Tu9bGYKsnTukGAW6ssnuak6Mwcyb1wHy9MZc9xsUWqIoiazkZB5weECg==} + /polyclip-ts@0.16.5: + resolution: {integrity: sha512-ZchnG0zGZReHgEo3EYzEUi6UmfQFFzNnj6AFU+gBm+IJJ4qG9gL4CwjtCV6oi/PittUPpJLiLJxcn/AgrCBO+g==} dependencies: - splaytree: 3.1.2 + bignumber.js: 9.1.2 + splaytree-ts: 1.0.1 dev: false /prelude-ls@1.2.1: @@ -3027,8 +3032,8 @@ packages: is-fullwidth-code-point: 5.0.0 dev: true - /splaytree@3.1.2: - resolution: {integrity: sha512-4OM2BJgC5UzrhVnnJA4BkHKGtjXNzzUfpQjCO8I05xYPsfS/VuQDwjCGGMi8rYQilHEV4j8NBqTFbls/PZEE7A==} + /splaytree-ts@1.0.1: + resolution: {integrity: sha512-B+VzCm33/KEchi/fzT6/3NRHm8k5+Kf37SBQO3meHHS/tK2xBnIm4ZvusQ1wUpHgKMCCqEWgXnwFXAa1nD289g==} dev: false /sshpk@1.18.0: diff --git a/src/assets/translations/en.json b/src/assets/translations/en.json index 0d2ac852..7ab21066 100644 --- a/src/assets/translations/en.json +++ b/src/assets/translations/en.json @@ -9,7 +9,9 @@ "startCircle": "Click to place circle center", "finishCircle": "Click to finish circle", "placeCircleMarker": "Click to place circle marker", - "placeText": "Click to place text" + "placeText": "Click to place text", + "selectFirstLayerFor": "Select first layer for {action}", + "selectSecondLayerFor": "Select second layer for {action}" }, "actions": { "finish": "Finish", @@ -32,7 +34,10 @@ "rotateButton": "Rotate Layers", "drawTextButton": "Draw Text", "scaleButton": "Scale Layers", - "autoTracingButton": "Auto trace Line" + "autoTracingButton": "Auto trace Line", + "snapGuidesButton": "Show SnapGuides", + "unionButton": "Union layers", + "differenceButton": "Subtract layers" }, "measurements": { "totalLength": "Length", diff --git a/src/css/controls.css b/src/css/controls.css index 290d050c..b0e7449a 100644 --- a/src/css/controls.css +++ b/src/css/controls.css @@ -159,6 +159,7 @@ border-bottom: none; height: 29px; line-height: 29px; + vertical-align: middle; } .leaflet-pm-toolbar .button-container:first-child.pos-right.active diff --git a/src/js/Edit/L.PM.Edit.Line.js b/src/js/Edit/L.PM.Edit.Line.js index 0fd35d26..ae72ed2e 100644 --- a/src/js/Edit/L.PM.Edit.Line.js +++ b/src/js/Edit/L.PM.Edit.Line.js @@ -535,13 +535,16 @@ Edit.Line = Edit.extend({ // remove the marker and the middlemarkers next to it from the map if (marker._middleMarkerPrev) { this._markerGroup.removeLayer(marker._middleMarkerPrev); + this._removeFromCache(marker._middleMarkerPrev); } if (marker._middleMarkerNext) { this._markerGroup.removeLayer(marker._middleMarkerNext); + this._removeFromCache(marker._middleMarkerNext); } // remove the marker from the map this._markerGroup.removeLayer(marker); + this._removeFromCache(marker); if (markerArr) { let rightMarkerIndex; diff --git a/src/js/Edit/L.PM.Edit.Rectangle.js b/src/js/Edit/L.PM.Edit.Rectangle.js index 4a1d3e2e..cba0abe7 100644 --- a/src/js/Edit/L.PM.Edit.Rectangle.js +++ b/src/js/Edit/L.PM.Edit.Rectangle.js @@ -101,7 +101,12 @@ Edit.Rectangle = Edit.Polygon.extend({ // (Without this, it's occasionally possible for a marker to get stuck as 'snapped,' which prevents Rectangle resizing) draggedMarker._snapped = false; - this._fireMarkerDragStart(e); + const { indexPath } = L.PM.Utils.findDeepMarkerIndex( + this._markers, + draggedMarker + ); + + this._fireMarkerDragStart(e, indexPath); }, _onMarkerDrag(e) { @@ -119,7 +124,11 @@ Edit.Rectangle = Edit.Polygon.extend({ this._adjustRectangleForMarkerMove(draggedMarker); - this._fireMarkerDrag(e); + const { indexPath } = L.PM.Utils.findDeepMarkerIndex( + this._markers, + draggedMarker + ); + this._fireMarkerDrag(e, indexPath); this._fireChange(this._layer.getLatLngs(), 'Edit'); }, @@ -135,7 +144,11 @@ Edit.Rectangle = Edit.Polygon.extend({ delete m._oppositeCornerLatLng; }); - this._fireMarkerDragEnd(e); + const { indexPath } = L.PM.Utils.findDeepMarkerIndex( + this._markers, + draggedMarker + ); + this._fireMarkerDragEnd(e, indexPath); // fire edit event this._fireEdit(); diff --git a/src/js/Mixins/MarkerLimits.js b/src/js/Mixins/MarkerLimits.js index 04305e23..77bb4b99 100644 --- a/src/js/Mixins/MarkerLimits.js +++ b/src/js/Mixins/MarkerLimits.js @@ -20,6 +20,7 @@ const MarkerLimits = { // remove events when edit mode is disabled this._layer.on('pm:disable', this._removeMarkerLimitEvents, this); + this._layer.on('remove', this._removeMarkerLimitEvents, this); // add markers closest to the mouse if (this.options.limitMarkersToCount > -1) { @@ -40,6 +41,12 @@ const MarkerLimits = { const allMarkers = [...this._markerGroup.getLayers(), ...this.markerCache]; this.markerCache = allMarkers.filter((v, i, s) => s.indexOf(v) === i); }, + _removeFromCache(marker) { + const markerCacheIndex = this.markerCache.indexOf(marker); + if (markerCacheIndex > -1) { + this.markerCache.splice(markerCacheIndex, 1); + } + }, renderLimits(markers) { this.markerCache.forEach((l) => { if (markers.includes(l)) { diff --git a/src/js/Toolbar/L.Controls.js b/src/js/Toolbar/L.Controls.js index 41d415b6..39cea401 100644 --- a/src/js/Toolbar/L.Controls.js +++ b/src/js/Toolbar/L.Controls.js @@ -29,11 +29,19 @@ const PMButton = L.Control.extend({ this.options.position ); } - this.buttonsDomNode = this._makeButton(this._button); - this._container.appendChild(this.buttonsDomNode); + this._renderButton(); return this._container; }, + _renderButton() { + const oldDomNode = this.buttonsDomNode; + this.buttonsDomNode = this._makeButton(this._button); + if (oldDomNode) { + oldDomNode.replaceWith(this.buttonsDomNode); + } else { + this._container.appendChild(this.buttonsDomNode); + } + }, onRemove() { this.buttonsDomNode.remove(); @@ -123,24 +131,28 @@ const PMButton = L.Control.extend({ const actions = { cancel: { text: getTranslation('actions.cancel'), + title: getTranslation('actions.cancel'), onClick() { this._triggerClick(); }, }, finishMode: { text: getTranslation('actions.finish'), + title: getTranslation('actions.finish'), onClick() { this._triggerClick(); }, }, removeLastVertex: { text: getTranslation('actions.removeLastVertex'), + title: getTranslation('actions.removeLastVertex'), onClick() { this._map.pm.Draw[button.jsClass]._removeLastVertex(); }, }, finish: { text: getTranslation('actions.finish'), + title: getTranslation('actions.finish'), onClick(e) { this._map.pm.Draw[button.jsClass]._finishShape(e); }, @@ -166,6 +178,10 @@ const PMButton = L.Control.extend({ actionNode.setAttribute('tabindex', '0'); actionNode.href = '#'; + if (action.title) { + actionNode.title = action.title; + } + actionNode.innerHTML = action.text; L.DomEvent.disableClickPropagation(actionNode); diff --git a/src/js/Toolbar/L.PM.Toolbar.js b/src/js/Toolbar/L.PM.Toolbar.js index 7cce4ac4..867c74d2 100644 --- a/src/js/Toolbar/L.PM.Toolbar.js +++ b/src/js/Toolbar/L.PM.Toolbar.js @@ -34,6 +34,15 @@ const Toolbar = L.Class.extend({ }, customButtons: [], initialize(map) { + // For some reason there is an reference between multiple maps instances + this.customButtons = []; + this.options.positions = { + draw: '', + edit: '', + options: '', + custom: '', + }; + this.init(map); }, reinit() { @@ -151,7 +160,7 @@ const Toolbar = L.Class.extend({ }, _addButton(name, button) { this.buttons[name] = button; - this.options[name] = this.options[name] || false; + this.options[name] = !!this.options[name] || false; return this.buttons[name]; }, diff --git a/src/js/helpers/turfHelper.js b/src/js/helpers/turfHelper.js index 66583510..258c8410 100644 --- a/src/js/helpers/turfHelper.js +++ b/src/js/helpers/turfHelper.js @@ -1,4 +1,4 @@ -import polygonClipping from 'polygon-clipping'; +import * as polygonClipping from 'polyclip-ts'; export function feature(geom) { const feat = { type: 'Feature' };