diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..6f0223c1 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v16.15.0 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f3f010f..f68bf848 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,9 +5,10 @@ Thank you for considering contributing to Leaflet-Geoman. Follow these steps to get up and running: 1. clone the repository -2. run `npm install` -3. run `npm start` to compile dev version and use watch mode -4. run `npm run cypress` to launch the test suite (also nice for TDD) -5. run `npm run test` to run cypress tests -6. run `npm run lint` to check the code with eslint -7. run `npm run prepare` to compile the build version +2. Make sure you run the node version specified in package.json under "engines" or run `nvm use` +3. run `npm install` +4. run `npm start` to compile dev version and use watch mode +5. run `npm run cypress` to launch the test suite (also nice for TDD) +6. run `npm run test` to run cypress tests +7. run `npm run lint` to check the code with eslint +8. run `npm run prepare` to compile the build version diff --git a/README.md b/README.md index 43801381..797e9751 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Features marked with ⭐ in this documentation are available in Leaflet-Geoman P - [Snapping](#snapping) - [Pinning ⭐](#pinning-) - [Measurement ⭐](#measurement-) + - [AutoTracing ⭐](#autotracing-) - [LayerGroup](#layergroup) - [Customization](#customize) - [Toolbar](#toolbar) @@ -125,7 +126,7 @@ L.marker([51.50915, -0.096112], { pmIgnore: true }).addTo(map); Enable Leaflet-Geoman on an ignored layer: ```js layer.options.pmIgnore = false; -L.PM.reInitLayer(layer); +L.PM.reInitLayer(layer); ``` If `Opt-In` (look below) is `true`, a layers `pmIgnore` property has to be set to `false` to get initiated. @@ -216,6 +217,7 @@ See the available options in the table below. | snappingOption | `true` | Adds a button to toggle the Snapping Option ⭐. | | splitMode | `true` | Adds a button to toggle the Split Mode for all layers ⭐. | | scaleMode | `true` | Adds a button to toggle the Scale Mode for all layers ⭐. | +| autoTracingOption | `false` | Adds a button to toggle the Auto Tracing Option ⭐. | To pass options to the buttons you have two ways: ```js @@ -246,7 +248,7 @@ map.pm.enableDraw('Polygon', { map.pm.disableDraw(); ``` -Currently available shapes are `Marker`, `CircleMarker`, `Circle`, `Line`, `Rectangle`, `Polygon` and `Cut`. +Currently available shapes are `Marker`, `CircleMarker`, `Circle`, `Line`, `Rectangle`, `Polygon`, `Text`, `Cut`, `CutCircle`⭐ and `Split`⭐. The following methods are available on `map.pm`: @@ -263,6 +265,8 @@ The following methods are available on `map.pm`: | getGlobalOptions() | `Object` | Returns the `globalOptions`. | | getGeomanLayers(`Boolean`) | `Array` | Returns all Leaflet-Geoman layers on the map as array. Pass `true` to get a L.FeatureGroup. | | getGeomanDrawLayers(`Boolean`) | `Array` | Returns all drawn Leaflet-Geoman layers on the map as array. Pass `true` to get a L.FeatureGroup. | +| Draw..setOptions(`options`) | - | Applies the options to the drawing shape and calls `setStyle`. `map.pm.Draw.Line.setOptions(options)`. | +| Draw..setStyle(`options`) | - | Applies the styles (`templineStyle`, `hintlineStyle`, `pathOptions`, `markerStyle`) to the drawing layer. `map.pm.Draw.Line.setStlye(options)`. | See the available options in the table below. @@ -275,8 +279,8 @@ See the available options in the table below. | requireSnapToFinish | `false` | Require the last point of a shape to be snapped. | | tooltips | `true` | Show helpful tooltips for your user. | | allowSelfIntersection | `true` | Allow self intersections. | -| templineStyle | `{ color: 'red' },` | [Leaflet path options](https://leafletjs.com/reference.html#path) for the lines between drawn vertices/markers. | -| hintlineStyle | `{ color: 'red', dashArray: [5, 5] }` | [Leaflet path options](https://leafletjs.com/reference.html#path) for the helper line between last drawn vertex and the cursor. | +| templineStyle | `{ color: '#3388ff' },` | [Leaflet path options](https://leafletjs.com/reference.html#path) for the lines between drawn vertices/markers. | +| hintlineStyle | `{ color: '#3388ff', dashArray: [5, 5] }` | [Leaflet path options](https://leafletjs.com/reference.html#path) for the helper line between last drawn vertex and the cursor. | | pathOptions | `null` | [Leaflet path options](https://leafletjs.com/reference.html#path) for the drawn layer (Only for L.Path layers). | | markerStyle | `{ draggable: true }` | [Leaflet marker options](https://leafletjs.com/reference.html#marker-icon) (only for drawing markers). | | cursorMarker | `true` | Show a marker at the cursor. | @@ -292,6 +296,10 @@ See the available options in the table below. | rectangleAngle | `0` | Rectangle can drawn with a rotation angle 0-360 degrees | | layersToCut | `[]` | Cut-Mode: Only the passed layers can be cut. Cutted layers are removed from the Array until no layers are left anymore and cutting is working on all layers again. | | textOptions | `{}` | Text Layer options. Look into [textOptions](#text-layer-drawing). | +| closedPolygonEdge | `false` | Closes the Polygon while drawing ⭐. | +| closedPolygonFill | `false` | Shows the Polygon fill while drawing ⭐. | +| autoTracing | `false` | Enables auto tracing while drawing ⭐. | +| allowCircleCut | `true` | Allow Cutting of a Circle ⭐. | This options can only set over `map.pm.setGlobalOptions({})`: @@ -414,8 +422,12 @@ See the available options in the table below. | limitMarkersToViewport | `false` | Shows only markers in the viewport. ⭐ | | limitMarkersToClick | `false` | Shows markers only after the layer was clicked. ⭐ | | pinning | `false` | Pin shared vertices/markers together during edit [Details](#pinning-⭐). ⭐ | +| allowPinning | `true` | Layer can be prevented from pinning.⭐ | +| allowScale | `true` | Layer can be prevented from scaling.⭐ | | centerScaling | `true` | Scale origin is the center, else it is the opposite corner. If `false` Alt-Key can be used. [Scale Mode](#scale-mode-). ⭐ | | uniformScaling | `true` | Width and height are scaled with the same ratio. If `false` Shift-Key can be used. [Scale Mode](#scale-mode-). ⭐ | +| allowAutoTracing | `true` | Layer can be prevented from auto tracing.⭐ | +| addVertexOnClick | `false` | Add Vertices while clicking on the line of Polyline or Polygon⭐ | You can listen to events related to editing on events like this: @@ -577,6 +589,10 @@ map.pm.enableGlobalCutMode({ Available options are the same as in [Draw Mode](#draw-mode). If the option `layersToCut: [layer1, layer2]` is passed, only this certain layers will be cutted. +In the Pro-Version ⭐ is the option `allowCircleCut` available, which makes it possible to cut Circles. + +Over the Global Options you enable cutting in shape form of a Circle `cutAsCircle: true` for the cut-button. Else you can enable `CutCircle` over `map.pm.enableDraw('CutCircle')` + The following methods are available on `map.pm`: | Method | Returns | Description | @@ -607,6 +623,15 @@ The following events are available on a map instance: The rotation is clockwise. It starts in the North with 0° and goes over East (90°) and South (180°) to West (270°). The rotation center is the center (`layer.getCenter()`) of a Polygon with the LatLngs of the layer. +**Rotation of Rectangles:** + +If a rotated rectangle is created programmatically, it is important to set the initial angle with `setInitAngle(degrees)`. +```js +const rect = L.rectangle(coords).addTo(map); // the Leaflet constructor always creates a non-rotated rectangle +rect.setLatLngs(coords); // setting the rotated coordinates +rect.pm.setInitAngle(angle); +``` + You can enable Rotate Mode for all layers on a map like this: ```js @@ -632,6 +657,7 @@ The following methods are available for layers under `layer.pm`: | rotateLayer(`degrees`) | - | Rotates the layer by `x` degrees. | | rotateLayerToAngle(`degrees`) | - | Rotates the layer to `x` degrees. | | getAngle() | `Degrees` | Returns the angle of the layer in degrees. | +| setInitAngle(`degrees`) | - | Set the initial angle of the layer in degrees. | The following events are available on a layer instance: @@ -787,6 +813,8 @@ The following events are available on a layer instance: | Event | Params | Description | Output | | :------------ | :----- | :----------------------------------------- | :----------------------- | | pm:textchange | `e` | Fired when the text of a layer is changed. | `text`, `layer`, `shape` | +| pm:textfocus | `e` | Fired when the text layer is focused. | `layer`, `shape` | +| pm:textblur | `e` | Fired when the text layer is blurred. | `layer`, `shape` | For custom text styling get the HTMLElement and add CSS styles: @@ -827,6 +855,13 @@ The following options are additionally to the [Draw](#draw-mode) and [Edit Mode] | snappingOrder | `Array` | Prioritize the order of snapping. Default: `['Marker','CircleMarker','Circle','Line','Polygon','Rectangle']`. | | layerGroup | `map` | add the created layers to a layergroup instead to the map. | | panes | `Object` | Defines in which [panes](https://leafletjs.com/reference.html#map-pane) the layers and helper vertices are created. Default: `{ vertexPane: 'markerPane', layerPane: 'overlayPane', markerPane: 'markerPane' }`. | +| cutAsCircle | `false` | Enable cutting in shape form of a Circle. | + +The following events are available on a map instance: + +| Event | Params | Description | Output | +| :------------------------ | :----- | :---------------------------------------- | :----------------------------------------------------- | +| pm:globaloptionschanged | `e` | Fired when global options are changed. | | Some details about a few more powerful options: @@ -871,6 +906,34 @@ See the available options in the table below. | width | `true` | Shows the width in the tooltip `Rectangle`. | | coordinates | `true` | Shows the coordinates in the tooltip `Marker`, `CircleMarker` and the current dragged marker while drawing / editing. | + +##### AutoTracing ⭐ + +![AutoTracing Demo](https://user-images.githubusercontent.com/19800037/196027144-7bc696aa-6b5d-4903-8a21-2df0e2d05e4e.gif) + +While drawing / cutting it is possible to auto trace the coordinates of another Layer. Exclusive for Leaflet-Geoman Pro ⭐ + +```js +map.pm.setGlobalOptions({ autoTracing: true }) +``` + +See the available options in the table below. + +| Option | Default | Description | +| :----------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------- | +| autoTracing | `true` | Enables auto tracing while drawing. | +| autoTraceMaxZoom | `10` | Until which zoom level the coordinates of the layers in the viewport will be used. | +| autoTraceMaxDistance | `20` | The distance to the layer when a snap for auto tracing should happen. | + +Here's a list of map events you can listen to: + +| Event | Params | Description | Output | +| :----------------------- | :----- | :-------------------------------------------------------------------------------------- | :------------------------ | +| pm:autotracestart | `e` | Fired when auto tracing is started and connected with a layer. | | +| pm:autotracelinechange | `e` | Fired when auto tracing hintline is changed. | `hintLatLngs` | +| pm:autotraceend | `e` | Fired when auto tracing is ended. | | + + ### LayerGroup Leaflet-Geoman can only work correct with `L.FeatureGroup` and `L.GeoJSON` (the extended versions of L.LayerGroup) we need the events `layeradd` and `layerremove`. @@ -921,7 +984,7 @@ Change the language of user-facing copy in Leaflet-Geoman map.pm.setLang('de'); ``` -Currently available languages are `cz`, `da`, `de`, `el`, `en`, `es`, `fa`, `fr`, `hu`, `id`, `it`, `ja`, `nl`, `no`, `pl`, `pt_br`, `ro`, `ru`, `sv`, `tr`, `ua`, `zh` and `zh_tw`. +Currently available languages are `cz`, `da`, `de`, `el`, `en`, `es`, `fa`, `fi`, `fr`, `hu`, `id`, `it`, `ja`, `ko`, `nl`, `no`, `pl`, `pt_br`, `ro`, `ru`, `sv`, `tr`, `ua`, `zh` and `zh_tw`. To add translations to the plugin, you can add [a translation file](src/assets/translations) via Pull Request. You can also provide your own custom translations. @@ -1056,17 +1119,19 @@ map.pm.Toolbar.getBlockPositions(); map.pm.Toolbar.createCustomControl(options); ``` -| Option | Default | Description | -| :--------- | :------- | :----------------------------------------------------------------------------------- | -| name | Required | Name of the control. | -| block | '' | block of the control. `draw`, `edit`, `custom`, `options`⭐ | -| title | '' | Text showing when you hover the control. | -| className | '' | CSS class with the Icon. | -| onClick | - | Function fired when clicking the control. | -| afterClick | - | Function fired after clicking the control. | -| actions | [ ] | Action that appears as tooltip. Look under [Actions](#actions) for more information. | -| toggle | true | Control can be toggled. | -| disabled | false | Control is disabled. | +| Option | Default | Description | +| :---------------------- | :------- | :----------------------------------------------------------------------------------- | +| name | Required | Name of the control. | +| block | '' | block of the control. `draw`, `edit`, `custom`, `options`⭐ | +| title | '' | Text showing when you hover the control. | +| className | '' | CSS class with the Icon. | +| onClick | - | Function fired when clicking the control. | +| afterClick | - | Function fired after clicking the control. | +| actions | [ ] | Action that appears as tooltip. Look under [Actions](#actions) for more information. | +| toggle | true | Control can be toggled. | +| disabled | false | Control is disabled. | +| disableOtherButtons | true | Control disables other buttons if enabled. | +| disableByOtherButtons | true | Control disabled if other buttons is enabled. | #### Inherit from an Existing Control diff --git a/cypress/integration/circle.spec.js b/cypress/integration/circle.spec.js index 02814f05..3a6f5c77 100644 --- a/cypress/integration/circle.spec.js +++ b/cypress/integration/circle.spec.js @@ -284,4 +284,26 @@ describe('Draw Circle', () => { expect(layer.getLatLng().equals(layer2.getLatLng())).to.eq(true); }); }); + + it('change color of circle while drawing', () => { + cy.toolbarButton('circle') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.get(mapSelector).click(200, 200); + cy.get(mapSelector).trigger('mousemove', 300, 300); + + cy.window().then(({ map }) => { + const style = { + color: 'red', + }; + map.pm.setGlobalOptions({ templineStyle: style, hintlineStyle: style }); + + const layer = map.pm.Draw.Circle._layer; + const hintLine = map.pm.Draw.Circle._hintline; + expect(layer.options.color).to.eql('red'); + expect(hintLine.options.color).to.eql('red'); + }); + }); }); diff --git a/cypress/integration/circlemarker.spec.js b/cypress/integration/circlemarker.spec.js index fb7dc7d9..88470dde 100644 --- a/cypress/integration/circlemarker.spec.js +++ b/cypress/integration/circlemarker.spec.js @@ -428,4 +428,77 @@ describe('Draw Circle Marker', () => { }); cy.hasLayers(3); }); + + it('check if snapping works with max radius of circle', () => { + cy.window().then(({ map }) => { + map.pm.setGlobalOptions({ + editable: true, + }); + }); + cy.toolbarButton('circle-marker') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.get(mapSelector).click(320, 250).click(450, 250); + + cy.window().then(({ map }) => { + map.pm.setGlobalOptions({ + maxRadiusCircleMarker: 100, + }); + }); + + cy.get(mapSelector).click(325, 250).click(475, 250); + + cy.window().then(({ map }) => { + const layer = map.pm.getGeomanDrawLayers()[0]; + const layer2 = map.pm.getGeomanDrawLayers()[1]; + expect(layer.getLatLng().equals(layer2.getLatLng())).to.eq(true); + }); + }); + + it('change color of circleMarker while drawing', () => { + cy.toolbarButton('circle-marker') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.get(mapSelector).trigger('mousemove', 300, 300); + + cy.window().then(({ map }) => { + const style = { + color: 'red', + }; + map.pm.setGlobalOptions({ templineStyle: style, hintlineStyle: style }); + + const layer = map.pm.Draw.CircleMarker._layer; + expect(layer.options.color).to.eql('red'); + }); + }); + + it('change color of circleMarker (editable) while drawing', () => { + cy.window().then(({ map }) => { + map.pm.setGlobalOptions({ editable: true }); + }); + + cy.toolbarButton('circle-marker') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.get(mapSelector).click(200, 200); + cy.get(mapSelector).trigger('mousemove', 300, 300); + + cy.window().then(({ map }) => { + const style = { + color: 'red', + }; + map.pm.setGlobalOptions({ templineStyle: style, hintlineStyle: style }); + + const layer = map.pm.Draw.CircleMarker._layer; + const hintLine = map.pm.Draw.CircleMarker._hintline; + expect(layer.options.color).to.eql('red'); + expect(hintLine.options.color).to.eql('red'); + }); + }); }); diff --git a/cypress/integration/globalmodes.spec.js b/cypress/integration/globalmodes.spec.js index 59bbbd99..506e1f02 100644 --- a/cypress/integration/globalmodes.spec.js +++ b/cypress/integration/globalmodes.spec.js @@ -304,4 +304,48 @@ describe('Modes', () => { }); cy.hasVertexMarkers(8); }); + + it('prevent enabling multiple modes at the same time', () => { + cy.toolbarButton('edit').click(); + + cy.toolbarButton('delete') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.toolbarButton('edit') + .closest('.button-container') + .should('not.have.class', 'active'); + }); + + it('re-applies drag mode onAdd', () => { + cy.toolbarButton('polygon').click(); + + const jsonString = + '{"type":"Feature","properties":{},"geometry":{"type":"Polygon","coordinates":[[[-0.155182,51.515687],[-0.155182,51.521028],[-0.124283,51.521028],[-0.124283,51.510345],[-0.155182,51.515687]]]}}'; + + const poly = JSON.parse(jsonString); + + cy.get(mapSelector) + .click(320, 150) + .click(320, 100) + .click(400, 100) + .click(400, 200) + .click(320, 150); + + cy.toolbarButton('drag').click(); + + cy.window().then(({ map }) => { + expect(map.pm.getGeomanLayers()[0].pm.layerDragEnabled()).to.equal(true); + }); + + cy.window().then(({ map, L }) => { + L.geoJSON(poly).addTo(map); + }); + + cy.window().then(({ map }) => { + expect(map.pm.getGeomanLayers()[0].pm.layerDragEnabled()).to.equal(true); + expect(map.pm.getGeomanLayers()[1].pm.layerDragEnabled()).to.equal(true); + }); + }); }); diff --git a/cypress/integration/line.spec.js b/cypress/integration/line.spec.js index 4933e7e8..a4cd7d35 100644 --- a/cypress/integration/line.spec.js +++ b/cypress/integration/line.spec.js @@ -298,4 +298,27 @@ describe('Draw & Edit Line', () => { }); cy.hasLayers(2); }); + + it('change color of line while drawing', () => { + cy.toolbarButton('polyline') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.get(mapSelector).click(200, 200); + cy.get(mapSelector).click(100, 230); + cy.get(mapSelector).trigger('mousemove', 300, 300); + + cy.window().then(({ map }) => { + const style = { + color: 'red', + }; + map.pm.setGlobalOptions({ templineStyle: style, hintlineStyle: style }); + + const layer = map.pm.Draw.Line._layer; + const hintLine = map.pm.Draw.Line._hintline; + expect(layer.options.color).to.eql('red'); + expect(hintLine.options.color).to.eql('red'); + }); + }); }); diff --git a/cypress/integration/marker.spec.js b/cypress/integration/marker.spec.js index d38e8fa1..5c6d611a 100644 --- a/cypress/integration/marker.spec.js +++ b/cypress/integration/marker.spec.js @@ -294,4 +294,26 @@ describe('Draw Marker', () => { expect(updateFired).to.eq(true); }); }); + + it('change icon of Marker while drawing', () => { + cy.toolbarButton('marker') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.get(mapSelector).trigger('mousemove', 300, 300); + + cy.window().then(({ map, L }) => { + map.pm.setGlobalOptions({ + markerStyle: { + icon: L.icon({ + iconUrl: 'someIcon.png', + }), + }, + }); + + const layer = map.pm.Draw.Marker._hintMarker; + expect(layer._icon.src.endsWith('someIcon.png')).to.eql(true); + }); + }); }); diff --git a/cypress/integration/options.spec.js b/cypress/integration/options.spec.js index 60ded48e..b3288bb9 100644 --- a/cypress/integration/options.spec.js +++ b/cypress/integration/options.spec.js @@ -3,41 +3,6 @@ describe('Options', () => { const mapSelector = '#map'; - it('Pinning fires pm:edit', () => { - cy.toolbarButton('polygon').click(); - - cy.window().then(({ map, L }) => { - cy.get(mapSelector) - .click(90, 250) - .click(100, 50) - .click(150, 50) - .click(150, 150) - .click(90, 250) - .then(() => { - let l; - map.eachLayer((layer) => { - if (layer instanceof L.Polygon) { - layer.pm.enable(); - l = layer; - } - }); - return l; - }) - .as('poly'); - }); - - cy.get('@poly').then((poly) => { - poly.on('pm:edit', (e) => { - console.log(e); - }); - }); - - cy.toolbarButton('marker').click(); - cy.get(mapSelector).click(150, 150); - - cy.toolbarButton('edit').click(); - }); - it('sets global options', () => { cy.toolbarButton('polygon').click(); @@ -165,4 +130,17 @@ describe('Options', () => { ); }); }); + + it('fires `pm:globaloptionschanged`', () => { + cy.window().then(({ map }) => { + let fired = false; + map.on('pm:globaloptionschanged', () => { + fired = true; + }); + + map.pm.setGlobalOptions({ snapSegment: false }); + + expect(fired).to.equal(true); + }); + }); }); diff --git a/cypress/integration/polygon.spec.js b/cypress/integration/polygon.spec.js index ac5f9cd9..48a036ea 100644 --- a/cypress/integration/polygon.spec.js +++ b/cypress/integration/polygon.spec.js @@ -900,6 +900,13 @@ describe('Draw & Edit Poly', () => { cy.window().then(({ map }) => { expect(2).to.eq(map.pm.getGeomanDrawLayers().length); }); + + cy.toolbarButton('delete').click(); + cy.get(mapSelector).click(160, 50); + + cy.window().then(({ map }) => { + expect(1).to.eq(map.pm.getGeomanDrawLayers().length); + }); }); it('requireSnapToFinish', () => { @@ -1176,4 +1183,88 @@ describe('Draw & Edit Poly', () => { cy.toolbarButton('edit').click(); }).to.not.throw(); }); + + it('remove vertex & layer by right-click', () => { + cy.toolbarButton('polygon').click(); + cy.get(mapSelector) + .click(150, 250) + .click(160, 50) + .click(250, 50) + .click(150, 250); + + cy.toolbarButton('edit').click(); + cy.hasDrawnLayers(1); + + // Add Vertex + cy.get(mapSelector).click(205, 50); + cy.hasVertexMarkers(4); + + // Remove Vertex + cy.get(mapSelector).rightclick(205, 50); + cy.hasVertexMarkers(3); + + cy.get(mapSelector).rightclick(150, 250); + cy.hasDrawnLayers(0); + }); + + it('re-render marker-handlers if hole is removed by right-click', () => { + cy.toolbarButton('polygon').click(); + cy.get(mapSelector) + .click(150, 250) + .click(150, 50) + .click(650, 50) + .click(650, 250) + .click(150, 250); + + cy.toolbarButton('cut').click(); + cy.get(mapSelector) + .click(250, 200) + .click(250, 100) + .click(450, 200) + .click(250, 200); + + cy.toolbarButton('edit').click(); + cy.hasVertexMarkers(7); + + // Remove hole + cy.get(mapSelector).rightclick(250, 200); + cy.hasVertexMarkers(4); + }); + + it('show correct shape for Polygon while drawing', () => { + cy.toolbarButton('polygon') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.get(mapSelector).click(150, 150).click(450, 150).click(450, 400); + + cy.window().then(({ map }) => { + const polygon = map.pm.Draw.Polygon._layer; + expect(polygon.pm.getShape()).to.equal('Polygon'); + }); + }); + + it('change color of Polygon while drawing', () => { + cy.toolbarButton('polygon') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.get(mapSelector).click(220, 220); + cy.get(mapSelector).click(100, 230); + cy.get(mapSelector).trigger('mousemove', 300, 300); + + cy.window().then(({ map }) => { + const style = { + color: 'red', + }; + map.pm.setGlobalOptions({ templineStyle: style, hintlineStyle: style }); + + const layer = map.pm.Draw.Polygon._layer; + const hintLine = map.pm.Draw.Polygon._hintline; + expect(layer.options.color).to.eql('red'); + expect(hintLine.options.color).to.eql('red'); + }); + }); }); diff --git a/cypress/integration/rectangle.spec.js b/cypress/integration/rectangle.spec.js index ba02f00c..b083cf38 100644 --- a/cypress/integration/rectangle.spec.js +++ b/cypress/integration/rectangle.spec.js @@ -860,4 +860,24 @@ describe('Draw Rectangle', () => { expect(mapCanvas.getRenderer(rect2) instanceof L.SVG).to.eq(true); }); }); + + it('change color of Rectangle while drawing', () => { + cy.toolbarButton('rectangle') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.get(mapSelector).click(220, 220); + cy.get(mapSelector).trigger('mousemove', 300, 300); + + cy.window().then(({ map }) => { + const style = { + color: 'red', + }; + map.pm.setGlobalOptions({ pathOptions: style }); + + const layer = map.pm.Draw.Rectangle._layer; + expect(layer.options.color).to.eql('red'); + }); + }); }); diff --git a/cypress/integration/rotation.spec.js b/cypress/integration/rotation.spec.js index c2bf7436..dbc90e9b 100644 --- a/cypress/integration/rotation.spec.js +++ b/cypress/integration/rotation.spec.js @@ -207,4 +207,30 @@ describe('Rotation', () => { expect(!!rotatePoly._map).to.eq(false); }); }); + + it('rotate a new added layer', () => { + cy.window().then(({ map, L }) => { + expect(() => { + const coords = [ + [1, 2], + [3, 4], + ]; + const rect = L.rectangle(coords).addTo(map); + rect.pm.rotateLayer(50); + }).to.not.throw(); + }); + }); + + it("doesn't return the rotation help-layer over getGeomanLayers()", () => { + cy.window().then(({ map, L }) => { + const coords = [ + [1, 2], + [3, 4], + ]; + const rect = L.rectangle(coords).addTo(map); + rect.pm.enableRotate(); + + expect(map.pm.getGeomanLayers().length).to.eq(1); + }); + }); }); diff --git a/cypress/integration/text.spec.js b/cypress/integration/text.spec.js index f7697d86..bb174c90 100644 --- a/cypress/integration/text.spec.js +++ b/cypress/integration/text.spec.js @@ -427,6 +427,28 @@ describe('Text Layer', () => { expect(textLayer.pm.getText()).to.eq('Text Layer'); }); }); + it('unselect text on disable', () => { + cy.window().then(({ map, L }) => { + const textLayer = L.marker(map.getCenter(), { + textMarker: true, + text: 'Text Layer', + }).addTo(map); + expect(textLayer.pm.getText()).to.eq('Text Layer'); + + const textarea = textLayer.pm.getElement(); + textLayer.pm.enable(); + textarea.focus(); + textarea.setSelectionRange(2, 5); + expect(textarea.selectionStart).to.eq(2); + expect(textarea.selectionEnd).to.eq(5); + + textLayer.pm.disable(); + expect(textarea.selectionStart).to.eq(0); + expect(textarea.selectionEnd).to.eq(0); + }); + }); + }); + describe('Events', () => { it("fire event 'pm:textchange'", () => { let textLayer; let event = ''; @@ -450,24 +472,124 @@ describe('Text Layer', () => { expect(event).to.eq('pm:textchange'); }); }); - it('unselect text on disable', () => { + + it("fire event 'pm:edit'", () => { + let textLayer; + let event = ''; cy.window().then(({ map, L }) => { - const textLayer = L.marker(map.getCenter(), { + textLayer = L.marker(map.getCenter(), { textMarker: true, - text: 'Text Layer', + text: '', }).addTo(map); - expect(textLayer.pm.getText()).to.eq('Text Layer'); + textLayer.pm.enable(); + textLayer.pm.focus(); - const textarea = textLayer.pm.getElement(); + textLayer.on('pm:edit', (e) => { + event = e.type; + }); + + cy.get(textLayer.pm.getElement()).type('Hello World'); + }); + + cy.window().then(() => { + textLayer.pm.blur(); + expect(textLayer.pm.getText()).to.eq('Hello World'); + expect(event).to.eq('pm:edit'); + }); + }); + + it("fire event 'pm:update'", () => { + let textLayer; + let event = ''; + cy.window().then(({ map, L }) => { + textLayer = L.marker(map.getCenter(), { + textMarker: true, + text: '', + }).addTo(map); textLayer.pm.enable(); - textarea.focus(); - textarea.setSelectionRange(2, 5); - expect(textarea.selectionStart).to.eq(2); - expect(textarea.selectionEnd).to.eq(5); + textLayer.pm.focus(); + + textLayer.on('pm:update', (e) => { + event = e.type; + }); + + cy.get(textLayer.pm.getElement()).type('Hello World'); + }); + cy.window().then(() => { textLayer.pm.disable(); - expect(textarea.selectionStart).to.eq(0); - expect(textarea.selectionEnd).to.eq(0); + expect(textLayer.pm.getText()).to.eq('Hello World'); + expect(event).to.eq('pm:update'); + }); + }); + + it("fire event 'pm:textfocus'", () => { + let textLayer; + let event = ''; + cy.window().then(({ map, L }) => { + textLayer = L.marker(map.getCenter(), { + textMarker: true, + text: '', + }).addTo(map); + textLayer.pm.enable(); + + textLayer.on('pm:textfocus', (e) => { + event = e.type; + }); + textLayer.pm.focus(); + }); + + cy.window().then(() => { + expect(event).to.eq('pm:textfocus'); + }); + }); + + it("fire event 'pm:textblur'", () => { + let textLayer; + let event = ''; + cy.window().then(({ map, L }) => { + textLayer = L.marker(map.getCenter(), { + textMarker: true, + text: '', + }).addTo(map); + textLayer.pm.enable(); + + textLayer.on('pm:textblur', (e) => { + event = e.type; + }); + textLayer.pm.focus(); + textLayer.pm.blur(); + }); + + cy.window().then(() => { + expect(event).to.eq('pm:textblur'); + }); + }); + + it("fire event 'pm:textblur' only once", () => { + let textLayer; + let event = ''; + let count = 0; + cy.window().then(({ map, L }) => { + textLayer = L.marker(map.getCenter(), { + textMarker: true, + text: '', + }).addTo(map); + textLayer.pm.enable(); + + count = 0; + textLayer.on('pm:textblur', (e) => { + count += 1; + event = e.type; + }); + textLayer.pm.focus(); + textLayer.pm.blur(); + textLayer.pm.blur(); + }); + + cy.window().then(() => { + expect(event).to.eq('pm:textblur'); + expect(count).to.eq(1); }); }); }); diff --git a/cypress/integration/tooltips.spec.js b/cypress/integration/tooltips.spec.js index 112ba999..5a961fa4 100644 --- a/cypress/integration/tooltips.spec.js +++ b/cypress/integration/tooltips.spec.js @@ -42,7 +42,10 @@ describe('Shows Tooltips', () => { cy.get(mapSelector).click(290, 250); + cy.wait(500); + cy.get('.leaflet-tooltip-bottom').then((el) => { + expect(el.length).to.eq(1); expect(el).to.have.text('Click to place marker'); }); @@ -204,6 +207,8 @@ describe('Shows Tooltips', () => { cy.get(mapSelector).click(290, 250); + cy.wait(500); + cy.get('.leaflet-tooltip-bottom').then((el) => { expect(el).to.have.text('Presiona para colocar un marcador de círculo'); }); diff --git a/cypress/support/index.js b/cypress/support/index.js index 781f1bff..887c410b 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -30,6 +30,7 @@ beforeEach(() => { { attribution: '© OpenStreetMap contributors', + maxZoom: 22, } ); diff --git a/index.html b/index.html index 26548d09..eb2081b8 100644 --- a/index.html +++ b/index.html @@ -3,10 +3,7 @@ Leaflet Geometry Management - + diff --git a/leaflet-geoman.d.ts b/leaflet-geoman.d.ts index ab079d05..9adfc90d 100644 --- a/leaflet-geoman.d.ts +++ b/leaflet-geoman.d.ts @@ -293,11 +293,21 @@ declare module 'leaflet' { once(type: 'pm:change', fn: PM.ChangeEventHandler): this; off(type: 'pm:change', fn?: PM.ChangeEventHandler): this; - /** Fired when position / coordinates of a layer changed. */ + /** Fired when the text of a layer is changed. */ on(type: 'pm:textchange', fn: PM.TextChangeEventHandler): this; once(type: 'pm:textchange', fn: PM.TextChangeEventHandler): this; off(type: 'pm:textchange', fn?: PM.TextChangeEventHandler): this; + /** Fired when the text layer is focused. */ + on(type: 'pm:textfocus', fn: PM.TextFocusEventHandler): this; + once(type: 'pm:textfocus', fn: PM.TextFocusEventHandler): this; + off(type: 'pm:textfocus', fn?: PM.TextFocusEventHandler): this; + + /** Fired when the text layer is blurred. */ + on(type: 'pm:textblur', fn: PM.TextBlurEventHandler): this; + once(type: 'pm:textblur', fn: PM.TextBlurEventHandler): this; + off(type: 'pm:textblur', fn?: PM.TextBlurEventHandler): this; + /****************************************** * * TODO: EDIT MODE EVENTS ON MAP ONLY @@ -493,11 +503,13 @@ declare module 'leaflet' { | 'en' | 'es' | 'fa' + | 'fi' | 'fr' | 'hu' | 'id' | 'it' | 'ja' + | 'ko' | 'nl' | 'no' | 'pl' @@ -867,8 +879,8 @@ declare module 'leaflet' { /** Disables rotate mode on the layer. */ disableRotate(): void; - /** Toggles rotate mode on the layer. */ - rotateEnabled(): void; + /** Returns if rotate mode is enabled for the layer. */ + rotateEnabled(): boolean; /** Rotates the layer by x degrees. */ rotateLayer(degrees: number): void; @@ -878,6 +890,9 @@ declare module 'leaflet' { /** Returns the angle of the layer in degrees. */ getAngle(): number; + + /** Set the initial angle of the layer in degrees. */ + setInitAngle(degrees: number): void; } interface Draw { @@ -1422,6 +1437,14 @@ declare module 'leaflet' { layer: L.Layer; text: string; }) => void; + export type TextFocusEventHandler = (e: { + shape: PM.SUPPORTED_SHAPES; + layer: L.Layer; + }) => void; + export type TextBlurEventHandler = (e: { + shape: PM.SUPPORTED_SHAPES; + layer: L.Layer; + }) => void; /** * EDIT MODE MAP EVENT HANDLERS diff --git a/package-lock.json b/package-lock.json index df96a9ae..de6365fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@geoman-io/leaflet-geoman-free", - "version": "2.13.1", + "version": "2.14.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@geoman-io/leaflet-geoman-free", - "version": "2.13.0", + "version": "2.13.1", "license": "MIT", "dependencies": { "@turf/boolean-contains": "^6.5.0", @@ -31,7 +31,7 @@ "eslint-plugin-cypress": "2.11.3", "eslint-plugin-import": "2.22.1", "file-loader": "6.2.0", - "leaflet": "1.9.2", + "leaflet": "1.9.3", "mini-css-extract-plugin": "1.6.0", "prettier": "2.2.1", "prosthetic-hand": "1.3.1", @@ -5452,9 +5452,9 @@ } }, "node_modules/leaflet": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.2.tgz", - "integrity": "sha512-Kc77HQvWO+y9y2oIs3dn5h5sy2kr3j41ewdqCMEUA4N89lgfUUfOBy7wnnHEstDpefiGFObq12FdopGRMx4J7g==", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz", + "integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ==", "dev": true }, "node_modules/levn": { @@ -12338,9 +12338,9 @@ "dev": true }, "leaflet": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.2.tgz", - "integrity": "sha512-Kc77HQvWO+y9y2oIs3dn5h5sy2kr3j41ewdqCMEUA4N89lgfUUfOBy7wnnHEstDpefiGFObq12FdopGRMx4J7g==", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz", + "integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ==", "dev": true }, "levn": { diff --git a/package.json b/package.json index 70fdcefb..e31c67e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@geoman-io/leaflet-geoman-free", - "version": "2.13.1", + "version": "2.14.1", "description": "A Leaflet Plugin For Editing Geometry Layers in Leaflet 1.0", "keywords": [ "leaflet", @@ -43,7 +43,7 @@ "eslint-plugin-cypress": "2.11.3", "eslint-plugin-import": "2.22.1", "file-loader": "6.2.0", - "leaflet": "1.9.2", + "leaflet": "1.9.3", "mini-css-extract-plugin": "1.6.0", "prettier": "2.2.1", "prosthetic-hand": "1.3.1", diff --git a/src/assets/translations/de.json b/src/assets/translations/de.json index 478aa265..d91f8bc7 100644 --- a/src/assets/translations/de.json +++ b/src/assets/translations/de.json @@ -30,6 +30,19 @@ "snappingButton": "Bewegter Layer an andere Layer oder Vertexe einhacken", "pinningButton": "Vertexe an der gleichen Position verknüpfen", "rotateButton": "Layer drehen", - "drawTextButton": "Text zeichnen" + "drawTextButton": "Text zeichnen", + "scaleButton": "Layer skalieren", + "autoTracingButton": "Linie automatisch nachzeichen" + }, + "measurements": { + "totalLength": "Länge", + "segmentLength": "Segment Länge", + "area": "Fläche", + "radius": "Radius", + "perimeter": "Umfang", + "height": "Höhe", + "width": "Breite", + "coordinates": "Position", + "coordinatesMarker": "Position Marker" } } diff --git a/src/assets/translations/en.json b/src/assets/translations/en.json index a78d9bdb..0d2ac852 100644 --- a/src/assets/translations/en.json +++ b/src/assets/translations/en.json @@ -30,6 +30,19 @@ "snappingButton": "Snap dragged marker to other layers and vertices", "pinningButton": "Pin shared vertices together", "rotateButton": "Rotate Layers", - "drawTextButton": "Draw Text" + "drawTextButton": "Draw Text", + "scaleButton": "Scale Layers", + "autoTracingButton": "Auto trace Line" + }, + "measurements": { + "totalLength": "Length", + "segmentLength": "Segment length", + "area": "Area", + "radius": "Radius", + "perimeter": "Perimeter", + "height": "Height", + "width": "Width", + "coordinates": "Position", + "coordinatesMarker": "Position Marker" } } diff --git a/src/assets/translations/fi.json b/src/assets/translations/fi.json new file mode 100644 index 00000000..52290283 --- /dev/null +++ b/src/assets/translations/fi.json @@ -0,0 +1,35 @@ +{ + "tooltips": { + "placeMarker": "Klikkaa asettaaksesi merkin", + "firstVertex": "Klikkaa asettaakseni ensimmäisen osuuden", + "continueLine": "Klikkaa jatkaaksesi piirtämistä", + "finishLine": "Klikkaa olemassa olevaa merkkiä lopettaaksesi", + "finishPoly": "Klikkaa ensimmäistä merkkiä lopettaaksesi", + "finishRect": "Klikkaa lopettaaksesi", + "startCircle": "Klikkaa asettaaksesi ympyrän keskipisteen", + "finishCircle": "Klikkaa lopettaaksesi ympyrän", + "placeCircleMarker": "Klikkaa asettaaksesi ympyrämerkin", + "placeText": "Klikkaa asettaaksesi tekstin" + }, + "actions": { + "finish": "Valmis", + "cancel": "Peruuta", + "removeLastVertex": "Poista viimeinen osuus" + }, + "buttonTitles": { + "drawMarkerButton": "Piirrä merkkejä", + "drawPolyButton": "Piirrä monikulmioita", + "drawLineButton": "Piirrä viivoja", + "drawCircleButton": "Piirrä ympyrä", + "drawRectButton": "Piirrä neliskulmioita", + "editButton": "Muokkaa", + "dragButton": "Siirrä", + "cutButton": "Leikkaa", + "deleteButton": "Poista", + "drawCircleMarkerButton": "Piirrä ympyrämerkki", + "snappingButton": "Kiinnitä siirrettävä merkki toisiin muotoihin", + "pinningButton": "Kiinnitä jaetut muodot yhteen", + "rotateButton": "Käännä", + "drawTextButton": "Piirrä tekstiä" + } +} diff --git a/src/assets/translations/index.js b/src/assets/translations/index.js index 2067ac44..fff25242 100644 --- a/src/assets/translations/index.js +++ b/src/assets/translations/index.js @@ -23,6 +23,8 @@ import ua from './ua.json'; import tr from './tr.json'; import cz from './cz.json'; import ja from './ja.json'; +import fi from './fi.json'; +import ko from './ko.json'; export default { en, @@ -48,4 +50,6 @@ export default { tr, cz, ja, + fi, + ko, }; diff --git a/src/assets/translations/ko.json b/src/assets/translations/ko.json new file mode 100644 index 00000000..aa1783a5 --- /dev/null +++ b/src/assets/translations/ko.json @@ -0,0 +1,35 @@ +{ + "tooltips": { + "placeMarker": "마커 위치를 클릭하세요", + "firstVertex": "첫번째 꼭지점 위치을 클릭하세요", + "continueLine": "계속 그리려면 클릭하세요", + "finishLine": "끝내려면 기존 마커를 클릭하세요", + "finishPoly": "끝내려면 처음 마커를 클릭하세요", + "finishRect": "끝내려면 클릭하세요", + "startCircle": "원의 중심이 될 위치를 클릭하세요", + "finishCircle": "원을 끝내려면 클릭하세요", + "placeCircleMarker": "원 마커 위치를 클릭하세요", + "placeText": "텍스트 위치를 클릭하세요" + }, + "actions": { + "finish": "끝내기", + "cancel": "취소", + "removeLastVertex": "마지막 꼭지점 제거" + }, + "buttonTitles": { + "drawMarkerButton": "마커 그리기", + "drawPolyButton": "다각형 그리기", + "drawLineButton": "다각선 그리기", + "drawCircleButton": "원 그리기", + "drawRectButton": "직사각형 그리기", + "editButton": "레이어 편집하기", + "dragButton": "레이어 끌기", + "cutButton": "레이어 자르기", + "deleteButton": "레이어 제거하기", + "drawCircleMarkerButton": "원 마커 그리기", + "snappingButton": "잡아끈 마커를 다른 레이어 및 꼭지점에 들러붙게 하기", + "pinningButton": "공유 꼭지점을 함께 찍기", + "rotateButton": "레이어 회전하기", + "drawTextButton": "텍스트 그리기" + } +} diff --git a/src/css/layers.css b/src/css/layers.css index a692e9aa..a4763d49 100644 --- a/src/css/layers.css +++ b/src/css/layers.css @@ -57,16 +57,16 @@ } .pm-textarea { - background-color: #fff; - color: #000; - resize: none; - border: none; - outline: 0; - cursor: pointer; - border-radius: 3px; - padding-left: 7px; - padding-bottom: 0; - padding-top: 4px; + background-color: #fff; + color: #000; + resize: none; + border: none; + outline: 0; + cursor: pointer; + border-radius: 3px; + padding-left: 7px; + padding-bottom: 0; + padding-top: 4px; } .leaflet-pm-draggable .pm-textarea { diff --git a/src/js/Draw/L.PM.Draw.Circle.js b/src/js/Draw/L.PM.Draw.Circle.js index 908e2b64..2df71226 100644 --- a/src/js/Draw/L.PM.Draw.Circle.js +++ b/src/js/Draw/L.PM.Draw.Circle.js @@ -18,12 +18,12 @@ Draw.Circle = Draw.extend({ this._enabled = true; // create a new layergroup - this._layerGroup = new L.LayerGroup(); + this._layerGroup = new L.FeatureGroup(); this._layerGroup._pmTempLayer = true; this._layerGroup.addTo(this._map); // this is the circle we want to draw - this._layer = L.circle([0, 0], { + this._layer = L.circle(this._map.getCenter(), { ...this.options.templineStyle, radius: 0, }); @@ -31,7 +31,7 @@ Draw.Circle = Draw.extend({ this._layer._pmTempLayer = true; // this is the marker in the center of the circle - this._centerMarker = L.marker([0, 0], { + this._centerMarker = L.marker(this._map.getCenter(), { icon: L.divIcon({ className: 'marker-icon' }), draggable: false, zIndexOffset: 100, @@ -40,7 +40,7 @@ Draw.Circle = Draw.extend({ this._centerMarker._pmTempLayer = true; // this is the hintmarker on the mouse cursor - this._hintMarker = L.marker([0, 0], { + this._hintMarker = L.marker(this._map.getCenter(), { zIndexOffset: 110, icon: L.divIcon({ className: 'marker-icon cursor-marker' }), }); @@ -284,14 +284,14 @@ Draw.Circle = Draw.extend({ } }, _getNewDestinationOfHintMarker() { - const latlng = this._centerMarker.getLatLng(); let secondLatLng = this._hintMarker.getLatLng(); - const distance = latlng.distanceTo(secondLatLng); - - if (latlng.equals(L.latLng([0, 0]))) { + if (!this._layerGroup.hasLayer(this._centerMarker)) { return secondLatLng; } + const latlng = this._centerMarker.getLatLng(); + const distance = latlng.distanceTo(secondLatLng); + if ( this.options.minRadiusCircle && distance < this.options.minRadiusCircle @@ -320,7 +320,7 @@ Draw.Circle = Draw.extend({ const latlng = this._centerMarker.getLatLng(); const secondLatLng = this._hintMarker.getLatLng(); const distance = latlng.distanceTo(secondLatLng); - if (latlng.equals(L.latLng([0, 0]))) { + if (!this._layerGroup.hasLayer(this._centerMarker)) { // do nothing } else if ( this.options.minRadiusCircle && @@ -337,4 +337,8 @@ Draw.Circle = Draw.extend({ // calculate the new latlng of marker if the snapped latlng radius is out of min/max this._hintMarker.setLatLng(this._getNewDestinationOfHintMarker()); }, + setStyle() { + this._layer?.setStyle(this.options.templineStyle); + this._hintline?.setStyle(this.options.hintlineStyle); + }, }); diff --git a/src/js/Draw/L.PM.Draw.CircleMarker.js b/src/js/Draw/L.PM.Draw.CircleMarker.js index c6760794..0d9529c8 100644 --- a/src/js/Draw/L.PM.Draw.CircleMarker.js +++ b/src/js/Draw/L.PM.Draw.CircleMarker.js @@ -24,21 +24,21 @@ Draw.CircleMarker = Draw.Marker.extend({ if (this.options.editable) { // we need to set the radius to 0 without overwriting the CircleMarker style const templineStyle = {}; - L.setOptions(templineStyle, this.options.templineStyle); + L.extend(templineStyle, this.options.templineStyle); templineStyle.radius = 0; // create a new layergroup - this._layerGroup = new L.LayerGroup(); + this._layerGroup = new L.FeatureGroup(); this._layerGroup._pmTempLayer = true; this._layerGroup.addTo(this._map); // this is the circle we want to draw - this._layer = L.circleMarker([0, 0], templineStyle); + this._layer = L.circleMarker(this._map.getCenter(), templineStyle); this._setPane(this._layer, 'layerPane'); this._layer._pmTempLayer = true; // this is the marker in the center of the circle - this._centerMarker = L.marker([0, 0], { + this._centerMarker = L.marker(this._map.getCenter(), { icon: L.divIcon({ className: 'marker-icon' }), draggable: false, zIndexOffset: 100, @@ -47,7 +47,7 @@ Draw.CircleMarker = Draw.Marker.extend({ this._centerMarker._pmTempLayer = true; // this is the hintmarker on the mouse cursor - this._hintMarker = L.marker([0, 0], { + this._hintMarker = L.marker(this._map.getCenter(), { zIndexOffset: 110, icon: L.divIcon({ className: 'marker-icon cursor-marker' }), }); @@ -87,7 +87,10 @@ Draw.CircleMarker = Draw.Marker.extend({ this._map.on('click', this._createMarker, this); // this is the hintmarker on the mouse cursor - this._hintMarker = L.circleMarker([0, 0], this.options.templineStyle); + this._hintMarker = L.circleMarker( + this._map.getCenter(), + this.options.templineStyle + ); this._setPane(this._hintMarker, 'layerPane'); this._hintMarker._pmTempLayer = true; this._hintMarker.addTo(this._map); @@ -375,12 +378,12 @@ Draw.CircleMarker = Draw.Marker.extend({ _getNewDestinationOfHintMarker() { let secondLatLng = this._hintMarker.getLatLng(); if (this.options.editable) { - const latlng = this._centerMarker.getLatLng(); - - if (latlng.equals(L.latLng([0, 0]))) { + if (!this._layerGroup.hasLayer(this._centerMarker)) { return secondLatLng; } + const latlng = this._centerMarker.getLatLng(); + const distance = this._map .project(latlng) .distanceTo(this._map.project(secondLatLng)); @@ -416,7 +419,9 @@ Draw.CircleMarker = Draw.Marker.extend({ const distance = this._map .project(latlng) .distanceTo(this._map.project(secondLatLng)); - if ( + if (!this._layerGroup.hasLayer(this._centerMarker)) { + // do nothing + } else if ( this.options.minRadiusCircleMarker && distance < this.options.minRadiusCircleMarker ) { @@ -438,4 +443,13 @@ Draw.CircleMarker = Draw.Marker.extend({ const pointB = L.point(pointA.x + radius, pointA.y); return this._map.unproject(pointB).distanceTo(center); }, + setStyle() { + const templineStyle = {}; + L.extend(templineStyle, this.options.templineStyle); + if (this.options.editable) { + templineStyle.radius = 0; + } + this._layer?.setStyle(templineStyle); + this._hintline?.setStyle(this.options.hintlineStyle); + }, }); diff --git a/src/js/Draw/L.PM.Draw.Cut.js b/src/js/Draw/L.PM.Draw.Cut.js index 11e9252c..0b224b49 100644 --- a/src/js/Draw/L.PM.Draw.Cut.js +++ b/src/js/Draw/L.PM.Draw.Cut.js @@ -28,7 +28,23 @@ Draw.Cut = Draw.Polygon.extend({ } } + // If snap finish is required but the last marker wasn't snapped, do not finish the shape! + if ( + this.options.requireSnapToFinish && + !this._hintMarker._snapped && + !this._isFirstLayer() + ) { + return; + } + + // get coordinates const coords = this._layer.getLatLngs(); + + // only finish the shape if there are 3 or more vertices + if (coords.length <= 2) { + return; + } + const polygonLayer = L.polygon(coords, this.options.pathOptions); // readout information about the latlngs like snapping points polygonLayer._latlngInfos = this._layer._latlngInfo; diff --git a/src/js/Draw/L.PM.Draw.Line.js b/src/js/Draw/L.PM.Draw.Line.js index fee9ac13..89e1a45d 100644 --- a/src/js/Draw/L.PM.Draw.Line.js +++ b/src/js/Draw/L.PM.Draw.Line.js @@ -16,13 +16,18 @@ Draw.Line = Draw.extend({ // enable draw mode this._enabled = true; + this._markers = []; + // create a new layergroup - this._layerGroup = new L.LayerGroup(); + this._layerGroup = new L.FeatureGroup(); this._layerGroup._pmTempLayer = true; this._layerGroup.addTo(this._map); // this is the polyLine that'll make up the polygon - this._layer = L.polyline([], this.options.templineStyle); + this._layer = L.polyline([], { + ...this.options.templineStyle, + pmIgnore: false, + }); this._setPane(this._layer, 'layerPane'); this._layer._pmTempLayer = true; this._layerGroup.addLayer(this._layer); @@ -174,7 +179,7 @@ Draw.Line = Draw.extend({ // if self-intersection is forbidden, handle it if (!this.options.allowSelfIntersection) { - this._handleSelfIntersection(true, e.latlng); + this._handleSelfIntersection(true, this._hintMarker.getLatLng()); } const latlngs = this._layer._defaultShape().slice(); latlngs.push(this._hintMarker.getLatLng()); @@ -238,9 +243,16 @@ Draw.Line = Draw.extend({ const latlng = this._hintMarker.getLatLng(); // check if the first and this vertex have the same latlng - if (latlng.equals(this._layer.getLatLngs()[0])) { + // or the last vertex and the hintMarker have the same latlng (dbl-click) + const latlngs = this._layer.getLatLngs(); + + const lastLatLng = latlngs[latlngs.length - 1]; + if ( + latlng.equals(latlngs[0]) || + (latlngs.length > 0 && latlng.equals(lastLatLng)) + ) { // yes? finish the polygon - this._finishShape(e); + this._finishShape(); // "why?", you ask? Because this happens when we snap the last vertex to the first one // and then click without hitting the last marker. Click happens on the map @@ -258,7 +270,7 @@ Draw.Line = Draw.extend({ const newMarker = this._createMarker(latlng); this._setTooltipText(); - this._hintline.setLatLngs([latlng, latlng]); + this._setHintLineAfterNewVertex(latlng); this._fireVertexAdded(newMarker, undefined, latlng, 'Draw'); this._change(this._layer.getLatLngs()); @@ -267,40 +279,53 @@ Draw.Line = Draw.extend({ this._finishShape(e); } }, + _setHintLineAfterNewVertex(hintMarkerLatLng) { + // make the new drawn line (with another style) visible + this._hintline.setLatLngs([hintMarkerLatLng, hintMarkerLatLng]); + }, _removeLastVertex() { - // remove last coords - const coords = this._layer.getLatLngs(); - const removedCoord = coords.pop(); + const markers = this._markers; - // if all coords are gone, cancel drawing - if (coords.length < 1) { + // if all markers are gone, cancel drawing + if (markers.length <= 1) { this.disable(); return; } - // find corresponding marker - const marker = this._layerGroup - .getLayers() - .filter((l) => l instanceof L.Marker) - .filter((l) => !L.DomUtil.hasClass(l._icon, 'cursor-marker')) - .find((l) => l.getLatLng() === removedCoord); + // remove last coords + let coords = this._layer.getLatLngs(); + + const removedMarker = markers[markers.length - 1]; - const markers = this._layerGroup - .getLayers() - .filter((l) => l instanceof L.Marker); // the index path to the marker inside the multidimensional marker array - const { indexPath } = L.PM.Utils.findDeepMarkerIndex(markers, marker); + const { indexPath } = L.PM.Utils.findDeepMarkerIndex( + markers, + removedMarker + ); + + // remove last marker from array + markers.pop(); // remove that marker - this._layerGroup.removeLayer(marker); + this._layerGroup.removeLayer(removedMarker); + + const markerPrevious = markers[markers.length - 1]; + + // no need for findDeepMarkerIndex because the coords are always flat (Polyline) no matter if Line or Polygon + const indexMarkerPrev = coords.indexOf(markerPrevious.getLatLng()); + + // +1 don't cut out the previous marker + coords = coords.slice(0, indexMarkerPrev + 1); + // update layer with new coords this._layer.setLatLngs(coords); + this._layer._latlngInfo.pop(); // sync the hintline again this._syncHintLine(); this._setTooltipText(); - this._fireVertexRemoved(marker, indexPath, 'Draw'); + this._fireVertexRemoved(removedMarker, indexPath, 'Draw'); this._change(this._layer.getLatLngs()); }, _finishShape() { @@ -360,6 +385,7 @@ Draw.Line = Draw.extend({ // add it to the map this._layerGroup.addLayer(marker); + this._markers.push(marker); // a click on any marker finishes this shape marker.on('click', this._finishShape, this); @@ -381,4 +407,8 @@ Draw.Line = Draw.extend({ _change(latlngs) { this._fireChange(latlngs, 'Draw'); }, + setStyle() { + this._layer?.setStyle(this.options.templineStyle); + this._hintline?.setStyle(this.options.hintlineStyle); + }, }); diff --git a/src/js/Draw/L.PM.Draw.Marker.js b/src/js/Draw/L.PM.Draw.Marker.js index 4679e83d..786287a6 100644 --- a/src/js/Draw/L.PM.Draw.Marker.js +++ b/src/js/Draw/L.PM.Draw.Marker.js @@ -22,7 +22,10 @@ Draw.Marker = Draw.extend({ this._map.pm.Toolbar.toggleButton(this.toolbarButtonName, true); // this is the hintmarker on the mouse cursor - this._hintMarker = L.marker([0, 0], this.options.markerStyle); + this._hintMarker = L.marker( + this._map.getCenter(), + this.options.markerStyle + ); this._setPane(this._hintMarker, 'markerPane'); this._hintMarker._pmTempLayer = true; this._hintMarker.addTo(this._map); @@ -178,4 +181,9 @@ Draw.Marker = Draw.extend({ this.disable(); } }, + setStyle() { + if (this.options.markerStyle?.icon) { + this._hintMarker?.setIcon(this.options.markerStyle.icon); + } + }, }); diff --git a/src/js/Draw/L.PM.Draw.Polygon.js b/src/js/Draw/L.PM.Draw.Polygon.js index 1310fd09..f42c1458 100644 --- a/src/js/Draw/L.PM.Draw.Polygon.js +++ b/src/js/Draw/L.PM.Draw.Polygon.js @@ -7,6 +7,11 @@ Draw.Polygon = Draw.Line.extend({ this._shape = 'Polygon'; this.toolbarButtonName = 'drawPolygon'; }, + enable(options) { + L.PM.Draw.Line.prototype.enable.call(this, options); + // Overwrite the shape "Line" of this._layer + this._layer.pm._shape = 'Polygon'; + }, _createMarker(latlng) { // create the new marker const marker = new L.Marker(latlng, { @@ -20,6 +25,7 @@ Draw.Polygon = Draw.Line.extend({ // add it to the map this._layerGroup.addLayer(marker); + this._markers.push(marker); // if the first marker gets clicked again, finish this shape if (this._layer.getLatLngs().flat().length === 1) { diff --git a/src/js/Draw/L.PM.Draw.Rectangle.js b/src/js/Draw/L.PM.Draw.Rectangle.js index 16da2f48..e85df516 100644 --- a/src/js/Draw/L.PM.Draw.Rectangle.js +++ b/src/js/Draw/L.PM.Draw.Rectangle.js @@ -16,7 +16,7 @@ Draw.Rectangle = Draw.extend({ this._enabled = true; // create a new layergroup - this._layerGroup = new L.LayerGroup(); + this._layerGroup = new L.FeatureGroup(); this._layerGroup._pmTempLayer = true; this._layerGroup.addTo(this._map); @@ -33,7 +33,7 @@ Draw.Rectangle = Draw.extend({ // this is the marker at the origin of the rectangle // this needs to be present, for tracking purposes, but we'll make it invisible if a user doesn't want to see it! - this._startMarker = L.marker([0, 0], { + this._startMarker = L.marker(this._map.getCenter(), { icon: L.divIcon({ className: 'marker-icon rect-start-marker' }), draggable: false, zIndexOffset: -100, @@ -44,7 +44,7 @@ Draw.Rectangle = Draw.extend({ this._layerGroup.addLayer(this._startMarker); // this is the hintmarker on the mouse cursor - this._hintMarker = L.marker([0, 0], { + this._hintMarker = L.marker(this._map.getCenter(), { zIndexOffset: 150, icon: L.divIcon({ className: 'marker-icon cursor-marker' }), }); @@ -52,6 +52,11 @@ Draw.Rectangle = Draw.extend({ this._hintMarker._pmTempLayer = true; this._layerGroup.addLayer(this._hintMarker); + // show the hintmarker if the option is set + if (this.options.cursorMarker) { + L.DomUtil.addClass(this._hintMarker._icon, 'visible'); + } + // add tooltip to hintmarker if (this.options.tooltips) { this._hintMarker @@ -65,14 +70,11 @@ Draw.Rectangle = Draw.extend({ .openTooltip(); } - // show the hintmarker if the option is set if (this.options.cursorMarker) { - L.DomUtil.addClass(this._hintMarker._icon, 'visible'); - // Add two more matching style markers, if cursor marker is rendered this._styleMarkers = []; for (let i = 0; i < 2; i += 1) { - const styleMarker = L.marker([0, 0], { + const styleMarker = L.marker(this._map.getCenter(), { icon: L.divIcon({ className: 'marker-icon rect-style-marker', }), @@ -304,4 +306,7 @@ Draw.Rectangle = Draw.extend({ this.enable(); } }, + setStyle() { + this._layer?.setStyle(this.options.pathOptions); + }, }); diff --git a/src/js/Draw/L.PM.Draw.js b/src/js/Draw/L.PM.Draw.js index b9e8de3a..0679d5a5 100644 --- a/src/js/Draw/L.PM.Draw.js +++ b/src/js/Draw/L.PM.Draw.js @@ -35,7 +35,9 @@ const Draw = L.Class.extend({ }, setOptions(options) { L.Util.setOptions(this, options); + this.setStyle(this.options); }, + setStyle() {}, getOptions() { return this.options; }, diff --git a/src/js/Edit/L.PM.Edit.Circle.js b/src/js/Edit/L.PM.Edit.Circle.js index dbd6d608..9aa8c97c 100644 --- a/src/js/Edit/L.PM.Edit.Circle.js +++ b/src/js/Edit/L.PM.Edit.Circle.js @@ -95,7 +95,7 @@ Edit.Circle = Edit.extend({ } // add markerGroup to map, markerGroup includes regular and middle markers - this._helperLayers = new L.LayerGroup(); + this._helperLayers = new L.FeatureGroup(); this._helperLayers._pmTempLayer = true; this._helperLayers.addTo(map); diff --git a/src/js/Edit/L.PM.Edit.CircleMarker.js b/src/js/Edit/L.PM.Edit.CircleMarker.js index 28dfea66..0eed52a4 100644 --- a/src/js/Edit/L.PM.Edit.CircleMarker.js +++ b/src/js/Edit/L.PM.Edit.CircleMarker.js @@ -153,7 +153,7 @@ Edit.CircleMarker = Edit.extend({ } // add markerGroup to map, markerGroup includes regular and middle markers - this._helperLayers = new L.LayerGroup(); + this._helperLayers = new L.FeatureGroup(); this._helperLayers._pmTempLayer = true; this._helperLayers.addTo(map); diff --git a/src/js/Edit/L.PM.Edit.Line.js b/src/js/Edit/L.PM.Edit.Line.js index e0cb8784..dbc55ed5 100644 --- a/src/js/Edit/L.PM.Edit.Line.js +++ b/src/js/Edit/L.PM.Edit.Line.js @@ -2,7 +2,7 @@ import kinks from '@turf/kinks'; import lineIntersect from '@turf/line-intersect'; import get from 'lodash/get'; import Edit from './L.PM.Edit'; -import { hasValues, removeEmptyCoordRings } from '../helpers'; +import { copyLatLngs, hasValues, removeEmptyCoordRings } from '../helpers'; import MarkerLimits from '../Mixins/MarkerLimits'; @@ -107,7 +107,7 @@ Edit.Line = Edit.extend({ L.DomUtil.removeClass(el, 'leaflet-pm-draggable'); // remove invalid class if layer has self intersection - if (this.hasSelfIntersection()) { + if (!this._map.hasLayer(this._layer) || this.hasSelfIntersection()) { L.DomUtil.removeClass(el, 'leaflet-pm-invalid'); } @@ -145,7 +145,7 @@ Edit.Line = Edit.extend({ } // add markerGroup to map, markerGroup includes regular and middle markers - this._markerGroup = new L.LayerGroup(); + this._markerGroup = new L.FeatureGroup(); this._markerGroup._pmTempLayer = true; // handle coord-rings (outer, inner, etc) @@ -442,8 +442,10 @@ Edit.Line = Edit.extend({ // if self intersection isn't allowed, save the coords upon dragstart // in case we need to reset the layer if (!this.options.allowSelfIntersection) { - const c = this._layer.getLatLngs(); - this._coordsBeforeEdit = JSON.parse(JSON.stringify(c)); + this._coordsBeforeEdit = copyLatLngs( + this._layer, + this._layer.getLatLngs() + ); } // coords of the layer @@ -504,9 +506,7 @@ Edit.Line = Edit.extend({ this._layer.setLatLngs(coords); // re-enable editing so unnecessary markers are removed - // TODO: kind of an ugly workaround maybe do it better? - this.disable(); - this.enable(this.options); + this._initMarkers(); layerRemoved = true; } @@ -668,7 +668,10 @@ Edit.Line = Edit.extend({ // if self intersection isn't allowed, save the coords upon dragstart // in case we need to reset the layer if (!this.options.allowSelfIntersection) { - this._coordsBeforeEdit = this._layer.getLatLngs(); + this._coordsBeforeEdit = copyLatLngs( + this._layer, + this._layer.getLatLngs() + ); } if ( diff --git a/src/js/Edit/L.PM.Edit.Rectangle.js b/src/js/Edit/L.PM.Edit.Rectangle.js index f3254580..b4da0299 100644 --- a/src/js/Edit/L.PM.Edit.Rectangle.js +++ b/src/js/Edit/L.PM.Edit.Rectangle.js @@ -15,7 +15,7 @@ Edit.Rectangle = Edit.Polygon.extend({ } // add markerGroup to map, markerGroup includes regular and middle markers - this._markerGroup = new L.LayerGroup(); + this._markerGroup = new L.FeatureGroup(); this._markerGroup._pmTempLayer = true; map.addLayer(this._markerGroup); diff --git a/src/js/Edit/L.PM.Edit.Text.js b/src/js/Edit/L.PM.Edit.Text.js index 3acb72f8..bfce1c6c 100644 --- a/src/js/Edit/L.PM.Edit.Text.js +++ b/src/js/Edit/L.PM.Edit.Text.js @@ -151,12 +151,21 @@ Edit.Text = Edit.extend({ }, _focusChange(e = {}) { + const focusAlreadySet = this._hasFocus; this._hasFocus = e.type === 'focus'; - - if (this._hasFocus) { - this._applyFocus(); - } else { - this._removeFocus(); + if (!focusAlreadySet !== !this._hasFocus) { + if (this._hasFocus) { + this._applyFocus(); + this._focusText = this.getText(); + this._fireTextFocus(); + } else { + this._removeFocus(); + this._fireTextBlur(); + if (this._focusText !== this.getText()) { + this._fireEdit(); + this._layerEdited = true; + } + } } }, _applyFocus() { diff --git a/src/js/L.PM.Map.js b/src/js/L.PM.Map.js index 888ce734..eaf71ef1 100644 --- a/src/js/L.PM.Map.js +++ b/src/js/L.PM.Map.js @@ -104,7 +104,7 @@ const Map = L.Class.extend({ let reenableCircleMarker = false; if ( this.map.pm.Draw.CircleMarker.enabled() && - this.map.pm.Draw.CircleMarker.options.editable !== options.editable + !!this.map.pm.Draw.CircleMarker.options.editable !== !!options.editable ) { this.map.pm.Draw.CircleMarker.disable(); reenableCircleMarker = true; @@ -125,11 +125,13 @@ const Map = L.Class.extend({ layer.pm.setOptions(options); }); - // apply the options (actually trigger the functionality) - this.applyGlobalOptions(); + this.map.fire('pm:globaloptionschanged'); // store options this.globalOptions = options; + + // apply the options (actually trigger the functionality) + this.applyGlobalOptions(); }, applyGlobalOptions() { const layers = L.PM.Utils.findLayers(this.map); diff --git a/src/js/L.PM.Utils.js b/src/js/L.PM.Utils.js index 6522532c..0a9ea6d2 100644 --- a/src/js/L.PM.Utils.js +++ b/src/js/L.PM.Utils.js @@ -111,7 +111,7 @@ const Utils = { }, createGeodesicPolygon, getTranslation, - findDeepCoordIndex(arr, latlng) { + findDeepCoordIndex(arr, latlng, exact = true) { // find latlng in arr and return its location as path // thanks for the function, Felix Heck let result; @@ -119,7 +119,12 @@ const Utils = { const run = (path) => (v, i) => { const iRes = path.concat(i); - if (v.lat && v.lat === latlng.lat && v.lng === latlng.lng) { + if (exact) { + if (v.lat && v.lat === latlng.lat && v.lng === latlng.lng) { + result = iRes; + return true; + } + } else if (v.lat && L.latLng(v).equals(latlng)) { result = iRes; return true; } diff --git a/src/js/L.PM.js b/src/js/L.PM.js index 081480a0..d5c7002a 100644 --- a/src/js/L.PM.js +++ b/src/js/L.PM.js @@ -69,6 +69,10 @@ L.PM = L.PM || { } else if (!this.options.pmIgnore) { this.pm = new L.PM.Map(this); } + + if (this.pm) { + this.pm.setGlobalOptions({}); + } } L.Map.addInitHook(initMap); @@ -238,5 +242,34 @@ L.PM = L.PM || { }, }; +if (L.version === '1.7.1') { + // Canvas Mode: After dragging the map the target layer can't be dragged anymore until it is clicked + // https://github.com/Leaflet/Leaflet/issues/7775 a fix is already merged for the Leaflet 1.8.0 version + L.Canvas.include({ + _onClick(e) { + const point = this._map.mouseEventToLayerPoint(e); + let layer; + let clickedLayer; + + for (let order = this._drawFirst; order; order = order.next) { + layer = order.layer; + if (layer.options.interactive && layer._containsPoint(point)) { + // changing e.type !== 'preclick' to e.type === 'preclick' fix the issue + if ( + !(e.type === 'click' || e.type === 'preclick') || + !this._map._draggableMoved(layer) + ) { + clickedLayer = layer; + } + } + } + if (clickedLayer) { + L.DomEvent.fakeStop(e); + this._fireEvent([clickedLayer], e); + } + }, + }); +} + // initialize leaflet-geoman L.PM.initialize(); diff --git a/src/js/Mixins/Dragging.js b/src/js/Mixins/Dragging.js index 196d330c..45a20c1a 100644 --- a/src/js/Mixins/Dragging.js +++ b/src/js/Mixins/Dragging.js @@ -329,6 +329,7 @@ const DragMixin = { // fire edit this._fireEdit(); + this._layerEdited = true; }, 10); return true; diff --git a/src/js/Mixins/Events.js b/src/js/Mixins/Events.js index f69b1965..3fca04fa 100644 --- a/src/js/Mixins/Events.js +++ b/src/js/Mixins/Events.js @@ -373,6 +373,33 @@ const EventMixin = { ); }, + // Fired when text layer focused + _fireTextFocus(source = 'Edit', customPayload = {}) { + this.__fire( + this._layer, + 'pm:textfocus', + { + layer: this._layer, + shape: this.getShape(), + }, + source, + customPayload + ); + }, + // Fired when text layer blurred + _fireTextBlur(source = 'Edit', customPayload = {}) { + this.__fire( + this._layer, + 'pm:textblur', + { + layer: this._layer, + shape: this.getShape(), + }, + source, + customPayload + ); + }, + // Snapping Events // Fired during a marker move/drag and other layers are existing _fireSnapDrag(fireLayer, eventInfo, source = 'Snapping', customPayload = {}) { diff --git a/src/js/Mixins/MarkerLimits.js b/src/js/Mixins/MarkerLimits.js index ea9b41a0..04305e23 100644 --- a/src/js/Mixins/MarkerLimits.js +++ b/src/js/Mixins/MarkerLimits.js @@ -1,8 +1,6 @@ const MarkerLimits = { filterMarkerGroup() { - // don't do it if the option is disabled - - // define cache + // define cache of markers this.markerCache = []; this.createCache(); @@ -12,6 +10,14 @@ const MarkerLimits = { // apply filter for the first time this.applyLimitFilters({}); + if (!this.throttledApplyLimitFilters) { + this.throttledApplyLimitFilters = L.Util.throttle( + this.applyLimitFilters, + 100, + this + ); + } + // remove events when edit mode is disabled this._layer.on('pm:disable', this._removeMarkerLimitEvents, this); @@ -21,11 +27,11 @@ const MarkerLimits = { // The reason is that syncing this cache with a removed marker was impossible to do this._layer.on('pm:vertexremoved', this._initMarkers, this); - this._map.on('mousemove', this.applyLimitFilters, this); + this._map.on('mousemove', this.throttledApplyLimitFilters, this); } }, _removeMarkerLimitEvents() { - this._map.off('mousemove', this.applyLimitFilters, this); + this._map.off('mousemove', this.throttledApplyLimitFilters, this); this._layer.off('pm:edit', this.createCache, this); this._layer.off('pm:disable', this._removeMarkerLimitEvents, this); this._layer.off('pm:vertexremoved', this._initMarkers, this); @@ -59,6 +65,10 @@ const MarkerLimits = { const markers = [...this.markerCache]; const limit = this.options.limitMarkersToCount; + if (limit === -1) { + return markers; + } + // sort markers by distance to cursor markers.sort((l, t) => { const distanceA = l._latlng.distanceTo(latlng); diff --git a/src/js/Mixins/Modes/Mode.Drag.js b/src/js/Mixins/Modes/Mode.Drag.js index 470d70dd..5e9d7c2b 100644 --- a/src/js/Mixins/Modes/Mode.Drag.js +++ b/src/js/Mixins/Modes/Mode.Drag.js @@ -19,8 +19,8 @@ const GlobalDragMode = { } // add map handler - this.map.on('layeradd', this.throttledReInitDrag, this); this.map.on('layeradd', this._layerAddedDrag, this); + this.map.on('layeradd', this.throttledReInitDrag, this); // toogle the button in the toolbar if this is called programatically this.Toolbar.toggleButton('dragMode', this.globalDragModeEnabled()); @@ -37,6 +37,7 @@ const GlobalDragMode = { }); // remove map handler + this.map.off('layeradd', this._layerAddedDrag, this); this.map.off('layeradd', this.throttledReInitDrag, this); // toogle the button in the toolbar if this is called programatically diff --git a/src/js/Mixins/Modes/Mode.Edit.js b/src/js/Mixins/Modes/Mode.Edit.js index 582d1c4e..4d01a971 100644 --- a/src/js/Mixins/Modes/Mode.Edit.js +++ b/src/js/Mixins/Modes/Mode.Edit.js @@ -1,7 +1,10 @@ // this mixin adds a global edit mode to the map const GlobalEditMode = { _globalEditModeEnabled: false, - enableGlobalEditMode(options) { + enableGlobalEditMode(o) { + const options = { + ...o, + }; // set status this._globalEditModeEnabled = true; diff --git a/src/js/Mixins/Rotating.js b/src/js/Mixins/Rotating.js index b0b29529..b6a21966 100644 --- a/src/js/Mixins/Rotating.js +++ b/src/js/Mixins/Rotating.js @@ -167,6 +167,7 @@ const RotateMixin = { }); // we connect the temp polygon (that will be enabled for rotation) with the current layer, so that we can rotate the current layer too this._rotatePoly.pm._rotationLayer = this._layer; + this._rotatePoly._pmTempLayer = true; this._rotatePoly.pm.enable(); // store the original latlngs @@ -206,10 +207,10 @@ const RotateMixin = { return this._rotateEnabled; }, // angle is clockwise (0-360) - rotateLayer(angle) { + rotateLayer(degrees) { const oldAngle = this.getAngle(); const oldLatLngs = this._layer.getLatLngs(); - const rads = angle * (Math.PI / 180); + const rads = degrees * (Math.PI / 180); this._layer.setLatLngs( this._rotateLayer( rads, @@ -221,7 +222,7 @@ const RotateMixin = { ); // store the new latlngs this._rotateOrgLatLng = L.polygon(this._layer.getLatLngs()).getLatLngs(); - this._setAngle(this.getAngle() + angle); + this._setAngle(this.getAngle() + degrees); if ( this.rotateEnabled() && this._rotatePoly && @@ -245,18 +246,27 @@ const RotateMixin = { this._startAngle = oldAngle; this._fireRotation(this._layer, angleDiff, oldLatLngs, this._layer); - this._fireRotation(this._map, angleDiff, oldLatLngs, this._layer); + this._fireRotation( + this._map || this._layer._map, + angleDiff, + oldLatLngs, + this._layer + ); delete this._startAngle; this._fireChange(this._layer.getLatLngs(), 'Rotation'); }, - rotateLayerToAngle(angle) { - const newAnlge = angle - this.getAngle(); + rotateLayerToAngle(degrees) { + const newAnlge = degrees - this.getAngle(); this.rotateLayer(newAnlge); }, // angle is clockwise (0-360) getAngle() { return this._angle || 0; }, + // angle is clockwise (0-360) + setInitAngle(degrees) { + this._setAngle(degrees); + }, }; export default RotateMixin; diff --git a/src/js/Mixins/Snapping.js b/src/js/Mixins/Snapping.js index 2d13dbdd..aa8011fb 100644 --- a/src/js/Mixins/Snapping.js +++ b/src/js/Mixins/Snapping.js @@ -32,7 +32,12 @@ const SnapMixin = { marker.on('dragend', this._cleanupSnapping, this); }); }, - _cleanupSnapping() { + _cleanupSnapping(e) { + if (e) { + // reset snap flag of the dragged helper-marker + const marker = e.target; + marker._snapped = false; + } // delete it, we need to refresh this with each start of a drag because // meanwhile, new layers could've been added to the map delete this._snapList; @@ -164,6 +169,7 @@ const SnapMixin = { this._unsnap(eventInfo); marker._snapped = false; + marker._snapInfo = undefined; // and fire unsnap event this._fireUnsnap(eventInfo.marker, eventInfo); @@ -259,6 +265,9 @@ const SnapMixin = { this._snapList.splice(index, 1); }, _calcClosestLayer(latlng, layers) { + return this._calcClosestLayers(latlng, layers, 1)[0]; + }, + _calcClosestLayers(latlng, layers, amount = 1) { // the closest polygon to our dragged marker latlng let closestLayers = []; let closestLayer = {}; @@ -273,15 +282,22 @@ const SnapMixin = { const results = this._calcLayerDistances(latlng, layer); results.distance = Math.floor(results.distance); - if (this.debugIndicatorLines[index]) { + if (this.debugIndicatorLines) { + if (!this.debugIndicatorLines[index]) { + const debugLine = L.polyline([], { color: 'red', pmIgnore: true }); + debugLine._pmTempLayer = true; + this.debugIndicatorLines[index] = debugLine; + } + // show indicator lines, it's for debugging this.debugIndicatorLines[index].setLatLngs([latlng, results.latlng]); } // save the info if it doesn't exist or if the distance is smaller than the previous one if ( - closestLayer.distance === undefined || - results.distance <= closestLayer.distance + amount === 1 && + (closestLayer.distance === undefined || + results.distance <= closestLayer.distance) ) { if (results.distance < closestLayer.distance) { closestLayers = []; @@ -289,12 +305,29 @@ const SnapMixin = { closestLayer = results; closestLayer.layer = layer; closestLayers.push(closestLayer); + } else if (amount !== 1) { + closestLayer = {}; + closestLayer = results; + closestLayer.layer = layer; + closestLayers.push(closestLayer); } }); + if (amount !== 1) { + // sort the layers by distance + closestLayers = closestLayers.sort((a, b) => a.distance - b.distance); + } + + if (amount === -1) { + amount = closestLayers.length; + } // return the closest layer and it's data // if there is no closest layer, return an empty object - return this._getClosestLayerByPriority(closestLayers); + const result = this._getClosestLayerByPriority(closestLayers, amount); + if (L.Util.isArray(result)) { + return result; + } + return [result]; }, _calcLayerDistances(latlng, layer) { const map = this._map; @@ -310,20 +343,23 @@ const SnapMixin = { const P = latlng; // the coords of the layer - const latlngs = isMarker ? layer.getLatLng() : layer.getLatLngs(); if (isMarker) { // return the info for the marker, no more calculations needed + const latlngs = layer.getLatLng(); return { latlng: { ...latlngs }, distance: this._getDistance(map, latlngs, P), }; } + return this._calcLatLngDistances(P, layer.getLatLngs(), map, isPolygon); + }, + _calcLatLngDistances(latlng, latlngs, map, closedShape = false) { // the closest coord of the layer let closestCoord; - // the shortest distance from P to closestCoord + // the shortest distance from latlng to closestCoord let shortestDistance; // the closest segment (line between two points) of the layer @@ -342,7 +378,7 @@ const SnapMixin = { let nextIndex; // and the next coord (B) as points - if (isPolygon) { + if (closedShape) { nextIndex = index + 1 === coords.length ? 0 : index + 1; } else { nextIndex = index + 1 === coords.length ? undefined : index + 1; @@ -350,8 +386,8 @@ const SnapMixin = { const B = coords[nextIndex]; if (B) { - // calc the distance between P and AB-segment - const distance = this._getDistanceToSegment(map, P, A, B); + // calc the distance between latlng and AB-segment + const distance = this._getDistanceToSegment(map, latlng, A, B); // is the distance shorter than the previous one? Save it and the segment if (shortestDistance === undefined || distance < shortestDistance) { @@ -361,7 +397,7 @@ const SnapMixin = { } } else { // Only snap on the coords - const distancePoint = this._getDistance(map, P, coord); + const distancePoint = this._getDistance(map, latlng, coord); if ( shortestDistance === undefined || @@ -377,7 +413,7 @@ const SnapMixin = { loopThroughCoords(latlngs); if (this.options.snapSegment) { - // now, take the closest segment (closestSegment) and calc the closest point to P on it. + // now, take the closest segment (closestSegment) and calc the closest point to latlng on it. const C = this._getClosestPointOnSegment( map, latlng, @@ -399,7 +435,7 @@ const SnapMixin = { distance: shortestDistance, }; }, - _getClosestLayerByPriority(layers) { + _getClosestLayerByPriority(layers, amount = 1) { // sort the layers by creation, so it is snapping to the oldest layer from the same shape layers = layers.sort((a, b) => a._leaflet_id - b._leaflet_id); @@ -425,7 +461,10 @@ const SnapMixin = { // sort layers by priority layers.sort(prioritiseSort('instanceofShape', prioOrder)); - return layers[0] || {}; + if (amount === 1) { + return layers[0] || {}; + } + return layers.slice(0, amount); }, // we got the point we want to snap to (C), but we need to check if a coord of the polygon // receives priority over C as the snapping point. Let's check this here diff --git a/src/js/Toolbar/L.Controls.js b/src/js/Toolbar/L.Controls.js index 28b82d86..4f0d11c0 100644 --- a/src/js/Toolbar/L.Controls.js +++ b/src/js/Toolbar/L.Controls.js @@ -5,11 +5,12 @@ const PMButton = L.Control.extend({ includes: [EventMixin], options: { position: 'topleft', + disableByOtherButtons: true, }, // TODO: clean up variable names like _button should be _options and that domNodeVariable stuff initialize(options) { // replaced setOptions with this because classNames returned undefined 🤔 - this._button = { ...this.options, ...options }; + this._button = L.Util.extend({}, this.options, options); }, onAdd(map) { this._map = map; diff --git a/src/js/Toolbar/L.PM.Toolbar.js b/src/js/Toolbar/L.PM.Toolbar.js index d0b03914..965cbb99 100644 --- a/src/js/Toolbar/L.PM.Toolbar.js +++ b/src/js/Toolbar/L.PM.Toolbar.js @@ -161,18 +161,14 @@ const Toolbar = L.Class.extend({ // we can't have two active modes because of possible event conflicts // so, we trigger a click on all currently active (toggled) buttons - // the options toolbar should not be disabled during the different modes - // TODO: probably need to abstract this a bit so different options are automatically - // disabled for different modes, like pinning for circles - const exceptOptionButtons = ['snappingOption']; - for (const name in this.buttons) { + const button = this.buttons[name]; if ( - !exceptOptionButtons.includes(name) && - this.buttons[name] !== exceptThisButton && - this.buttons[name].toggled() + button._button.disableByOtherButtons && + button !== exceptThisButton && + button.toggled() ) { - this.buttons[name]._triggerClick(); + button._triggerClick(); } } }, @@ -548,7 +544,8 @@ const Toolbar = L.Class.extend({ afterClick: options.afterClick, doToggle: options.toggle, toggleStatus: false, - disableOtherButtons: true, + disableOtherButtons: options.disableOtherButtons ?? true, + disableByOtherButtons: options.disableByOtherButtons ?? true, cssToggle: options.toggle, position: this.options.position, actions: options.actions || [], diff --git a/src/js/helpers/turfHelper.js b/src/js/helpers/turfHelper.js index abe85d12..66583510 100644 --- a/src/js/helpers/turfHelper.js +++ b/src/js/helpers/turfHelper.js @@ -12,10 +12,17 @@ export function getGeometry(geojson) { } export function getCoords(geojson) { - return geojson.geometry.coordinates; + if (geojson && geojson.geometry && geojson.geometry.coordinates) + return geojson.geometry.coordinates; + return geojson; } -export function turfPoint(coords) { +export function turfPoint(coords, precision = -1) { + if (precision > -1) { + coords[0] = L.Util.formatNum(coords[0], precision); + coords[1] = L.Util.formatNum(coords[1], precision); + } + return feature({ type: 'Point', coordinates: coords }); } @@ -100,3 +107,18 @@ export function groupToMultiLineString(group) { }); return turfMultiLineString(coords); } + +export function convertToLatLng(coords) { + const lnglat = getCoords(coords); + return L.latLng(lnglat[1], lnglat[0]); +} + +export function convertArrayToLatLngs(arr) { + const latlngs = []; + if (arr.features) { + arr.features.forEach((geojson) => { + latlngs.push(convertToLatLng(geojson)); + }); + } + return latlngs; +}