diff --git a/README.md b/README.md index d06465e4..06c8d5f4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

Leaflet Plugin For Creating And Editing Geometry Layers
Draw, Edit, Drag, Cut, Snap and Pin Layers
- Supports Markers, CircleMarkers, Polylines, Polygons, Circles, Rectangles, LayerGroups, GeoJSON and MultiPolygons + Supports Markers, CircleMarkers, Polylines, Polygons, Circles, Rectangles, ImageOverlays, LayerGroups, GeoJSON and MultiPolygons

@@ -111,12 +111,25 @@ their options when creating them. Example: 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); +``` +If `Opt-In` (look below) is `true`, a layers `pmIgnore` property has to be set to `false` to get initiated. + + ##### Opt-In If you want to use leaflet-geoman as opt-in, call the following function right after importing: ```js -L.PM.initialize({ optIn: true }); +L.PM.setOptIn(true); +``` + +And to disable it: +```js +L.PM.setOptIn(false); ``` All layers will be ignored by leaflet-geoman, unless you specify `pmIgnore: false` on a layer: @@ -195,18 +208,18 @@ map.pm.Draw.getShapes(); The following methods are available on `map.pm`: -| Method | Returns | Description | -| :---------------------------- | :-------- | :----------------------------------------------------------------------- | -| enableDraw(`shape`,`options`) | - | Enable Drawing Mode with the passed shape. | -| disableDraw(`shape`) | - | Disable Drawing Mode. The passed shape is optional. | -| Draw.getShapes() | `Array` | Array of available shapes. | -| Draw.getActiveShape() | `String` | Returns the active shape. | -| globalDrawModeEnabled() | `Boolean` | Returns `true` if global draw mode is enabled. `false` when disabled. | -| setPathOptions(`options`) | - | Customize the style of the drawn layer. | -| setGlobalOptions(`options`) | - | Set drawing options. | -| getGlobalOptions() | `Object` | Returns the global options. | -| getGeomanLayers() | `Array` | Returns all Geoman layers on the map. | -| getGeomanDrawLayers() | `Array` | Returns all drawn Geoman layers on the map. | +| Method | Returns | Description | +| :---------------------------- | :-------- | :---------------------------------------------------------------------------------------------- | +| enableDraw(`shape`,`options`) | - | Enable Drawing Mode with the passed shape. | +| disableDraw(`shape`) | - | Disable Drawing Mode. The passed shape is optional. | +| Draw.getShapes() | `Array` | Array of available shapes. | +| Draw.getActiveShape() | `String` | Returns the active shape. | +| globalDrawModeEnabled() | `Boolean` | Returns `true` if global draw mode is enabled. `false` when disabled. | +| setPathOptions(`options`) | - | Customize the style of the drawn layer. | +| setGlobalOptions(`options`) | - | Set drawing options. | +| getGlobalOptions() | `Object` | Returns the global options. | +| getGeomanLayers(`Boolean`) | `Array` | Returns all Geoman layers on the map as array. Pass `true` to get a L.FeatureGroup. | +| getGeomanDrawLayers(`Boolean`)| `Array` | Returns all drawn Geoman layers on the map as array. Pass `true` to get a L.FeatureGroup. | See the available options in the table below. @@ -222,8 +235,22 @@ See the available options in the table below. | cursorMarker | `true` | show a marker at the cursor | | finishOn | `null` | leaflet layer event to finish the drawn shape, like `'dblclick'`. [Here's a list](http://leafletjs.com/reference-1.2.0.html#interactive-layer-click). | | markerStyle | `{ draggable: true }` | [leaflet marker options](https://leafletjs.com/reference-1.4.0.html#marker-icon) (only for drawing markers). | +| hideMiddleMarkers | `false` | hide the middle Markers in edit mode from Polyline and Polygon. | +| minRadiusCircle | `null` | set the min radius of a `Circle`. | +| maxRadiusCircle | `null` | set the max radius of a `Circle`. | +| minRadiusCircleMarker | `null` | set the min radius of a `CircleMarker` when editable is active. | +| maxRadiusCircleMarker | `null` | set the max radius of a `CircleMarker` when editable is active. | | editable | `false` | makes a `CircleMarker` editable like a `Circle` | -| hideMiddleMarkers | `false` | hide the middle Markers in edit mode from Polyline and Polygon. | +| markerEditable | `true` | Markers and CircleMarkers are editable during the draw-session (you can drag them around immediately after drawing them) | +| continueDrawing | `false` / `true` | Draw-Mode stays enabled after finishing a layer to immediately draw the next layer. Defaults to `true` for Markers and CircleMarkers and `false` for all other layers. | | + + + +This options are only available for the global options: + +| Option | Default | Description | +| :-------------------- | :------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------- | +| layerGroup | `map` | add the created layers to a layergroup instead to the map. | You can listen to map events to hook into the drawing procedure like this: @@ -328,6 +355,7 @@ The following events are available on a layer instance: | pm:vertexadded | `e` | Fired when a vertex is added | `layer`, `indexPath`, `latlng`, `marker`, `shape` | | pm:vertexremoved | `e` | Fired when a vertex is removed | `layer`, `indexPath`, `marker`, `shape` | | pm:markerdragstart | `e` | Fired when dragging of a marker which corresponds to a vertex starts | `layer`, `indexPath`, `markerEvent`, `shape` | +| pm:markerdrag | `e` | Fired when dragging a vertex-marker | `layer`, `indexPath`, `markerEvent`, `shape` | | pm:markerdragend | `e` | Fired when dragging of a vertex-marker ends | `layer`, `indexPath`, `markerEvent`, `shape` | | pm:snapdrag | `e` | Fired during a marker move/drag. Payload includes info about involved layers and snapping calculation| `shape`, `distance`, `layer` = `workingLayer`, `marker`, `layerInteractedWith`, `segment`, `snapLatLng` | | pm:snap | `e` | Fired when a vertex-marker is snapped to another vertex. Also fired on the marker itself. | `shape`, `distance`, `layer` = `workingLayer`, `marker`, `layerInteractedWith`, `segment`, `snapLatLng` | @@ -508,19 +536,20 @@ map.pm.setGlobalOptions({ pinning: true, limitMarkersToCount: 15, limitMarkersCo The following options are available globally and apply when going into global edit mode. -| Option | Default | Description | -| :------------------------ | :------ | :-------------------------------------------------------------------------------------------------------- | -| snappable | `true` | Enable snapping to other layers vertices for precision drawing. Can be disabled by holding the `ALT` key. | -| snapDistance | `20` | The distance to another vertex when a snap should happen. | -| pinning | `false` | Pin shared vertices/markers together during edit ⭐. [Details](#pinning) | -| allowSelfIntersection | `true` | Allow/Disallow self-intersections on polygons and polylines. | -| preventMarkerRemoval | `false` | Disable the removal of markers/vertexes via right click. | -| limitMarkersToCount | `-1` | Shows only `n` markers per layer closest to the cursor. Use `-1` for no limit | -| limitMarkersCountGlobally | `false` | Activates `limitMarkersToCount` across layers on the entire map, not just per layer ⭐ | -| limitMarkersToZoom | `-1` | Shows markers when under the given zoom level ⭐ | -| limitMarkersToViewport | `false` | Shows only markers in the viewport ⭐ | -| limitMarkersToClick | `false` | Shows markers only after the layer was clicked ⭐ | -| editable | `false` | Makes a `CircleMarker` editable like a `Circle` | +| Option | Default | Description | +| :------------------------ | :------ | :-------------------------------------------------------------------------------------------------------------------------- | +| snappable | `true` | Enable snapping to other layers vertices for precision drawing. Can be disabled by holding the `ALT` key. | +| snapDistance | `20` | The distance to another vertex when a snap should happen. | +| pinning | `false` | Pin shared vertices/markers together during edit ⭐. [Details](#pinning) | +| allowSelfIntersection | `true` | Allow/Disallow self-intersections on polygons and polylines. | +| preventMarkerRemoval | `false` | Disable the removal of markers/vertexes via right click. | +| limitMarkersToCount | `-1` | Shows only `n` markers per layer closest to the cursor. Use `-1` for no limit | +| limitMarkersCountGlobally | `false` | Activates `limitMarkersToCount` across layers on the entire map, not just per layer ⭐ | +| limitMarkersToZoom | `-1` | Shows markers when under the given zoom level ⭐ | +| limitMarkersToViewport | `false` | Shows only markers in the viewport ⭐ | +| limitMarkersToClick | `false` | Shows markers only after the layer was clicked ⭐ | +| editable | `false` | Makes a `CircleMarker` editable like a `Circle` | +| snappingOrder | `Array` | Prioritize the order of snapping. Default: `['Marker','CircleMarker','Circle','Line','Polygon','Rectangle']` | Some details about a few more powerful options: @@ -550,7 +579,7 @@ Change the language of user-facing copy in leaflet-geoman map.pm.setLang('de'); ``` -Currently available languages are `en`, `de`, `it`, `ru`, `ro`, `es`, `fr`, `pt_br`, `id`, `zh`, `nl`, `el`, `pl`, `sv` and `hu`. +Currently available languages are `en`, `de`, `it`, `ru`, `ro`, `es`, `fr`, `pt_br`, `id`, `zh`, `zh_tw`, `nl`, `el`, `pl`, `sv`, `da` and `hu`. 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. @@ -701,14 +730,15 @@ map.pm.Toolbar.createCustomControl(options) | Option | Default | Description | | :------------ | :---------- | :----------------------------------------------------------------------------------------------- | -| name | Required | Name of the control | -| block | '' | block of the control. `draw`, `edit`, `options`⭐, `custom` | -| 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 | +| name | Required | Name of the control | +| block | '' | block of the control. `draw`, `edit`, `options`⭐, `custom` | +| 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 | **Inherit from an Existing Control** @@ -764,10 +794,20 @@ The following methods are available on `map.pm.Toolbar`: | Method | Returns | Description | | :------------------------------------------ | :-------- | :------------------------------------------------------------------------------------------------------------ | | createCustomControl(`options`) | - | To add a custom Control to the Toolbar. | -| copyDrawControl(`instance`, `options`) | `Object` | Creates a copy of a draw Control. Returns the `drawInstance` and the `control`. | -| changeActionsOfControl(`name`, `actions`) | - | Change the actions of an existing button. | +| copyDrawControl(`instance`, `options`) | `Object` | Creates a copy of a draw Control. Returns the `drawInstance` and the `control`. | +| changeActionsOfControl(`name`, `actions`) | - | Change the actions of an existing button. | | changeControlOrder(`shapes`) | - | Change the order of the controls in the Toolbar. You can pass all shapes and `Edit`, `Drag`, `Removal`, `Cut` | | getControlOrder() | `Array` | Get the current order of the controls. | +| setButtonDisabled(`name`, `Boolean`) | - | Enable / disable a button. | + +The following events are available on a map instance: + +| Event | Params | Description | Output | +| :------------- | :----- | :---------------------------------------- | :--------------------------------------------------- | +| pm:buttonclick | `e` | Fired when a Toolbar button is clicked | `btnName`, `button` | +| pm:actionclick | `e` | Fired when a Toolbar action is clicked | `text`, `action`, `btnName`, `button` | + + ### Feature Requests diff --git a/cypress/fixtures/FeatureCollectionEventFire.json b/cypress/fixtures/FeatureCollectionEventFire.json new file mode 100644 index 00000000..bdf678de --- /dev/null +++ b/cypress/fixtures/FeatureCollectionEventFire.json @@ -0,0 +1,29 @@ +{ + "type":"FeatureCollection", + "features":[ + { + "type":"Feature", + "geometry":{ + "type":"MultiPoint", + "coordinates":[ + [ + 32.83604819, + 40.0169319 + ] + ] + } + }, + { + "type":"Feature", + "geometry":{ + "type":"MultiPoint", + "coordinates":[ + [ + 32.8298979, + 40.02567418 + ] + ] + } + } + ] +} diff --git a/cypress/integration/circle.spec.js b/cypress/integration/circle.spec.js index b5548fef..788d3b42 100644 --- a/cypress/integration/circle.spec.js +++ b/cypress/integration/circle.spec.js @@ -84,4 +84,157 @@ describe('Draw Circle', () => { }); }); }); + + it('enable continueDrawing', () => { + cy.window().then(({ map }) => { + map.pm.setGlobalOptions({continueDrawing: true}); + }); + + cy.toolbarButton('circle') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + // draw first circle + cy.get(mapSelector) + .click(200, 200) + .click(250, 250); + + // draw with continueDrawing: ture the second circle + cy.get(mapSelector) + .click(300, 200) + .click(350, 250); + + cy.toolbarButton('edit').click(); + cy.hasVertexMarkers(4); + }); + + it('set max radius of circle', () => { + let handFinish = false; + + cy.toolbarButton('circle') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.window().then(({ map, L }) => { + L.marker(map.getCenter()).addTo(map); + map.pm.setGlobalOptions({ + minRadiusCircle: 500, + maxRadiusCircle: 1500, + }); + cy.get(mapSelector) + .click(250,200) + .click(620,190) + .then(() => { + map.eachLayer(layer => { + if (layer instanceof L.Circle) { + expect(layer.getRadius()).to.equal(1500); + } + }); + }); + }); + + cy.toolbarButton('edit') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.window().then(({ Hand, map, L }) => { + const handMarker = new Hand({ + timing: 'frame', + onStop: ()=>{ + map.eachLayer(layer => { + if (layer instanceof L.Circle) { + expect(true).to.equal(layer.getRadius() < 1500); + } + }); + const handMarker2 = new Hand({ + timing: 'frame', + onStop: () => { + handFinish = true; + map.eachLayer(layer => { + if (layer instanceof L.Circle) { + expect(true).to.equal(layer.getRadius() >= 1495 && layer.getRadius() < 1505); + } + }); + } + }); + const toucherMarker2 = handMarker2.growFinger('mouse'); + toucherMarker2.wait(100).moveTo(317,198, 100).down().wait(500).moveTo(500,198, 600).up().wait(100) + } + }); + const toucherMarker = handMarker.growFinger('mouse'); + toucherMarker.wait(100).moveTo(379,198, 100).down().wait(500).moveTo(317,198, 400).up().wait(100) + + // wait until hand is finished + cy.waitUntil(() => cy.window().then(() => handFinish)).then( ()=> { + expect(handFinish).to.equal(true); + }); + }); + + }); + it('set min radius of circle', () => { + let handFinish = false; + + cy.toolbarButton('circle') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.window().then(({ map, L }) => { + L.marker(map.getCenter()).addTo(map); + map.pm.setGlobalOptions({ + minRadiusCircle: 1500, + maxRadiusCircle: 3000, + }); + cy.get(mapSelector) + .click(250,200) + .click(300,190) + .then(() => { + map.eachLayer(layer => { + if (layer instanceof L.Circle) { + expect(layer.getRadius()).to.equal(1500); + } + }); + }); + }); + + cy.toolbarButton('edit') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.window().then(({ Hand, map, L }) => { + const handMarker = new Hand({ + timing: 'frame', + onStop: ()=>{ + map.eachLayer(layer => { + if (layer instanceof L.Circle) { + expect(true).to.equal(layer.getRadius() > 1500) + } + }); + const handMarker2 = new Hand({ + timing: 'frame', + onStop: () =>{ + handFinish = true; + map.eachLayer(layer => { + if (layer instanceof L.Circle) { + expect(true).to.equal(layer.getRadius() >= 1495 && layer.getRadius() < 1505); + } + }); + } + }); + const toucherMarker2 = handMarker2.growFinger('mouse'); + toucherMarker2.wait(200).moveTo(490,198, 100).down().wait(500).moveTo(317,198, 600).up().wait(100) + } + }); + const toucherMarker = handMarker.growFinger('mouse'); + toucherMarker.wait(100).moveTo(379,198, 100).down().wait(500).moveTo(500,198, 400).up().wait(100) + // wait until hand is finished + cy.waitUntil(() => cy.window().then(() => handFinish)).then( ()=> { + expect(handFinish).to.equal(true); + }); + }); + }); }); diff --git a/cypress/integration/circlemarker.spec.js b/cypress/integration/circlemarker.spec.js index c7ee36c5..cd681901 100644 --- a/cypress/integration/circlemarker.spec.js +++ b/cypress/integration/circlemarker.spec.js @@ -44,55 +44,53 @@ describe('Draw Circle Marker', () => { createMarkers(); }); - it('handles 6k circle markers in under 1 sec', () => { + cy.toolbarButton('circle-marker').click(); - cy.toolbarButton('circle-marker') - .click() - - cy.get(mapSelector) - .click(150, 250) + cy.get(mapSelector).click(150, 250); cy.testLayerAdditionPerformance(); }); - - it('correctly disables drag', () => { - createMarkers(); cy.window().then(({ map, L }) => { - map.eachLayer((layer) => { + map.eachLayer(layer => { if (layer instanceof L.CircleMarker) { - assert.isFalse(L.DomUtil.hasClass(layer._path, 'leaflet-pm-draggable'), 'not draggable') + assert.isFalse( + L.DomUtil.hasClass(layer._path, 'leaflet-pm-draggable'), + 'not draggable' + ); } - }) + }); }); - cy.toolbarButton('edit') - .click() + cy.toolbarButton('edit').click(); cy.window().then(({ map, L }) => { - map.eachLayer((layer) => { + map.eachLayer(layer => { if (layer instanceof L.CircleMarker) { - assert.isTrue(L.DomUtil.hasClass(layer._path, 'leaflet-pm-draggable'), 'draggable') + assert.isTrue( + L.DomUtil.hasClass(layer._path, 'leaflet-pm-draggable'), + 'draggable' + ); } - }) + }); }); - cy.toolbarButton('edit') - .click() + cy.toolbarButton('edit').click(); cy.window().then(({ map, L }) => { - map.eachLayer((layer) => { + map.eachLayer(layer => { if (layer instanceof L.CircleMarker) { - assert.isFalse(L.DomUtil.hasClass(layer._path, 'leaflet-pm-draggable'), 'not draggable') + assert.isFalse( + L.DomUtil.hasClass(layer._path, 'leaflet-pm-draggable'), + 'not draggable' + ); } - }) + }); }); - - }); it('deletes all circle markers', () => { @@ -116,10 +114,9 @@ describe('Draw Circle Marker', () => { cy.hasCircleLayers(0); }); - it('draw a CircleMarker like a Circle', () => { - cy.window().then(({ map}) => { - map.pm.setGlobalOptions({editable: true}); + cy.window().then(({ map }) => { + map.pm.setGlobalOptions({ editable: true, continueDrawing: false }); }); cy.toolbarButton('circle-marker') @@ -133,7 +130,6 @@ describe('Draw Circle Marker', () => { cy.hasCircleLayers(1); - cy.toolbarButton('edit') .click() .closest('.button-container') @@ -141,4 +137,187 @@ describe('Draw Circle Marker', () => { cy.hasVertexMarkers(2); }); + it('snapping to CircleMarker with pmIgnore:true', () => { + cy.window().then(({ map, L }) => { + L.circleMarker(map.getCenter(), { pmIgnore: true }).addTo(map); + }); + + cy.toolbarButton('rectangle') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.get(mapSelector) + .click(200, 200) + .click(400, 350); + + cy.toolbarButton('edit') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.hasVertexMarkers(4); + }); + + it('disable continueDrawing', () => { + cy.window().then(({ map }) => { + map.pm.setGlobalOptions({ continueDrawing: false }); + }); + + cy.toolbarButton('circle-marker').click(); + cy.get(mapSelector).click(191, 216); + + cy.get(mapSelector).click(350, 350); + + cy.toolbarButton('circle-marker') + .closest('.button-container') + .should('have.not.class', 'active'); + + cy.toolbarButton('edit').click(); + cy.hasLayers(3); + }); + + it('disable markerEditable', () => { + cy.window().then(({ map }) => { + map.pm.setGlobalOptions({ markerEditable: false }); + }); + + cy.toolbarButton('circle-marker').click(); + cy.get(mapSelector).click(191, 216); + + cy.window().then(({ map }) => { + const marker = map.pm.getGeomanDrawLayers()[0]; + const enabled = marker.pm.enabled(); + expect(enabled).to.equal(false); + }); + }); + + it('enable markerEditable but disable MarkerRemoval', () => { + cy.window().then(({ map }) => { + map.pm.setGlobalOptions({ + markerEditable: true, + preventMarkerRemoval: true, + }); + }); + + cy.toolbarButton('circle-marker').click(); + cy.get(mapSelector).click(191, 216); + + cy.window().then(({ map }) => { + const marker = map.pm.getGeomanDrawLayers()[0]; + const enabled = marker.pm.enabled(); + expect(enabled).to.equal(true); + }); + + cy.get(mapSelector).rightclick(191, 214); + + cy.hasLayers(5); + }); + + it('set max radius of circleMarker', () => { + cy.toolbarButton('circle-marker') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.window().then(({ map, L }) => { + L.marker(map.getCenter()).addTo(map); + map.pm.setGlobalOptions({ + minRadiusCircleMarker: 50, + maxRadiusCircleMarker: 150, + editable: true, + }); + cy.get(mapSelector) + .click(250, 200) + .click(400, 190) + .then(() => { + const layers = map.pm.getGeomanDrawLayers(); + layers.forEach(layer => { + if (layer instanceof L.CircleMarker) { + expect(layer.getRadius()).to.equal(150); + } + }); + }); + }); + }); + it('set min radius of circleMarker', () => { + let handFinish = false; + + cy.toolbarButton('circle-marker') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.window().then(({ map, L }) => { + L.marker(map.getCenter()).addTo(map); + map.pm.setGlobalOptions({ + minRadiusCircleMarker: 150, + maxRadiusCircleMarker: 300, + editable: true, + }); + cy.get(mapSelector) + .click(250, 200) + .click(300, 200) + .then(() => { + const layers = map.pm.getGeomanDrawLayers(); + layers.forEach(layer => { + if (layer instanceof L.CircleMarker) { + expect(layer.getRadius()).to.equal(150); + } + }); + }); + }); + cy.toolbarButton('edit') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.window().then(({ Hand, map, L }) => { + const handMarker = new Hand({ + timing: 'frame', + onStop: () => { + map.eachLayer(layer => { + if (layer instanceof L.CircleMarker) { + expect(true).to.equal(layer.getRadius() > 150); + } + }); + const handMarker2 = new Hand({ + timing: 'frame', + onStop: () => { + handFinish = true; + map.eachLayer(layer => { + if (layer instanceof L.CircleMarker) { + expect(true).to.equal( + layer.getRadius() >= 145 && layer.getRadius() < 155 + ); + } + }); + }, + }); + const toucherMarker2 = handMarker2.growFinger('mouse'); + toucherMarker2 + .wait(200) + .moveTo(490, 198, 100) + .down() + .wait(500) + .moveTo(317, 198, 600) + .up() + .wait(100); + }, + }); + const toucherMarker = handMarker.growFinger('mouse'); + toucherMarker + .wait(100) + .moveTo(400, 198, 100) + .down() + .wait(500) + .moveTo(500, 198, 400) + .up() + .wait(100); + // wait until hand is finished + cy.waitUntil(() => cy.window().then(() => handFinish)).then(() => { + expect(handFinish).to.equal(true); + }); + }); + }); }); diff --git a/cypress/integration/events.spec.js b/cypress/integration/events.spec.js index d0b96b8f..a008a4e3 100644 --- a/cypress/integration/events.spec.js +++ b/cypress/integration/events.spec.js @@ -345,12 +345,14 @@ describe('Events', () => { it('Events while editing: pm:edit,pm:update,pm:enable,pm:disable,pm:vertexadded,pm:vertexremoved', () => { let calledevent = ""; + let calledeventArr = []; cy.window().then(({map}) => { function logEvent(e){ console.log(e.type) calledevent = e.type; + calledeventArr[e.type] = e.type; } map.on("pm:create",({layer}) => { @@ -414,8 +416,9 @@ describe('Events', () => { map.pm.disableGlobalEditMode(); }).then(()=>{ cy.wait(100); - expect(calledevent).to.equal("pm:update"); + expect(calledeventArr["pm:update"]).to.equal("pm:update"); calledevent = ""; + calledeventArr = []; }); cy.window().then(({map}) => { @@ -520,7 +523,49 @@ describe('Events', () => { }); -/* - */ + it('snappingOrder', () => { + + let event = ""; + cy.window().then(({ map}) => { + map.on('pm:drawstart',(e)=>{ + e.workingLayer.on('pm:snap', (x)=>{event=x}); + }); + + map.pm.setGlobalOptions({snappingOrder: ['Marker']}); + }); + + cy.window().then(() => { + cy.toolbarButton('marker').click(); + cy.get(mapSelector) + .click(200, 250); + + cy.toolbarButton('circle-marker').click(); + cy.get(mapSelector) + .click(200, 250); + + cy.toolbarButton('marker').click(); + cy.get(mapSelector) + .trigger("mousemove", 200, 250, { which: 1 }) + }); + cy.window().then(() => { + const shape = event.layerInteractedWith.pm._shape; + expect(shape).to.eq("Marker"); + }); + + + cy.window().then(({map}) => { + map.pm.setGlobalOptions({snappingOrder: ['CircleMarker']}); + + map.pm.enableDraw('Marker'); + + cy.get(mapSelector) + .trigger("mousemove", 200, 150, { which: 1 }) + .trigger("mousemove", 200, 250, { which: 1 }) + }); + cy.window().then(() => { + const shape = event.layerInteractedWith.pm._shape; + expect(shape).to.eq("CircleMarker"); + }); + }); }); diff --git a/cypress/integration/globalmodes.spec.js b/cypress/integration/globalmodes.spec.js index d6e26ca2..e124cd3c 100644 --- a/cypress/integration/globalmodes.spec.js +++ b/cypress/integration/globalmodes.spec.js @@ -311,4 +311,20 @@ describe('Modes', () => { ).to.equal(0); }); }); + it('re-enable layers that added while in globaleditmode', () => { + + cy.window().then(({ map, L }) => { + map.pm.enableGlobalEditMode(); + + const json = JSON.parse("{\"type\":\"Feature\",\"properties\":{},\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[-74.058559,40.718564],[-74.058559,40.726045],[-74.03959,40.726045],[-74.03959,40.718564],[-74.058559,40.718564]]]}}"); + const json2 = JSON.parse("{\"type\":\"Feature\",\"properties\":{},\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[-74.035277,40.703719],[-74.035277,40.712633],[-74.017596,40.712633],[-74.017596,40.703719],[-74.035277,40.703719]]]}}"); + const p2 = L.geoJson(json).addTo(map); + L.geoJson(json2).addTo(map); + + map.fitBounds(p2.getBounds()); + map.setZoom(13); + }); + cy.hasVertexMarkers(8); + + }); }); diff --git a/cypress/integration/imageoverlay.spec.js b/cypress/integration/imageoverlay.spec.js new file mode 100644 index 00000000..fdad2769 --- /dev/null +++ b/cypress/integration/imageoverlay.spec.js @@ -0,0 +1,32 @@ +describe('Opens Testing Environment', () => { + const mapSelector = '#map'; + + it('Snap to ImageOverlay', () => { + let eventcalled = ""; + cy.window().then(({ map, L }) => { + map.setView([18.74469, 72.1258],10); + const icon = 'https://camo.githubusercontent.com/33fa9a94048274f81a806631ca881a55c2aa8f0a/68747470733a2f2f66696c652d6a787a796a67717775742e6e6f772e73682f'; + L.imageOverlay(icon, [[18.74469, 72.1258], [18.94469, 72.3258]], {interactive: true}) + .addTo(map); + + map.on('pm:drawstart',(e)=>{ + const layer = e.workingLayer; + layer.on('pm:snap', (x)=>eventcalled = x.type); + }); + }); + + cy.toolbarButton('polygon') + .click(); + + cy.window().then(({map}) => { + const point = map.latLngToContainerPoint([18.74469, 72.1258]); + cy.get(mapSelector) + .click(point); + }); + + cy.window().then(({}) => { + expect(eventcalled).to.equal("pm:snap"); + }); + + }); +}); diff --git a/cypress/integration/layergroup.spec.js b/cypress/integration/layergroup.spec.js index 081ba2bb..f49d908f 100644 --- a/cypress/integration/layergroup.spec.js +++ b/cypress/integration/layergroup.spec.js @@ -1,5 +1,5 @@ describe('Edit LayerGroup', () => { - // const mapSelector = '#map'; + const mapSelector = '#map'; it('correctly enables geojson featureCollection', () => { cy.drawShape('FeatureCollectionWithCircles'); @@ -43,8 +43,7 @@ describe('Edit LayerGroup', () => { }); it('supports clearLayers', () => { - - cy.window().then(({ L, map }) => { + cy.window().then(({L, map}) => { const featureGroup = new L.FeatureGroup(); featureGroup.addTo(map) featureGroup.addLayer(new L.Marker([19.04469, 72.9258])); @@ -53,5 +52,227 @@ describe('Edit LayerGroup', () => { expect(featureGroup.pm._layers).to.have.lengthOf(0); }); + }); + + it('adds the created layers to a layergroup', () => { + let fg; + let fg2; + + // Add layer to group + cy.window().then(({L, map}) => { + fg = L.featureGroup().addTo(map); + fg2 = L.featureGroup().addTo(map); + map.on('click', (e) => console.log(map.latLngToContainerPoint(e.latlng))); + + map.pm.setGlobalOptions({layerGroup: fg}); + + cy.toolbarButton('rectangle') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + cy.get(mapSelector) + .click(200, 200) + .click(400, 350); + + cy.hasLayers(5); + }); + + // check if layer is on group and will be removed from map, when group is removed + cy.window().then(({map}) => { + fg.removeFrom(map); + cy.hasLayers(3); + + const count = fg.getLayers().length; + expect(count).to.equal(1); + }); + // delete layer from group and map + cy.window().then(({map}) => { + fg.addTo(map); + + cy.toolbarButton('delete') + .click(); + + cy.get(mapSelector) + .click(280, 280); + }); + cy.window().then(() => { + const count = fg.getLayers().length; + expect(count).to.equal(0); + }); + + // add layer and then change group + cy.window().then(() => { + cy.toolbarButton('rectangle') + .click(); + + cy.get(mapSelector) + .click(200, 200) + .click(400, 350); + }); + cy.window().then(({map}) => { + map.pm.setGlobalOptions({layerGroup: fg2}); + cy.hasLayers(5); + + cy.toolbarButton('circle') + .click(); + cy.get(mapSelector) + .click(200, 200) + .click(250, 250); + }); + // delete layer from another (first) group + cy.window().then(() => { + cy.toolbarButton('delete') + .click(); + + cy.get(mapSelector) + .click(280, 280); + }); + cy.window().then(() => { + const count = fg.getLayers().length; + expect(count).to.equal(1); + const count2 = fg2.getLayers().length; + expect(count2).to.equal(1); + cy.hasLayers(5); + }); + // delete circle from second group + cy.window().then(() => { + cy.get(mapSelector) + .click(180, 180); + cy.toolbarButton('delete') + .click(); + }); + cy.window().then(() => { + const count2 = fg2.getLayers().length; + expect(count2).to.equal(0); + cy.hasLayers(4); + }); }) + + + it('pass the fired event of the layer to group', () => { + let fg; + let firedEvent = ""; + + cy.window().then(({map, L}) => { + fg = L.featureGroup(); + fg.on("pm:cut", (e) => { + firedEvent = e.type; + }); + + map.on('pm:create', (e) => { + e.layer.addTo(fg); + }) + }); + // activate polygon drawing + cy.toolbarButton('polygon') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + // draw a polygon + cy.get(mapSelector) + .click(90, 250) + .click(150, 50) + .click(500, 50) + .click(500, 300) + .click(300, 350) + .click(90, 250); + + // activate cutting drawing + cy.toolbarButton('cut') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + // draw a polygon to cut + cy.get(mapSelector) + .click(450, 100) + .click(450, 150) + .click(400, 150) + .click(390, 140) + .click(390, 100) + .click(450, 100); + + cy.window().then(() => { + expect(firedEvent).to.equal('pm:cut'); + }); + }); + + it('event is fired only once if group has multiple sub-groups with the same layer', () => { + + let firedEventCount = 0; + cy.window().then(({map, L}) => { + const group = L.featureGroup().addTo(map); + const layers = L.featureGroup().addTo(group); + const markers = L.featureGroup().addTo(group); + const markersChild = L.featureGroup().addTo(markers); + L.marker(map.getCenter()).addTo(layers).addTo(markers).addTo(markersChild); + + group.on('pm:enable', () => { + firedEventCount += 1 + }); + }); + + cy.wait(100); + + cy.toolbarButton('edit').click(); + + cy.window().then(() => { + expect(firedEventCount).to.equal(1); + }); + }); + + it('event is fired on every parent group of a layer (once)', () => { + + let firedEventCount = 0; + cy.window().then(({map, L}) => { + const group = L.featureGroup().addTo(map); + const layers = L.featureGroup().addTo(group); + const markers = L.featureGroup().addTo(group); + const markersChild = L.featureGroup().addTo(markers); + L.marker(map.getCenter()).addTo(layers).addTo(markers).addTo(markersChild); + + group.on('pm:enable', () => { + firedEventCount += 1 + }); + layers.on('pm:enable', () => { + firedEventCount += 1 + }); + markers.on('pm:enable', () => { + firedEventCount += 1 + }); + markersChild.on('pm:enable', () => { + firedEventCount += 1 + }); + }); + + cy.wait(100); + + cy.toolbarButton('edit').click(); + + cy.window().then(() => { + expect(firedEventCount).to.equal(4); + }); + }); + + it('event is fired on L.geoJson when it has FeatureCollections', () => { + cy.drawShape('FeatureCollectionEventFire'); + + let firedEventCount = 0; + cy.get('@feature').then(feature => { + feature.on('pm:enable', () => { + firedEventCount += 1 + }); + }); + + cy.wait(100); + + cy.toolbarButton('edit').click(); + + cy.window().then(() => { + expect(firedEventCount).to.equal(2); + }); + }); + }); diff --git a/cypress/integration/line.spec.js b/cypress/integration/line.spec.js index 571ea327..21884f44 100644 --- a/cypress/integration/line.spec.js +++ b/cypress/integration/line.spec.js @@ -184,4 +184,28 @@ describe('Draw & Edit Line', () => { cy.hasMiddleMarkers(0); }); + it('enable continueDrawing', () => { + cy.window().then(({ map }) => { + map.pm.setGlobalOptions({continueDrawing: true}); + }); + + cy.toolbarButton('polyline').click(); + + // draw a line + cy.get(mapSelector) + .click(150, 250) + .click(160, 50) + .click(250, 50) + .click(250, 50); + + cy.get(mapSelector) + .click(200, 200) + .click(250, 250) + .click(250, 250); + + + cy.toolbarButton('edit').click(); + cy.hasVertexMarkers(5); + }); + }); diff --git a/cypress/integration/marker.spec.js b/cypress/integration/marker.spec.js index 65cd0687..87151d1b 100644 --- a/cypress/integration/marker.spec.js +++ b/cypress/integration/marker.spec.js @@ -95,10 +95,8 @@ describe('Draw Marker', () => { }); }); - - it('calls pm:drag-events on Marker drag', () => { - + let handFinish = false; let dragstart = false; let drag = false; let dragend = false; @@ -133,12 +131,86 @@ describe('Draw Marker', () => { expect(dragstart).to.equal(true); expect(drag).to.equal(true); expect(dragend).to.equal(true); + handFinish = true; } }); const toucherMarker = handMarker.growFinger('mouse'); toucherMarker.wait(100).moveTo(150, 240, 100).down().wait(500).moveTo(170, 290, 400).up().wait(100) // Not allowed }); + + // wait until hand is finished + cy.waitUntil(() => cy.window().then(() => handFinish)).then( ()=> { + expect(handFinish).to.equal(true); + }); + }); + + + it('enabled of Marker is true in edit-mode', () => { + cy.toolbarButton('marker').click(); + cy.get(mapSelector) + .click(150, 250); + cy.toolbarButton('edit').click(); + + cy.window().then(({ map }) => { + const marker = map.pm.getGeomanDrawLayers()[0]; + const enabled = marker.pm.enabled(); + expect(enabled).to.equal(true); + }); + }); + + it('disable continueDrawing', () => { + cy.window().then(({ map }) => { + map.pm.setGlobalOptions({continueDrawing: false}); + }); + + cy.toolbarButton('marker').click(); + cy.get(mapSelector) + .click(191,216); + + cy.get(mapSelector) + .click(350, 350); + + + cy.toolbarButton('edit').click(); + cy.hasLayers(2); + }); + + it('disable markerEditable', () => { + cy.window().then(({ map }) => { + map.pm.setGlobalOptions({markerEditable: false}); + }); + + cy.toolbarButton('marker').click(); + cy.get(mapSelector) + .click(191,216); + + cy.window().then(({ map }) => { + const marker = map.pm.getGeomanDrawLayers()[0]; + const enabled = marker.pm.enabled(); + expect(enabled).to.equal(false); + }); + }); + + it('enable markerEditable but disable MarkerRemoval', () => { + cy.window().then(({ map }) => { + map.pm.setGlobalOptions({markerEditable: true, preventMarkerRemoval: true}); + }); + + cy.toolbarButton('marker').click(); + cy.get(mapSelector) + .click(191,216); + + cy.window().then(({ map }) => { + const marker = map.pm.getGeomanDrawLayers()[0]; + const enabled = marker.pm.enabled(); + expect(enabled).to.equal(true); + }); + + cy.get(mapSelector) + .rightclick(191,214); + + cy.hasLayers(4); }); }); diff --git a/cypress/integration/polygon.spec.js b/cypress/integration/polygon.spec.js index 50d54e44..abac2720 100644 --- a/cypress/integration/polygon.spec.js +++ b/cypress/integration/polygon.spec.js @@ -22,7 +22,7 @@ describe('Draw & Edit Poly', () => { it('works without pmIgnore', () => { cy.window().then(({ L }) => { - L.PM.initialize({ optIn: false }); + L.PM.setOptIn(false); cy.drawShape('MultiPolygon'); }); @@ -33,7 +33,7 @@ describe('Draw & Edit Poly', () => { it('respects pmIgnore', () => { cy.window().then(({ L }) => { - L.PM.initialize({ optIn: false }); + L.PM.setOptIn(false); cy.drawShape('MultiPolygon', true); }); @@ -44,7 +44,7 @@ describe('Draw & Edit Poly', () => { it('respects optIn', () => { cy.window().then(({ L }) => { - L.PM.initialize({ optIn: true }); + L.PM.setOptIn(true); cy.drawShape('MultiPolygon'); }); @@ -55,7 +55,7 @@ describe('Draw & Edit Poly', () => { it('respects pmIgnore with optIn', () => { cy.window().then(({ L }) => { - L.PM.initialize({ optIn: true }); + L.PM.setOptIn(true); cy.drawShape('MultiPolygon', false); }); @@ -64,6 +64,57 @@ describe('Draw & Edit Poly', () => { cy.hasVertexMarkers(8); }); + it('respects optIn and reinit layer', () => { + cy.window().then(({ L }) => { + L.PM.setOptIn(true); + cy.drawShape('MultiPolygon').then((poly)=>{ + cy.hasVertexMarkers(0); //Not allowed because optIn + L.PM.setOptIn(false); + L.PM.reInitLayer(poly); + }) + }); + cy.toolbarButton('edit').click(); + + cy.hasVertexMarkers(8); + }); + + it('respects optIn and reinit layer with pmIgnore', () => { + cy.window().then(({ L }) => { + L.PM.setOptIn(true); + cy.drawShape('MultiPolygon',true).then((poly)=>{ + cy.hasVertexMarkers(0); //Not allowed because optIn + L.PM.reInitLayer(poly);//Not allowed because pmIgnore is not false + cy.hasVertexMarkers(0); + L.PM.setOptIn(false); + L.PM.reInitLayer(poly);//Not allowed because pmIgnore is true + cy.hasVertexMarkers(0); + poly.options.pmIgnore = false; + poly.eachLayer((layer)=>{ + layer.options.pmIgnore = false; + }); + L.PM.reInitLayer(poly);//Allowed because pmIgnore is not true + }) + }); + cy.toolbarButton('edit').click(); + + cy.hasVertexMarkers(8); + }); + + it('respects optIn and disable optIn', () => { + cy.window().then(({ L }) => { + L.PM.setOptIn(true); + cy.drawShape('MultiPolygon'); + cy.drawShape('MultiPolygon',false).then(()=>{ + L.PM.setOptIn(false); + cy.drawShape('MultiPolygon'); + }) + }); + + cy.toolbarButton('edit').click(); + + cy.hasVertexMarkers(16); + }); + it('doesnt finish single point polys', () => { cy.toolbarButton('polygon').click(); @@ -589,26 +640,15 @@ describe('Draw & Edit Poly', () => { const layer = L.geoJSON(json).getLayers()[0].addTo(map); const bounds = layer.getBounds(); map.fitBounds(bounds); - console.log(map.getCenter()); return layer; }) .as('poly'); cy.get("@poly").then((poly)=>{ + let handFinish = false; expect(poly.pm.hasSelfIntersection()).to.equal(true); - const hand_selfIntersectionTrue = new Hand({ - timing: 'frame', - onStop () { - expect(poly.pm.hasSelfIntersection()).to.equal(true); - - const toucher_selfIntersectionFalse = hand_selfIntersectionFalse.growFinger('mouse'); - toucher_selfIntersectionFalse.wait(100).moveTo(504, 337, 100).down().wait(500).moveTo(780, 259, 400).up().wait(100) // allowed - // No intersection anymore - .moveTo(294, 114, 100).down().wait(500).moveTo(752, 327, 800).up().wait(500) // Not allowed - } - }); - var hand_selfIntersectionFalse = new Hand({ + const handSelfIntersectionFalse = new Hand({ timing: 'frame', onStop () { expect(poly.pm.hasSelfIntersection()).to.equal(false); @@ -617,7 +657,18 @@ describe('Draw & Edit Poly', () => { const center = map.getCenter(); expect(center.lat).to.equal(48.77492609799526); expect(center.lng).to.equal(4.847301999999988); + handFinish = true; + } + }); + const handSelfIntersectionTrue = new Hand({ + timing: 'frame', + onStop () { + expect(poly.pm.hasSelfIntersection()).to.equal(true); + const toucherSelfIntersectionFalse = handSelfIntersectionFalse.growFinger('mouse'); + toucherSelfIntersectionFalse.wait(100).moveTo(504, 337, 100).down().wait(500).moveTo(780, 259, 400).up().wait(100) // allowed + // No intersection anymore + .moveTo(294, 114, 100).down().wait(500).moveTo(752, 327, 800).up().wait(500) // Not allowed } }); @@ -625,12 +676,98 @@ describe('Draw & Edit Poly', () => { map.pm.enableGlobalEditMode({ allowSelfIntersection: false, allowSelfIntersectionEdit: true, }); - const toucher_selfIntersectionTrue = hand_selfIntersectionTrue.growFinger('mouse'); - toucher_selfIntersectionTrue.wait(100).moveTo(294, 114, 100).down().wait(500).moveTo(782, 127, 400).up().wait(100) // Not allowed + const toucherSelfIntersectionTrue = handSelfIntersectionTrue.growFinger('mouse'); + toucherSelfIntersectionTrue.wait(100).moveTo(294, 114, 100).down().wait(500).moveTo(782, 127, 400).up().wait(100) // Not allowed .moveTo(313, 345, 100).down().wait(500).moveTo(256, 311, 400).up().wait(100) // allowed .moveTo(317, 252, 100).down().wait(500).moveTo(782, 127, 400).up().wait(500); // allowed - }) + + // wait until hand is finished + cy.waitUntil(() => cy.window().then(() => handFinish),{ + timeout: 9000, + }).then( ()=> { + expect(handFinish).to.equal(true); + }); + + }); }); }); + + it('no snapping to polygon with no coords', () => { + cy.window().then(({ map, L }) => { + L.polygon([]).addTo(map); + }); + + // activate line drawing + cy.toolbarButton('polyline') + .click() + .closest('.button-container') + .should('have.class', 'active'); + + // draw a line + cy.get(mapSelector) + .click(150, 250) + .click(160, 50) + .click(160, 50); + + + cy.toolbarButton('edit') + .click(); + + cy.hasVertexMarkers(2); + }); + + it('don\'t Cut if it has selfIntersection on finish', () => { + cy.toolbarButton('polygon') + .click(); + + cy.get(mapSelector) + .click(90, 250) + .click(150, 50) + .click(500, 50) + .click(500, 300) + .click(300, 350) + .click(90, 250); + + + cy.toolbarButton('cut') + .click(); + + // draw a polygon to cut + cy.get(mapSelector) + .click(200, 100) + .click(450, 100) + .click(150, 200) + .click(450, 200) + .click(200, 100); + + cy.toolbarButton('edit') + .click(); + + cy.hasVertexMarkers(5); + }); + + it('enable continueDrawing', () => { + cy.window().then(({ map }) => { + map.pm.setGlobalOptions({continueDrawing: true}); + }); + + cy.toolbarButton('polygon').click(); + + // draw a line + cy.get(mapSelector) + .click(150, 250) + .click(160, 50) + .click(250, 50) + .click(150, 250); + + cy.get(mapSelector) + .click(230, 230) + .click(250, 250) + .click(250, 300) + .click(230, 230); + + cy.toolbarButton('edit').click(); + cy.hasVertexMarkers(6); + }); }); diff --git a/cypress/integration/rectangle.spec.js b/cypress/integration/rectangle.spec.js index dd180522..d2e5d6ec 100644 --- a/cypress/integration/rectangle.spec.js +++ b/cypress/integration/rectangle.spec.js @@ -97,5 +97,116 @@ describe('Draw Rectangle', () => { cy.toolbarButton('edit').click(); cy.hasVertexMarkers(16); - }) + }); + + it('goes back to blue after self-intersection removed', ()=>{ + cy.toolbarButton('rectangle').click(); + cy.get(mapSelector) + .click(100,50) + .click(700,400); + + cy.toolbarButton('cut').click(); + cy.get(mapSelector) + .click(200,200) + .click(250,230) + .click(300,250) + .click(370,200) + .click(200,200); + + cy.toolbarButton('cut').click(); + cy.get(mapSelector) + .click(200,300) + .click(250,270) + .click(300,250) + .click(370,300) + .click(200,300); + + cy.toolbarButton('edit').click(); + cy.hasVertexMarkers(12); + + cy.get(mapSelector).rightclick(300,250); + + cy.window().then(({ map, L }) => { + const rect = map.pm.getGeomanDrawLayers()[0]; + expect(rect.options.color).to.not.equal('red'); + + }) + }); + + it('remove empty coord rings', ()=>{ + cy.toolbarButton('rectangle').click(); + cy.get(mapSelector) + .click(100,50) + .click(700,400); + + cy.toolbarButton('cut').click(); + cy.get(mapSelector) + .click(200,200) + .click(300,250) + .click(370,200) + .click(200,200); + + + cy.toolbarButton('edit').click(); + cy.hasVertexMarkers(7); + + cy.get(mapSelector).rightclick(300,250); + + cy.window().then(({ map}) => { + const rect = map.pm.getGeomanDrawLayers()[0]; + const geojson = rect.toGeoJSON(); + const coords = geojson.geometry.coordinates; + expect(coords.length).to.equal(1); + }) + }); + + it('enable continueDrawing', () => { + cy.window().then(({ map }) => { + map.pm.setGlobalOptions({continueDrawing: true}); + }); + + cy.toolbarButton('rectangle').click(); + cy.get(mapSelector) + .click(191,216) + .click(608,323); + + cy.get(mapSelector) + .click(230, 230) + .click(350, 350); + + + cy.toolbarButton('edit').click(); + cy.hasVertexMarkers(8); + }); + + it('disable popup on layer while drawing', ()=>{ + let rect = null; + cy.window().then(({ map, L }) => { + map.on("pm:create",(e)=>{ + e.layer.bindPopup('Popup test'); + if(e.layer instanceof L.Rectangle){ + rect = e.layer; + } + }); + }); + + cy.toolbarButton('rectangle').click(); + cy.get(mapSelector) + .click(100,50) + .click(700,400); + + cy.toolbarButton('marker').click(); + cy.get(mapSelector) + .click(300,250); + + cy.toolbarButton('edit').click(); + + cy.window().then(({ map}) => { + const len = map.pm.getGeomanDrawLayers().length; + expect(len).to.equal(2); + + const text = rect.getPopup().getContent(); + expect(text).to.equal('Popup test'); + }) + }); }); diff --git a/cypress/integration/toolbar.spec.js b/cypress/integration/toolbar.spec.js index 866b242f..1f8b066c 100644 --- a/cypress/integration/toolbar.spec.js +++ b/cypress/integration/toolbar.spec.js @@ -351,4 +351,36 @@ describe('Testing the Toolbar', () => { }); }); }); + it('Listen on pm:buttonclick and pm:actionclick', () => { + let eventFired = ""; + cy.window().then(({map}) => { + map.on('pm:buttonclick', ({btnName})=>{eventFired = btnName}); + map.on('pm:actionclick', ({text})=>{eventFired = text}); + }); + + cy.toolbarButton('polygon').click(); + + cy.window().then(() => { + expect(eventFired).to.equal('drawPolygon'); + }); + + cy.get('.button-container.active .action-cancel').click(); + + cy.window().then(() => { + expect(eventFired).to.equal('Cancel'); + }); + }); + it('Disable button', () => { + let eventFired = ""; + cy.window().then(({map}) => { + map.on('pm:buttonclick', ({btnName})=>{eventFired = btnName}); + map.pm.Toolbar.setButtonDisabled('drawPolygon',true); + }); + + cy.toolbarButton('polygon').click(); + + cy.window().then(() => { + expect(eventFired).to.not.equal('drawPolygon'); + }); + }); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index bbb742aa..1eb047e3 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -1,3 +1,4 @@ +import 'cypress-wait-until'; // *********************************************** // This example commands.js shows you how to // create various custom commands and overwrite @@ -136,6 +137,7 @@ Cypress.Commands.add('drawShape', (shape, ignore) => { const layer = L.geoJson(json, { pmIgnore: ignore }).addTo(map); const bounds = layer.getBounds(); map.fitBounds(bounds); + return layer; }); } @@ -171,6 +173,19 @@ Cypress.Commands.add('drawShape', (shape, ignore) => { }); } + if (shape === 'FeatureCollectionEventFire') { + cy.fixture(shape) + .then(json => { + // + const layer = L.geoJSON(json).addTo(map); + const bounds = layer.getBounds(); + map.fitBounds(bounds); + + return layer; + }) + .as('feature'); + } + if (shape === 'FeatureCollectionWithCircles') { cy.fixture(shape, ignore) .then(json => { diff --git a/demo/customcontrols.js b/demo/customcontrols.js index ca11b8c1..077cf282 100644 --- a/demo/customcontrols.js +++ b/demo/customcontrols.js @@ -30,3 +30,9 @@ map.pm.Draw.RectangleCopy.setPathOptions({color :'green'}); map.pm.Toolbar.changeControlOrder(["RectangleCopy"]) +map.on('pm:actionclick',function (e) { + console.log(e) +}) +map.on('pm:buttonclick',function (e) { + console.log(e) +}) diff --git a/demo/events.js b/demo/events.js index 3b79846a..ea3792a5 100644 --- a/demo/events.js +++ b/demo/events.js @@ -42,6 +42,7 @@ map.on('pm:create',function (e) { layer.on('pm:vertexadded', logEvent); layer.on('pm:vertexremoved', logEvent); layer.on('pm:markerdragstart', logEvent); + layer.on('pm:markerdrag', logEvent); layer.on('pm:markerdragend', logEvent); layer.on('pm:snap', logEvent); layer.on('pm:snapdrag', logEvent); diff --git a/package-lock.json b/package-lock.json index 6b86cb97..4567e737 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@geoman-io/leaflet-geoman-free", - "version": "2.7.0", + "version": "2.8.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1141,6 +1141,108 @@ "@turf/meta": "6.x" } }, + "@turf/bearing": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/bearing/-/bearing-5.1.5.tgz", + "integrity": "sha1-egt5ATbE70eX8CRjBdRcvi0ns/c=", + "requires": { + "@turf/helpers": "^5.1.5", + "@turf/invariant": "^5.1.5" + }, + "dependencies": { + "@turf/helpers": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz", + "integrity": "sha1-FTQFInq5M9AEpbuWQantmZ/L4M8=" + }, + "@turf/invariant": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-5.2.0.tgz", + "integrity": "sha1-8BUP9ykLOFd7c9CIt5MsHuCqkKc=", + "requires": { + "@turf/helpers": "^5.1.5" + } + } + } + }, + "@turf/boolean-contains": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@turf/boolean-contains/-/boolean-contains-6.0.1.tgz", + "integrity": "sha512-usAexEdWu7dV43paowGSFEM0PljexnlOuj09HF/VDZwO3FKelwUovF2ymetYatuG7KcIYcexeNEkQ5qQnGExlw==", + "requires": { + "@turf/bbox": "6.x", + "@turf/boolean-point-in-polygon": "6.x", + "@turf/boolean-point-on-line": "6.x", + "@turf/helpers": "6.x", + "@turf/invariant": "6.x" + } + }, + "@turf/boolean-point-in-polygon": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.0.1.tgz", + "integrity": "sha512-FKLOZ124vkJhjzNSDcqpwp2NvfnsbYoUOt5iAE7uskt4svix5hcjIEgX9sELFTJpbLGsD1mUbKdfns8tZxcMNg==", + "requires": { + "@turf/helpers": "6.x", + "@turf/invariant": "6.x" + } + }, + "@turf/boolean-point-on-line": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-on-line/-/boolean-point-on-line-6.0.1.tgz", + "integrity": "sha512-Vl724Tzh4CF/13kgblOAQnMcHagromCP1EfyJ9G/8SxpSoTYeY2G6FmmcpbW51GqKxC7xgM9+Pck50dun7oYkg==", + "requires": { + "@turf/helpers": "6.x", + "@turf/invariant": "6.x" + } + }, + "@turf/destination": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/destination/-/destination-5.1.5.tgz", + "integrity": "sha1-7TU4G9zoO73cvQei4rzivd/7zCY=", + "requires": { + "@turf/helpers": "^5.1.5", + "@turf/invariant": "^5.1.5" + }, + "dependencies": { + "@turf/helpers": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz", + "integrity": "sha1-FTQFInq5M9AEpbuWQantmZ/L4M8=" + }, + "@turf/invariant": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-5.2.0.tgz", + "integrity": "sha1-8BUP9ykLOFd7c9CIt5MsHuCqkKc=", + "requires": { + "@turf/helpers": "^5.1.5" + } + } + } + }, + "@turf/distance": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/distance/-/distance-5.1.5.tgz", + "integrity": "sha1-Oc8YIEu/h1h9cH5gmmARiQkVZAk=", + "requires": { + "@turf/helpers": "^5.1.5", + "@turf/invariant": "^5.1.5" + }, + "dependencies": { + "@turf/helpers": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz", + "integrity": "sha1-FTQFInq5M9AEpbuWQantmZ/L4M8=" + }, + "@turf/invariant": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-5.2.0.tgz", + "integrity": "sha1-8BUP9ykLOFd7c9CIt5MsHuCqkKc=", + "requires": { + "@turf/helpers": "^5.1.5" + } + } + } + }, "@turf/helpers": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.1.4.tgz", @@ -1184,6 +1286,87 @@ "@turf/meta": "6.x" } }, + "@turf/line-split": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/line-split/-/line-split-5.1.5.tgz", + "integrity": "sha1-Wy30w3YZty73JbUWPPmSbVVArLc=", + "requires": { + "@turf/bbox": "^5.1.5", + "@turf/helpers": "^5.1.5", + "@turf/invariant": "^5.1.5", + "@turf/line-intersect": "^5.1.5", + "@turf/line-segment": "^5.1.5", + "@turf/meta": "^5.1.5", + "@turf/nearest-point-on-line": "^5.1.5", + "@turf/square": "^5.1.5", + "@turf/truncate": "^5.1.5", + "geojson-rbush": "2.1.0" + }, + "dependencies": { + "@turf/bbox": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-5.1.5.tgz", + "integrity": "sha1-MFHfUUrUxQ9KT5uKLRX9i2hA7aM=", + "requires": { + "@turf/helpers": "^5.1.5", + "@turf/meta": "^5.1.5" + } + }, + "@turf/helpers": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz", + "integrity": "sha1-FTQFInq5M9AEpbuWQantmZ/L4M8=" + }, + "@turf/invariant": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-5.2.0.tgz", + "integrity": "sha1-8BUP9ykLOFd7c9CIt5MsHuCqkKc=", + "requires": { + "@turf/helpers": "^5.1.5" + } + }, + "@turf/line-intersect": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/line-intersect/-/line-intersect-5.1.5.tgz", + "integrity": "sha1-DikHGuQDKV5JFyO8SfXPrI0R3fM=", + "requires": { + "@turf/helpers": "^5.1.5", + "@turf/invariant": "^5.1.5", + "@turf/line-segment": "^5.1.5", + "@turf/meta": "^5.1.5", + "geojson-rbush": "2.1.0" + } + }, + "@turf/line-segment": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/line-segment/-/line-segment-5.1.5.tgz", + "integrity": "sha1-Mgeq7lRqskw9jcPMY/kcdwuAE+U=", + "requires": { + "@turf/helpers": "^5.1.5", + "@turf/invariant": "^5.1.5", + "@turf/meta": "^5.1.5" + } + }, + "@turf/meta": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-5.2.0.tgz", + "integrity": "sha1-OxrUhe4MOwsXdRMqMsOE1T5LpT0=", + "requires": { + "@turf/helpers": "^5.1.5" + } + }, + "geojson-rbush": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/geojson-rbush/-/geojson-rbush-2.1.0.tgz", + "integrity": "sha1-O9c745H8ELCuaT2bis6iquC4Oo0=", + "requires": { + "@turf/helpers": "*", + "@turf/meta": "*", + "rbush": "*" + } + } + } + }, "@turf/meta": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-6.0.2.tgz", @@ -1192,6 +1375,115 @@ "@turf/helpers": "6.x" } }, + "@turf/nearest-point-on-line": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/nearest-point-on-line/-/nearest-point-on-line-5.1.5.tgz", + "integrity": "sha1-VgauKX8VlHUkvqUaKp71HsG/nDY=", + "requires": { + "@turf/bearing": "^5.1.5", + "@turf/destination": "^5.1.5", + "@turf/distance": "^5.1.5", + "@turf/helpers": "^5.1.5", + "@turf/invariant": "^5.1.5", + "@turf/line-intersect": "^5.1.5", + "@turf/meta": "^5.1.5" + }, + "dependencies": { + "@turf/helpers": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz", + "integrity": "sha1-FTQFInq5M9AEpbuWQantmZ/L4M8=" + }, + "@turf/invariant": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-5.2.0.tgz", + "integrity": "sha1-8BUP9ykLOFd7c9CIt5MsHuCqkKc=", + "requires": { + "@turf/helpers": "^5.1.5" + } + }, + "@turf/line-intersect": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/line-intersect/-/line-intersect-5.1.5.tgz", + "integrity": "sha1-DikHGuQDKV5JFyO8SfXPrI0R3fM=", + "requires": { + "@turf/helpers": "^5.1.5", + "@turf/invariant": "^5.1.5", + "@turf/line-segment": "^5.1.5", + "@turf/meta": "^5.1.5", + "geojson-rbush": "2.1.0" + } + }, + "@turf/line-segment": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/line-segment/-/line-segment-5.1.5.tgz", + "integrity": "sha1-Mgeq7lRqskw9jcPMY/kcdwuAE+U=", + "requires": { + "@turf/helpers": "^5.1.5", + "@turf/invariant": "^5.1.5", + "@turf/meta": "^5.1.5" + } + }, + "@turf/meta": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-5.2.0.tgz", + "integrity": "sha1-OxrUhe4MOwsXdRMqMsOE1T5LpT0=", + "requires": { + "@turf/helpers": "^5.1.5" + } + }, + "geojson-rbush": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/geojson-rbush/-/geojson-rbush-2.1.0.tgz", + "integrity": "sha1-O9c745H8ELCuaT2bis6iquC4Oo0=", + "requires": { + "@turf/helpers": "*", + "@turf/meta": "*", + "rbush": "*" + } + } + } + }, + "@turf/square": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/square/-/square-5.1.5.tgz", + "integrity": "sha1-qnsh5gM8ySUsOlvW89iNq9b+0YA=", + "requires": { + "@turf/distance": "^5.1.5", + "@turf/helpers": "^5.1.5" + }, + "dependencies": { + "@turf/helpers": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz", + "integrity": "sha1-FTQFInq5M9AEpbuWQantmZ/L4M8=" + } + } + }, + "@turf/truncate": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/truncate/-/truncate-5.1.5.tgz", + "integrity": "sha1-nu37Oxi6gfLJjT6tCUMcyhiErYk=", + "requires": { + "@turf/helpers": "^5.1.5", + "@turf/meta": "^5.1.5" + }, + "dependencies": { + "@turf/helpers": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz", + "integrity": "sha1-FTQFInq5M9AEpbuWQantmZ/L4M8=" + }, + "@turf/meta": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-5.2.0.tgz", + "integrity": "sha1-OxrUhe4MOwsXdRMqMsOE1T5LpT0=", + "requires": { + "@turf/helpers": "^5.1.5" + } + } + } + }, "@types/json-schema": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", @@ -3235,6 +3527,12 @@ } } }, + "cypress-wait-until": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-1.7.1.tgz", + "integrity": "sha512-8DL5IsBTbAxBjfYgCzdbohPq/bY+IKc63fxtso1C8RWhLnQkZbVESyaclNr76jyxfId6uyzX8+Xnt0ZwaXNtkA==", + "dev": true + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", diff --git a/package.json b/package.json index ccd2bbff..8d96aac1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@geoman-io/leaflet-geoman-free", - "version": "2.7.0", + "version": "2.8.0", "description": "A Leaflet Plugin For Editing Geometry Layers in Leaflet 1.0", "keywords": [ "leaflet", @@ -23,7 +23,9 @@ "@turf/kinks": "6.x", "@turf/line-intersect": "^6.0.2", "polygon-clipping": "^0.15.1", - "lodash": "^4.17.15" + "lodash": "^4.17.15", + "@turf/boolean-contains": "^6.0.1", + "@turf/line-split": "^5.1.5" }, "devDependencies": { "@babel/core": "^7.9.6", @@ -32,6 +34,7 @@ "copy-webpack-plugin": "^6.2.1", "css-loader": "^3.5.3", "cypress": "^3.8.3", + "cypress-wait-until": "^1.7.1", "eslint": "^4.19.1", "eslint-config-airbnb-base": "^12.1.0", "eslint-config-prettier": "^3.6.0", diff --git a/src/assets/translations/da.json b/src/assets/translations/da.json new file mode 100644 index 00000000..b4a469dd --- /dev/null +++ b/src/assets/translations/da.json @@ -0,0 +1,32 @@ +{ + "tooltips": { + "placeMarker": "Tryk for at placere en markør", + "firstVertex": "Tryk for at placere det første punkt", + "continueLine": "Tryk for at fortsætte linjen", + "finishLine": "Tryk på et eksisterende punkt for at afslutte", + "finishPoly": "Tryk på det første punkt for at afslutte", + "finishRect": "Tryk for at afslutte", + "startCircle": "Tryk for at placere cirklens center", + "finishCircle": "Tryk for at afslutte cirklen", + "placeCircleMarker": "Tryk for at placere en cirkelmarkør" + }, + "actions": { + "finish": "Afslut", + "cancel": "Afbryd", + "removeLastVertex": "Fjern sidste punkt" + }, + "buttonTitles": { + "drawMarkerButton": "Placer markør", + "drawPolyButton": "Tegn polygon", + "drawLineButton": "Tegn linje", + "drawCircleButton": "Tegn cirkel", + "drawRectButton": "Tegn firkant", + "editButton": "Rediger", + "dragButton": "Træk", + "cutButton": "Klip", + "deleteButton": "Fjern", + "drawCircleMarkerButton": "Tegn cirkelmarkør", + "snappingButton": "Fastgør trukket markør til andre elementer", + "pinningButton": "Sammenlæg delte elementer" + } +} \ No newline at end of file diff --git a/src/assets/translations/de.json b/src/assets/translations/de.json index f84b179d..5a99ec32 100644 --- a/src/assets/translations/de.json +++ b/src/assets/translations/de.json @@ -25,6 +25,8 @@ "dragButton": "Layer bewegen", "cutButton": "Layer schneiden", "deleteButton": "Layer löschen", - "drawCircleMarkerButton": "Kreismarker zeichnen" + "drawCircleMarkerButton": "Kreismarker zeichnen", + "snappingButton": "Bewegter Layer an andere Layer oder Vertexe einhacken", + "pinningButton": "Vertexe an der gleichen Position verknüpfen" } } diff --git a/src/assets/translations/en.json b/src/assets/translations/en.json index 57b37843..b5b052fa 100644 --- a/src/assets/translations/en.json +++ b/src/assets/translations/en.json @@ -29,4 +29,4 @@ "snappingButton": "Snap dragged marker to other layers and vertices", "pinningButton": "Pin shared vertices together" } -} \ No newline at end of file +} diff --git a/src/assets/translations/es.json b/src/assets/translations/es.json index e975bfce..e6d04e4b 100644 --- a/src/assets/translations/es.json +++ b/src/assets/translations/es.json @@ -25,6 +25,8 @@ "dragButton": "Arrastrar Capas", "cutButton": "Cortar Capas", "deleteButton": "Remover Capas", - "drawCircleMarkerButton": "Dibujar Marcador de Circulo" + "drawCircleMarkerButton": "Dibujar Marcador de Circulo", + "snappingButton": "El marcador de Snap arrastrado a otras capas y vértices", + "pinningButton": "Fijar juntos los vértices compartidos" } } diff --git a/src/assets/translations/fr.json b/src/assets/translations/fr.json index 942c5a8b..ff647492 100644 --- a/src/assets/translations/fr.json +++ b/src/assets/translations/fr.json @@ -7,7 +7,8 @@ "finishPoly": "Cliquez sur le premier marqueur pour terminer", "finishRect": "Cliquez pour terminer", "startCircle": "Cliquez pour placer le centre du cercle", - "finishCircle": "Cliquez pour finir le cercle" + "finishCircle": "Cliquez pour finir le cercle", + "placeCircleMarker": "Cliquez pour placer le marqueur circulaire" }, "actions": { "finish": "Terminer", @@ -23,6 +24,9 @@ "editButton": "Éditer des calques", "dragButton": "Déplacer des calques", "cutButton": "Couper des calques", - "deleteButton": "Supprimer des calques" + "deleteButton": "Supprimer des calques", + "drawCircleMarkerButton": "Dessiner un marqueur circulaire", + "snappingButton": "Glisser le marqueur vers d'autres couches et sommets", + "pinningButton": "Épingler ensemble les sommets partagés" } } diff --git a/src/assets/translations/id.json b/src/assets/translations/id.json index ae3b369c..129f05c4 100644 --- a/src/assets/translations/id.json +++ b/src/assets/translations/id.json @@ -7,7 +7,8 @@ "finishPoly": "Klik marker pertama untuk mengakhiri", "finishRect": "Klik untuk mengakhiri", "startCircle": "Klik untuk menempatkan titik pusat lingkaran", - "finishCircle": "Klik untuk mengakhiri lingkaran" + "finishCircle": "Klik untuk mengakhiri lingkaran", + "placeCircleMarker": "Klik untuk menempatkan penanda lingkarann" }, "actions": { "finish": "Selesai", @@ -23,6 +24,9 @@ "editButton": "Edit Layer", "dragButton": "Geser Layer", "cutButton": "Potong Layer", - "deleteButton": "Hilangkan Layer" + "deleteButton": "Hilangkan Layer", + "drawCircleMarkerButton": "Digitasi Penanda Lingkaran", + "snappingButton": "Jepretkan penanda yang ditarik ke lapisan dan simpul lain", + "pinningButton": "Sematkan simpul bersama bersama" } } diff --git a/src/assets/translations/index.js b/src/assets/translations/index.js index 96538bea..bfa54d76 100644 --- a/src/assets/translations/index.js +++ b/src/assets/translations/index.js @@ -9,11 +9,14 @@ import nl from './nl.json'; import fr from './fr.json'; import zh from './zh.json'; // eslint-disable-next-line camelcase +import zh_tw from './zh_tw.json'; +// eslint-disable-next-line camelcase import pt_br from './pt_br.json'; import pl from './pl.json'; import sv from './sv.json'; import el from './el.json'; import hu from './hu.json'; +import da from './da.json'; export default { en, @@ -27,8 +30,10 @@ export default { fr, pt_br, zh, + zh_tw, pl, sv, el, - hu + hu, + da }; diff --git a/src/assets/translations/it.json b/src/assets/translations/it.json index 850cad9f..21f7e08e 100644 --- a/src/assets/translations/it.json +++ b/src/assets/translations/it.json @@ -25,6 +25,8 @@ "dragButton": "Sposta Livelli", "cutButton": "Ritaglia Livelli", "deleteButton": "Elimina Livelli", - "drawCircleMarkerButton": "Disegna Marker del Cherchio" + "drawCircleMarkerButton": "Disegna Marker del Cerchio", + "snappingButton": "Snap ha trascinato il pennarello su altri strati e vertici", + "pinningButton": "Pin condiviso vertici insieme" } } diff --git a/src/assets/translations/nl.json b/src/assets/translations/nl.json index 645f0810..92acb090 100644 --- a/src/assets/translations/nl.json +++ b/src/assets/translations/nl.json @@ -25,6 +25,8 @@ "dragButton": "Verplaats", "cutButton": "Knip", "deleteButton": "Verwijder", - "drawCircleMarkerButton": "Plaats Marker" + "drawCircleMarkerButton": "Plaats Marker", + "snappingButton": "Snap gesleepte marker naar andere lagen en hoekpunten", + "pinningButton": "Speld gedeelde hoekpunten samen" } } diff --git a/src/assets/translations/pl.json b/src/assets/translations/pl.json index c043a036..1c4c2938 100644 --- a/src/assets/translations/pl.json +++ b/src/assets/translations/pl.json @@ -25,6 +25,8 @@ "dragButton": "Przesuń", "cutButton": "Wytnij", "deleteButton": "Usuń", - "drawCircleMarkerButton": "Narysuj okrągły znacznik" + "drawCircleMarkerButton": "Narysuj okrągły znacznik", + "snappingButton": "Snap przeciągnięty marker na inne warstwy i wierzchołki", + "pinningButton": "Sworzeń wspólne wierzchołki razem" } } diff --git a/src/assets/translations/pt_br.json b/src/assets/translations/pt_br.json index b45753df..5a12666f 100644 --- a/src/assets/translations/pt_br.json +++ b/src/assets/translations/pt_br.json @@ -7,7 +7,8 @@ "finishPoly": "Clique no primeiro ponto para fechar o polígono", "finishRect": "Clique para finalizar", "startCircle": "Clique para posicionar o centro do círculo", - "finishCircle": "Clique para fechar o círculo" + "finishCircle": "Clique para fechar o círculo", + "placeCircleMarker": "Clique para posicionar o marcador circular" }, "actions": { "finish": "Finalizar", @@ -23,7 +24,9 @@ "editButton": "Editar camada(s)", "dragButton": "Mover camada(s)", "cutButton": "Recortar camada(s)", - "deleteButton": "Remover camada(s)" + "deleteButton": "Remover camada(s)", + "drawCircleMarkerButton": "Marcador de círculos de desenho", + "snappingButton": "Marcador arrastado para outras camadas e vértices", + "pinningButton": "Vértices compartilhados de pinos juntos" } } - \ No newline at end of file diff --git a/src/assets/translations/ro.json b/src/assets/translations/ro.json index a80a990d..01c3d5d6 100644 --- a/src/assets/translations/ro.json +++ b/src/assets/translations/ro.json @@ -25,6 +25,8 @@ "dragButton": "Mută straturile", "cutButton": "Taie straturile", "deleteButton": "Șterge straturile", - "placeCircleMarker": "Adaugă o bulină" + "drawCircleMarkerButton": "Desenează marcatorul cercului", + "snappingButton": "Fixați marcatorul glisat pe alte straturi și vârfuri", + "pinningButton": "Fixați vârfurile partajate împreună" } } diff --git a/src/assets/translations/sv.json b/src/assets/translations/sv.json index 78ae7f57..f5c96404 100644 --- a/src/assets/translations/sv.json +++ b/src/assets/translations/sv.json @@ -25,6 +25,8 @@ "dragButton": "Dra Lager", "cutButton": "Klipp i Lager", "deleteButton": "Ta bort Lager", - "drawCircleMarkerButton": "Rita Cirkelmarkör" + "drawCircleMarkerButton": "Rita Cirkelmarkör", + "snappingButton": "Snäpp dra markören till andra lager och hörn", + "pinningButton": "Fäst delade hörn tillsammans" } } diff --git a/src/assets/translations/zh.json b/src/assets/translations/zh.json index 76f97df2..e835fac0 100644 --- a/src/assets/translations/zh.json +++ b/src/assets/translations/zh.json @@ -7,7 +7,8 @@ "finishPoly": "单击第一个标记以完成", "finishRect": "单击完成", "startCircle": "单击放置圆心", - "finishCircle": "单击完成圆形" + "finishCircle": "单击完成圆形", + "placeCircleMarker": "点击放置圆形标记" }, "actions": { "finish": "完成", @@ -23,6 +24,9 @@ "editButton": "编辑图层", "dragButton": "拖拽图层", "cutButton": "剪切图层", - "deleteButton": "删除图层" + "deleteButton": "删除图层", + "drawCircleMarkerButton": "画圆圈标记", + "snappingButton": "将拖动的标记捕捉到其他图层和顶点", + "pinningButton": "将共享顶点固定在一起" } } diff --git a/src/assets/translations/zh_tw.json b/src/assets/translations/zh_tw.json new file mode 100644 index 00000000..9517c92d --- /dev/null +++ b/src/assets/translations/zh_tw.json @@ -0,0 +1,32 @@ +{ + "tooltips": { + "placeMarker": "單擊放置標記", + "firstVertex": "單擊放置第一個頂點", + "continueLine": "單擊繼續繪製", + "finishLine": "單擊任何存在的標記以完成", + "finishPoly": "單擊第一個標記以完成", + "finishRect": "單擊完成", + "startCircle": "單擊放置圓心", + "finishCircle": "單擊完成圓形", + "placeCircleMarker": "點擊放置圓形標記" + }, + "actions": { + "finish": "完成", + "cancel": "取消", + "removeLastVertex": "移除最後一個頂點" + }, + "buttonTitles": { + "drawMarkerButton": "放置標記", + "drawPolyButton": "繪製多邊形", + "drawLineButton": "繪製線段", + "drawCircleButton": "繪製圓形", + "drawRectButton": "繪製方形", + "editButton": "編輯圖形", + "dragButton": "移動圖形", + "cutButton": "裁切圖形", + "deleteButton": "刪除圖形", + "drawCircleMarkerButton": "畫圓圈標記", + "snappingButton": "將拖動的標記對齊到其他圖層和頂點", + "pinningButton": "將共享頂點固定在一起" + } +} diff --git a/src/css/controls.css b/src/css/controls.css index a4f2d5a8..af0ef3da 100644 --- a/src/css/controls.css +++ b/src/css/controls.css @@ -100,6 +100,7 @@ left: 100%; display: none; white-space: nowrap; + direction: ltr; } .leaflet-right @@ -144,3 +145,10 @@ z-index: 801; } +.leaflet-buttons-control-button.pm-disabled{ + background-color: #f4f4f4 !important; +} + +.control-icon.pm-disabled{ + filter: opacity(0.6); +} diff --git a/src/js/Draw/L.PM.Draw.Circle.js b/src/js/Draw/L.PM.Draw.Circle.js index c847b073..61f13f79 100644 --- a/src/js/Draw/L.PM.Draw.Circle.js +++ b/src/js/Draw/L.PM.Draw.Circle.js @@ -1,6 +1,7 @@ import Draw from './L.PM.Draw'; -import { getTranslation } from '../helpers'; +import {destinationOnLine, getTranslation} from '../helpers'; +import Utils from "../L.PM.Utils"; Draw.Circle = Draw.extend({ initialize(map) { @@ -55,7 +56,6 @@ Draw.Circle = Draw.extend({ permanent: true, offset: L.point(0, 10), direction: 'bottom', - opacity: 0.8, }) .openTooltip(); @@ -75,12 +75,6 @@ Draw.Circle = Draw.extend({ // sync hint marker with mouse cursor this._map.on('mousemove', this._syncHintMarker, this); - // fire drawstart event - this._map.fire('pm:drawstart', { - shape: this._shape, - workingLayer: this._layer, - }); - this._setGlobalDrawMode(); // toggle the draw button of the Toolbar in case drawing mode got enabled without the button this._map.pm.Toolbar.toggleButton(this.toolbarButtonName, true); @@ -88,6 +82,13 @@ Draw.Circle = Draw.extend({ // an array used in the snapping mixin. // TODO: think about moving this somewhere else? this._otherSnapLayers = []; + + // fire drawstart event + Utils._fireEvent(this._map,'pm:drawstart', { + shape: this._shape, + workingLayer: this._layer, + }); + this._setGlobalDrawMode(); }, disable() { // disable drawing mode @@ -110,9 +111,6 @@ Draw.Circle = Draw.extend({ // remove helping layers this._map.removeLayer(this._layerGroup); - // fire drawend event - this._map.fire('pm:drawend', { shape: this._shape }); - this._setGlobalDrawMode(); // toggle the draw button of the Toolbar in case drawing mode got disabled without the button this._map.pm.Toolbar.toggleButton(this.toolbarButtonName, false); @@ -121,6 +119,10 @@ Draw.Circle = Draw.extend({ if (this.options.snappable) { this._cleanupSnapping(); } + + // fire drawend event + Utils._fireEvent(this._map,'pm:drawend', { shape: this._shape }); + this._setGlobalDrawMode(); }, enabled() { return this._enabled; @@ -134,21 +136,35 @@ Draw.Circle = Draw.extend({ }, _syncHintLine() { const latlng = this._centerMarker.getLatLng(); - + const secondLatLng = this._getNewDestinationOfHintMarker(); // set coords for hintline from marker to last vertex of drawin polyline - this._hintline.setLatLngs([latlng, this._hintMarker.getLatLng()]); + this._hintline.setLatLngs([latlng, secondLatLng]); }, _syncCircleRadius() { const A = this._centerMarker.getLatLng(); const B = this._hintMarker.getLatLng(); - const distance = A.distanceTo(B); + let distance; - this._layer.setRadius(distance); + if (this._map.options.crs === L.CRS.Simple) { + distance = this._map.distance(A, B); + } else { + distance = A.distanceTo(B); + } + + if(this.options.minRadiusCircle && distance < this.options.minRadiusCircle) { + this._layer.setRadius(this.options.minRadiusCircle); + }else if(this.options.maxRadiusCircle && distance > this.options.maxRadiusCircle) { + this._layer.setRadius(this.options.maxRadiusCircle); + }else{ + this._layer.setRadius(distance); + } }, _syncHintMarker(e) { // move the cursor marker this._hintMarker.setLatLng(e.latlng); + // calculate the new latlng of marker if radius is out of min/max + this._hintMarker.setLatLng(this._getNewDestinationOfHintMarker()); // if snapping is enabled, do it if (this.options.snappable) { @@ -156,6 +172,8 @@ Draw.Circle = Draw.extend({ fakeDragEvent.target = this._hintMarker; this._handleSnapping(fakeDragEvent); } + + this._handleHintMarkerSnapping(); }, _placeCenterMarker(e) { // assign the coordinate of the click to the hintMarker, that's necessary for @@ -188,7 +206,7 @@ Draw.Circle = Draw.extend({ getTranslation('tooltips.finishCircle') ); - this._layer.fire('pm:centerplaced', { + Utils._fireEvent(this._layer,'pm:centerplaced', { workingLayer: this._layer, latlng, shape: this._shape @@ -205,37 +223,68 @@ Draw.Circle = Draw.extend({ // calc the radius const center = this._centerMarker.getLatLng(); const latlng = this._hintMarker.getLatLng(); - const radius = center.distanceTo(latlng); + + let radius; + + if (this._map.options.crs === L.CRS.Simple) { + radius = this._map.distance(center, latlng); + } else { + radius = center.distanceTo(latlng); + } + + if(this.options.minRadiusCircle && radius < this.options.minRadiusCircle){ + radius = this.options.minRadiusCircle; + }else if(this.options.maxRadiusCircle && radius > this.options.maxRadiusCircle){ + radius = this.options.maxRadiusCircle; + } + const options = Object.assign({}, this.options.pathOptions, { radius }); // create the final circle layer - const circleLayer = L.circle(center, options).addTo(this._map); - this._setShapeForFinishLayer(circleLayer); - this._addDrawnLayerProp(circleLayer); - - // create polygon around the circle border - circleLayer.pm._updateHiddenPolyCircle(); + const circleLayer = L.circle(center, options).addTo(this._map.pm._getContainingLayer()); + this._finishLayer(circleLayer); - // disable drawing - this.disable(); + if(circleLayer.pm) { + // create polygon around the circle border + circleLayer.pm._updateHiddenPolyCircle(); + } // fire the pm:create event and pass shape and layer - this._map.fire('pm:create', { + Utils._fireEvent(this._map,'pm:create', { shape: this._shape, layer: circleLayer, }); - }, - _createMarker(latlng) { - // create the new marker - const marker = new L.Marker(latlng, { - draggable: false, - icon: L.divIcon({ className: 'marker-icon' }), - }); - marker._pmTempLayer = true; - // add it to the map - this._layerGroup.addLayer(marker); + // disable drawing + this.disable(); + if(this.options.continueDrawing){ + this.enable(); + } + }, + _getNewDestinationOfHintMarker(){ + const latlng = this._centerMarker.getLatLng(); + let secondLatLng = this._hintMarker.getLatLng(); + const distance = latlng.distanceTo(secondLatLng); - return marker; + if(this.options.minRadiusCircle && distance < this.options.minRadiusCircle) { + secondLatLng = destinationOnLine(this._map,latlng,secondLatLng,this.options.minRadiusCircle); + }else if(this.options.maxRadiusCircle && distance > this.options.maxRadiusCircle) { + secondLatLng = destinationOnLine(this._map,latlng,secondLatLng,this.options.maxRadiusCircle); + } + return secondLatLng; }, + _handleHintMarkerSnapping(){ + if(this._hintMarker._snapped) { + const latlng = this._centerMarker.getLatLng(); + const secondLatLng = this._hintMarker.getLatLng(); + const distance = latlng.distanceTo(secondLatLng); + if(this.options.minRadiusCircle && distance < this.options.minRadiusCircle) { + this._hintMarker.setLatLng(this._hintMarker._orgLatLng); + } else if(this.options.maxRadiusCircle && distance > this.options.maxRadiusCircle) { + this._hintMarker.setLatLng(this._hintMarker._orgLatLng); + } + } + // calculate the new latlng of marker if the snapped latlng radius is out of min/max + this._hintMarker.setLatLng(this._getNewDestinationOfHintMarker()); + } }); diff --git a/src/js/Draw/L.PM.Draw.CircleMarker.js b/src/js/Draw/L.PM.Draw.CircleMarker.js index a2c4d2b5..98f3fc3f 100644 --- a/src/js/Draw/L.PM.Draw.CircleMarker.js +++ b/src/js/Draw/L.PM.Draw.CircleMarker.js @@ -1,12 +1,15 @@ import Draw from './L.PM.Draw'; -import { getTranslation } from '../helpers'; +import {destinationOnLine, getTranslation } from '../helpers'; +import Utils from "../L.PM.Utils"; Draw.CircleMarker = Draw.Marker.extend({ initialize(map) { this._map = map; this._shape = 'CircleMarker'; this.toolbarButtonName = 'drawCircleMarker'; + // with _layerIsDragging we check if a circlemarker is currently dragged and disable marker creation + this._layerIsDragging = false; }, enable(options) { // TODO: Think about if these options could be passed globally for all @@ -99,18 +102,10 @@ Draw.CircleMarker = Draw.Marker.extend({ } } - // sync hint marker with mouse cursor this._map.on('mousemove', this._syncHintMarker, this); - // fire drawstart event - this._map.fire('pm:drawstart', { - shape: this._shape, - workingLayer: this._layer, - }); - this._setGlobalDrawMode(); - - if (!this.options.editable) { + if (!this.options.editable && this.options.markerEditable) { // enable edit mode for existing markers this._map.eachLayer(layer => { if (this.isRelevantMarker(layer)) { @@ -118,12 +113,24 @@ Draw.CircleMarker = Draw.Marker.extend({ } }); } + + this._layer.bringToBack(); + + // fire drawstart event + Utils._fireEvent(this._map,'pm:drawstart', { + shape: this._shape, + workingLayer: this._layer, + }); + this._setGlobalDrawMode(); }, disable() { // cancel, if drawing mode isn't even enabled if (!this._enabled) { return; } + // change enabled state + this._enabled = false; + // disable when drawing like a Circle if (this.options.editable) { // reset cursor @@ -153,9 +160,6 @@ Draw.CircleMarker = Draw.Marker.extend({ // remove event listener to sync hint marker this._map.off('mousemove', this._syncHintMarker, this); - // fire drawend event - this._map.fire('pm:drawend', { shape: this._shape }); - // toggle the draw button of the Toolbar in case drawing mode got disabled without the button this._map.pm.Toolbar.toggleButton(this.toolbarButtonName, false); @@ -164,8 +168,9 @@ Draw.CircleMarker = Draw.Marker.extend({ this._cleanupSnapping(); } - // change enabled state - this._enabled = false; + // fire drawend event + Utils._fireEvent(this._map,'pm:drawend', { shape: this._shape }); + this._setGlobalDrawMode(); }, _placeCenterMarker(e) { // assign the coordinate of the click to the hintMarker, that's necessary for @@ -198,7 +203,7 @@ Draw.CircleMarker = Draw.Marker.extend({ getTranslation('tooltips.finishCircle') ); - this._layer.fire('pm:centerplaced', { + Utils._fireEvent(this._layer,'pm:centerplaced', { shape: this._shape, workingLayer: this._layer, latlng, @@ -207,9 +212,9 @@ Draw.CircleMarker = Draw.Marker.extend({ }, _syncHintLine() { const latlng = this._centerMarker.getLatLng(); - + const secondLatLng = this._getNewDestinationOfHintMarker(); // set coords for hintline from marker to last vertex of drawin polyline - this._hintline.setLatLngs([latlng, this._hintMarker.getLatLng()]); + this._hintline.setLatLngs([latlng, secondLatLng]); }, _syncCircleRadius() { const A = this._centerMarker.getLatLng(); @@ -217,11 +222,19 @@ Draw.CircleMarker = Draw.Marker.extend({ const distance = this._map.project(A).distanceTo(this._map.project(B)); - this._layer.setRadius(distance); + if(this.options.minRadiusCircleMarker && distance < this.options.minRadiusCircleMarker) { + this._layer.setRadius(this.options.minRadiusCircleMarker); + }else if(this.options.maxRadiusCircleMarker && distance > this.options.maxRadiusCircleMarker) { + this._layer.setRadius(this.options.maxRadiusCircleMarker); + }else{ + this._layer.setRadius(distance); + } }, _syncHintMarker(e) { // move the cursor marker this._hintMarker.setLatLng(e.latlng); + // calculate the new latlng of marker if radius is out of min/max + this._hintMarker.setLatLng(this._getNewDestinationOfHintMarker()); // if snapping is enabled, do it if (this.options.snappable) { @@ -229,12 +242,15 @@ Draw.CircleMarker = Draw.Marker.extend({ fakeDragEvent.target = this._hintMarker; this._handleSnapping(fakeDragEvent); } + + this._handleHintMarkerSnapping(); }, isRelevantMarker(layer) { return layer instanceof L.CircleMarker && !(layer instanceof L.Circle) && layer.pm && !layer._pmTempLayer; }, _createMarker(e) { - if (!e.latlng) { + // with _layerIsDragging we check if a circlemarker is currently dragged + if (!e.latlng || this._layerIsDragging) { return; } @@ -249,22 +265,27 @@ Draw.CircleMarker = Draw.Marker.extend({ // create marker const marker = L.circleMarker(latlng, this.options.pathOptions); - this._setShapeForFinishLayer(marker); - this._addDrawnLayerProp(marker); + this._finishLayer(marker); // add marker to the map - marker.addTo(this._map); + marker.addTo(this._map.pm._getContainingLayer()); - // enable editing for the marker - marker.pm.enable(); + if(marker.pm && this.options.markerEditable) { + // enable editing for the marker + marker.pm.enable(); + } // fire the pm:create event and pass shape and marker - this._map.fire('pm:create', { + Utils._fireEvent(this._map,'pm:create', { shape: this._shape, marker, // DEPRECATED layer: marker, }); this._cleanupSnapping(); + + if(!this.options.continueDrawing){ + this.disable(); + } }, _finishShape(e) { // assign the coordinate of the click to the hintMarker, that's necessary for @@ -276,23 +297,72 @@ Draw.CircleMarker = Draw.Marker.extend({ // calc the radius const center = this._centerMarker.getLatLng(); const latlng = this._hintMarker.getLatLng(); - const radius = this._map.project(center).distanceTo(this._map.project(latlng)); + let radius = this._map.project(center).distanceTo(this._map.project(latlng)); + + + if(this.options.editable) { + if (this.options.minRadiusCircleMarker && radius < this.options.minRadiusCircleMarker) { + radius = this.options.minRadiusCircleMarker; + } else if (this.options.maxRadiusCircleMarker && radius > this.options.maxRadiusCircleMarker) { + radius = this.options.maxRadiusCircleMarker; + } + } + const options = Object.assign({}, this.options.pathOptions, { radius }); // create the final circle layer - const circleLayer = L.circleMarker(center, options).addTo(this._map); - this._setShapeForFinishLayer(circleLayer); - this._addDrawnLayerProp(circleLayer); - // create polygon around the circle border - circleLayer.pm._updateHiddenPolyCircle(); - - // disable drawing - this.disable(); + const circleLayer = L.circleMarker(center, options).addTo(this._map.pm._getContainingLayer()); + this._finishLayer(circleLayer); + if(circleLayer.pm) { + // create polygon around the circle border + circleLayer.pm._updateHiddenPolyCircle(); + } // fire the pm:create event and pass shape and layer - this._map.fire('pm:create', { + Utils._fireEvent(this._map,'pm:create', { shape: this._shape, layer: circleLayer, }); + + // disable drawing + this.disable(); + if(this.options.continueDrawing){ + this.enable(); + } + }, + _getNewDestinationOfHintMarker(){ + let secondLatLng = this._hintMarker.getLatLng(); + if(this.options.editable) { + const latlng = this._centerMarker.getLatLng(); + const distance = this._map.project(latlng).distanceTo(this._map.project(secondLatLng)); + if (this.options.minRadiusCircleMarker && distance < this.options.minRadiusCircleMarker) { + secondLatLng = destinationOnLine(this._map, latlng, secondLatLng, this._pxRadiusToMeter(this.options.minRadiusCircleMarker)); + } else if (this.options.maxRadiusCircleMarker && distance > this.options.maxRadiusCircleMarker) { + secondLatLng = destinationOnLine(this._map, latlng, secondLatLng, this._pxRadiusToMeter(this.options.maxRadiusCircleMarker)); + } + } + return secondLatLng; + }, + _handleHintMarkerSnapping(){ + if(this.options.editable) { + if (this._hintMarker._snapped) { + const latlng = this._centerMarker.getLatLng(); + const secondLatLng = this._hintMarker.getLatLng(); + const distance = this._map.project(latlng).distanceTo(this._map.project(secondLatLng)); + if (this.options.minRadiusCircleMarker && distance < this.options.minRadiusCircleMarker) { + this._hintMarker.setLatLng(this._hintMarker._orgLatLng); + } else if (this.options.maxRadiusCircleMarker && distance > this.options.maxRadiusCircleMarker) { + this._hintMarker.setLatLng(this._hintMarker._orgLatLng); + } + } + // calculate the new latlng of marker if the snapped latlng radius is out of min/max + this._hintMarker.setLatLng(this._getNewDestinationOfHintMarker()); + } }, + _pxRadiusToMeter(radius){ + const center = this._centerMarker.getLatLng(); + const pointA = this._map.project(center); + const pointB = L.point(pointA.x + radius, pointA.y); + return this._map.unproject(pointB).distanceTo(center); + } }); diff --git a/src/js/Draw/L.PM.Draw.Cut.js b/src/js/Draw/L.PM.Draw.Cut.js index 9b683999..5225cf9f 100644 --- a/src/js/Draw/L.PM.Draw.Cut.js +++ b/src/js/Draw/L.PM.Draw.Cut.js @@ -1,3 +1,8 @@ +import lineIntersect from "@turf/line-intersect"; +import lineSplit from "@turf/line-split"; +import booleanContains from "@turf/boolean-contains"; +import get from "lodash/get"; +import Utils from "../L.PM.Utils"; import Draw from './L.PM.Draw'; import {difference, intersect} from "../helpers/turfHelper"; @@ -7,23 +12,83 @@ Draw.Cut = Draw.Polygon.extend({ this._shape = 'Cut'; this.toolbarButtonName = 'cutPolygon'; }, - _cut(layer) { + _finishShape() { + this._editedLayers = []; + // if self intersection is not allowed, do not finish the shape! + if (!this.options.allowSelfIntersection) { + // Check if polygon intersects when is completed and the line between the last and the first point is drawn + this._handleSelfIntersection(true, this._layer.getLatLngs()[0]); + + if (this._doesSelfIntersect) { + return; + } + } + + const coords = this._layer.getLatLngs(); + const polygonLayer = L.polygon(coords, this.options.pathOptions); + // readout information about the latlngs like snapping points + polygonLayer._latlngInfos = this._layer._latlngInfo; + this.cut(polygonLayer); + + // clean up snapping states + this._cleanupSnapping(); + + // remove the first vertex from "other snapping layers" + this._otherSnapLayers.splice(this._tempSnapLayerIndex, 1); + delete this._tempSnapLayerIndex; + + this._editedLayers.forEach(({layer, originalLayer}) =>{ + // fire pm:cut on the cutted layer + Utils._fireEvent(originalLayer,'pm:cut', { + shape: this._shape, + layer, + originalLayer, + }); + + // fire pm:cut on the map + Utils._fireEvent(this._map,'pm:cut', { + shape: this._shape, + layer, + originalLayer, + }); + + // fire edit event after cut + Utils._fireEvent(originalLayer,'pm:edit', { layer: originalLayer, shape: originalLayer.pm.getShape()}); + }); + this._editedLayers = []; + + // disable drawing + this.disable(); + if(this.options.continueDrawing){ + this.enable(); + } + }, + cut(layer) { const all = this._map._layers; + // contains information about snapping points + const _latlngInfos = layer._latlngInfos || []; // find all layers that intersect with `layer`, the just drawn cutting layer const layers = Object.keys(all) - // convert object to array + // convert object to array .map(l => all[l]) // only layers handled by leaflet-geoman .filter(l => l.pm) - // only polygons - .filter(l => l instanceof L.Polygon) + // only polyline instances + .filter(l => l instanceof L.Polyline) // exclude the drawn one .filter(l => l !== layer) + .filter(l => !this._layerGroup.hasLayer(l)) // only layers with intersections .filter(l => { try { + const lineInter = !!lineIntersect(layer.toGeoJSON(15), l.toGeoJSON(15)).features.length > 0; + + if(lineInter){ + return lineInter; + } return !!intersect(layer.toGeoJSON(15), l.toGeoJSON(15)); + } catch (e) { /* eslint-disable-next-line no-console */ console.error('You cant cut polygons with self-intersections'); @@ -33,15 +98,42 @@ Draw.Cut = Draw.Polygon.extend({ // loop through all layers that intersect with the drawn (cutting) layer layers.forEach(l => { + let newLayer; + if(l instanceof L.Polygon) { // Also for L.Rectangle + // easiest way to clone the complete latlngs without reference + newLayer = L.polygon(l.getLatLngs()); + const coords = newLayer.getLatLngs(); + + // snapping points added to the layer, so borders are cutted correct + _latlngInfos.forEach((info)=>{ + if(info && info.snapInfo) { + const {latlng} = info; + // get closest layer ( == input layer) with the closest segment to the intersection point + const closest = this._calcClosestLayer(latlng, [newLayer]); + if (closest && closest.segment && closest.distance < this.options.snapDistance) { + const {segment} = closest; + if (segment && segment.length === 2) { + const {indexPath, parentPath, newIndex} = Utils._getIndexFromSegment(coords, segment); + // define the coordsRing that is edited + const coordsRing = indexPath.length > 1 ? get(coords, parentPath) : coords; + coordsRing.splice(newIndex, 0, latlng); + } + } + } + }); + }else{ // L.Polyline + newLayer = l; + } + // find layer difference - const diff = difference(l.toGeoJSON(15), layer.toGeoJSON(15)); + const diff = this._cutLayer(layer,newLayer); // the resulting layer after the cut let resultLayer = L.geoJSON(diff, l.options); if (resultLayer.getLayers().length === 1) { [resultLayer] = resultLayer.getLayers(); // prevent that a unnecessary layergroup is created } - const resultingLayer = resultLayer.addTo(this._map); + const resultingLayer = resultLayer.addTo(this._map.pm._getContainingLayer()); // give the new layer the original options resultingLayer.pm.enable(this.options); @@ -53,7 +145,9 @@ Draw.Cut = Draw.Polygon.extend({ // remove old layer and cutting layer l.remove(); + l.removeFrom(this._map.pm._getContainingLayer()); layer.remove(); + layer.removeFrom(this._map.pm._getContainingLayer()); // Remove it only if it is a layergroup. It can be only not a layergroup if a layer exists if (resultingLayer.getLayers && resultingLayer.getLayers().length === 0) { @@ -68,50 +162,29 @@ Draw.Cut = Draw.Polygon.extend({ }); }); - }, - _finishShape() { - this._editedLayers = []; - // if self intersection is not allowed, do not finish the shape! - if (!this.options.allowSelfIntersection) { - this._handleSelfIntersection(false); - if (this._doesSelfIntersect) { - return; + }, + _cutLayer(layer,l){ + const fg = L.geoJSON(); + let diff; + // cut + if (l instanceof L.Polygon) { + // find layer difference + diff = difference(l.toGeoJSON(15), layer.toGeoJSON(15)); + } else { + // get splitted line to look which line part is coverd by the cut polygon + const lineDiff = lineSplit(l.toGeoJSON(15), layer.toGeoJSON(15)); + if (!lineDiff) { + return null; } - } - - const coords = this._layer.getLatLngs(); - const polygonLayer = L.polygon(coords, this.options.pathOptions); - this._cut(polygonLayer); - - // disable drawing - this.disable(); - - // clean up snapping states - this._cleanupSnapping(); - - // remove the first vertex from "other snapping layers" - this._otherSnapLayers.splice(this._tempSnapLayerIndex, 1); - delete this._tempSnapLayerIndex; - - this._editedLayers.forEach(({layer, originalLayer}) =>{ - // fire pm:cut on the cutted layer - originalLayer.fire('pm:cut', { - shape: this._shape, - layer, - originalLayer, - }); - - // fire pm:cut on the map - this._map.fire('pm:cut', { - shape: this._shape, - layer, - originalLayer, + L.geoJSON(lineDiff).getLayers().forEach((lay) => { + // add only parts to the map, they are not covered by the cut polygon + if (!booleanContains(layer.toGeoJSON(15), lay.toGeoJSON(15))) { + lay.addTo(fg); + } }); - - // fire edit event after cut - originalLayer.fire('pm:edit', { layer: originalLayer, shape: originalLayer.pm.getShape()}); - }); - this._editedLayers = []; + diff = fg.toGeoJSON(15); + } + return diff; }, }); diff --git a/src/js/Draw/L.PM.Draw.Line.js b/src/js/Draw/L.PM.Draw.Line.js index fa852f54..2f6445d3 100644 --- a/src/js/Draw/L.PM.Draw.Line.js +++ b/src/js/Draw/L.PM.Draw.Line.js @@ -1,5 +1,6 @@ import kinks from '@turf/kinks'; import Draw from './L.PM.Draw'; +import Utils from "../L.PM.Utils"; import { getTranslation } from '../helpers'; @@ -89,19 +90,20 @@ Draw.Line = Draw.extend({ // sync the hintline with hint marker this._hintMarker.on('move', this._syncHintLine, this); - // fire drawstart event - this._map.fire('pm:drawstart', { - shape: this._shape, - workingLayer: this._layer, - }); - this._setGlobalDrawMode(); - // toggle the draw button of the Toolbar in case drawing mode got enabled without the button this._map.pm.Toolbar.toggleButton(this.toolbarButtonName, true); // an array used in the snapping mixin. // TODO: think about moving this somewhere else? this._otherSnapLayers = []; + + + // fire drawstart event + Utils._fireEvent(this._map,'pm:drawstart', { + shape: this._shape, + workingLayer: this._layer, + }); + this._setGlobalDrawMode(); }, disable() { // disable draw mode @@ -130,10 +132,6 @@ Draw.Line = Draw.extend({ // remove layer this._map.removeLayer(this._layerGroup); - // fire drawend event - this._map.fire('pm:drawend', { shape: this._shape }); - this._setGlobalDrawMode(); - // toggle the draw button of the Toolbar in case drawing mode got disabled without the button this._map.pm.Toolbar.toggleButton(this.toolbarButtonName, false); @@ -141,6 +139,11 @@ Draw.Line = Draw.extend({ if (this.options.snappable) { this._cleanupSnapping(); } + + // fire drawend event + Utils._fireEvent(this._map,'pm:drawend', { shape: this._shape }); + this._setGlobalDrawMode(); + }, enabled() { return this._enabled; @@ -152,11 +155,6 @@ Draw.Line = Draw.extend({ this.enable(options); } }, - hasSelfIntersection() { - // check for self intersection of the layer and return true/false - const selfIntersection = kinks(this._layer.toGeoJSON(15)); - return selfIntersection.features.length > 0; - }, _syncHintLine() { const polyPoints = this._layer.getLatLngs(); @@ -186,6 +184,11 @@ Draw.Line = Draw.extend({ this._handleSelfIntersection(true, e.latlng); } }, + hasSelfIntersection() { + // check for self intersection of the layer and return true/false + const selfIntersection = kinks(this._layer.toGeoJSON(15)); + return selfIntersection.features.length > 0; + }, _handleSelfIntersection(addVertex, latlng) { // ok we need to check the self intersection here // problem: during draw, the marker on the cursor is not yet part @@ -219,33 +222,6 @@ Draw.Line = Draw.extend({ this._hintline.setStyle(this.options.hintlineStyle); } }, - _removeLastVertex() { - // remove last coords - const coords = this._layer.getLatLngs(); - const removedCoord = coords.pop(); - - // if all coords are gone, cancel drawing - if (coords.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 that marker - this._layerGroup.removeLayer(marker); - - // update layer with new coords - this._layer.setLatLngs(coords); - - // sync the hintline again - this._syncHintLine(); - }, _createVertex(e) { // don't create a vertex if we have a selfIntersection and it is not allowed if (!this.options.allowSelfIntersection) { @@ -279,18 +255,51 @@ Draw.Line = Draw.extend({ // is this the first point? const first = this._layer.getLatLngs().length === 0; + this._layer._latlngInfo = this._layer._latlngInfo || []; + this._layer._latlngInfo.push({ + latlng, + snapInfo: this._hintMarker._snapInfo + }); + this._layer.addLatLng(latlng); const newMarker = this._createMarker(latlng, first); this._hintline.setLatLngs([latlng, latlng]); - this._layer.fire('pm:vertexadded', { + Utils._fireEvent(this._layer,'pm:vertexadded', { shape: this._shape, workingLayer: this._layer, marker: newMarker, latlng, }); }, + _removeLastVertex() { + // remove last coords + const coords = this._layer.getLatLngs(); + const removedCoord = coords.pop(); + + // if all coords are gone, cancel drawing + if (coords.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 that marker + this._layerGroup.removeLayer(marker); + + // update layer with new coords + this._layer.setLatLngs(coords); + + // sync the hintline again + this._syncHintLine(); + }, _finishShape() { // if self intersection is not allowed, do not finish the shape! if (!this.options.allowSelfIntersection) { @@ -311,16 +320,12 @@ Draw.Line = Draw.extend({ // create the leaflet shape and add it to the map const polylineLayer = L.polyline(coords, this.options.pathOptions).addTo( - this._map + this._map.pm._getContainingLayer() ); - this._setShapeForFinishLayer(polylineLayer); - this._addDrawnLayerProp(polylineLayer); - - // disable drawing - this.disable(); + this._finishLayer(polylineLayer); // fire the pm:create event and pass shape and layer - this._map.fire('pm:create', { + Utils._fireEvent(this._map,'pm:create', { shape: this._shape, layer: polylineLayer, }); @@ -328,6 +333,12 @@ Draw.Line = Draw.extend({ if (this.options.snappable) { this._cleanupSnapping(); } + + // disable drawing + this.disable(); + if(this.options.continueDrawing){ + this.enable(); + } }, _createMarker(latlng, first) { // create the new marker diff --git a/src/js/Draw/L.PM.Draw.Marker.js b/src/js/Draw/L.PM.Draw.Marker.js index b2cd8f99..d718e01d 100644 --- a/src/js/Draw/L.PM.Draw.Marker.js +++ b/src/js/Draw/L.PM.Draw.Marker.js @@ -1,6 +1,6 @@ import Draw from './L.PM.Draw'; - import { getTranslation } from '../helpers'; +import Utils from "../L.PM.Utils"; Draw.Marker = Draw.extend({ initialize(map) { @@ -46,19 +46,22 @@ Draw.Marker = Draw.extend({ // sync hint marker with mouse cursor this._map.on('mousemove', this._syncHintMarker, this); + + // enable edit mode for existing markers + if(this.options.markerEditable) { + this._map.eachLayer(layer => { + if (this.isRelevantMarker(layer)) { + layer.pm.enable(); + } + }); + } + // fire drawstart event - this._map.fire('pm:drawstart', { + Utils._fireEvent(this._map,'pm:drawstart', { shape: this._shape, workingLayer: this._layer, }); this._setGlobalDrawMode(); - - // enable edit mode for existing markers - this._map.eachLayer(layer => { - if (this.isRelevantMarker(layer)) { - layer.pm.enable(); - } - }); }, disable() { // cancel, if drawing mode isn't even enabled @@ -85,9 +88,6 @@ Draw.Marker = Draw.extend({ } }); - // fire drawend event - this._map.fire('pm:drawend', { shape: this._shape }); - this._setGlobalDrawMode(); // toggle the draw button of the Toolbar in case drawing mode got disabled without the button this._map.pm.Toolbar.toggleButton(this.toolbarButtonName, false); @@ -97,9 +97,9 @@ Draw.Marker = Draw.extend({ this._cleanupSnapping(); } - }, - isRelevantMarker(layer) { - return layer instanceof L.Marker && layer.pm && !layer._pmTempLayer; + // fire drawend event + Utils._fireEvent(this._map,'pm:drawend', { shape: this._shape }); + this._setGlobalDrawMode(); }, enabled() { return this._enabled; @@ -111,6 +111,20 @@ Draw.Marker = Draw.extend({ this.enable(options); } }, + isRelevantMarker(layer) { + return layer instanceof L.Marker && layer.pm && !layer._pmTempLayer; + }, + _syncHintMarker(e) { + // move the cursor marker + this._hintMarker.setLatLng(e.latlng); + + // if snapping is enabled, do it + if (this.options.snappable) { + const fakeDragEvent = e; + fakeDragEvent.target = this._hintMarker; + this._handleSnapping(fakeDragEvent); + } + }, _createMarker(e) { if (!e.latlng) { return; @@ -127,33 +141,36 @@ Draw.Marker = Draw.extend({ // create marker const marker = new L.Marker(latlng, this.options.markerStyle); - this._setShapeForFinishLayer(marker); - this._addDrawnLayerProp(marker); + this._finishLayer(marker); + + if(!marker.pm){ + marker.options.draggable = false; + } // add marker to the map - marker.addTo(this._map); + marker.addTo(this._map.pm._getContainingLayer()); - // enable editing for the marker - marker.pm.enable(); + + if(marker.pm && this.options.markerEditable) { + // enable editing for the marker + marker.pm.enable(); + }else{ + if(marker.dragging) { + marker.dragging.disable(); + } + } // fire the pm:create event and pass shape and marker - this._map.fire('pm:create', { + Utils._fireEvent(this._map,'pm:create', { shape: this._shape, marker, // DEPRECATED layer: marker, }); this._cleanupSnapping(); - }, - _syncHintMarker(e) { - // move the cursor marker - this._hintMarker.setLatLng(e.latlng); - // if snapping is enabled, do it - if (this.options.snappable) { - const fakeDragEvent = e; - fakeDragEvent.target = this._hintMarker; - this._handleSnapping(fakeDragEvent); + if(!this.options.continueDrawing){ + this.disable(); } }, }); diff --git a/src/js/Draw/L.PM.Draw.Polygon.js b/src/js/Draw/L.PM.Draw.Polygon.js index 1320d28a..6cb40490 100644 --- a/src/js/Draw/L.PM.Draw.Polygon.js +++ b/src/js/Draw/L.PM.Draw.Polygon.js @@ -1,4 +1,5 @@ import Draw from './L.PM.Draw'; +import Utils from "../L.PM.Utils"; import { getTranslation } from '../helpers'; @@ -8,47 +9,6 @@ Draw.Polygon = Draw.Line.extend({ this._shape = 'Polygon'; this.toolbarButtonName = 'drawPolygon'; }, - _finishShape(e) { - // if self intersection is not allowed, do not finish the shape! - if (!this.options.allowSelfIntersection) { - // Check if polygon intersects when is completed and the line between the last and the first point is drawn - this._handleSelfIntersection(true, this._layer.getLatLngs()[0]); - - if (this._doesSelfIntersect) { - 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).addTo( - this._map - ); - this._setShapeForFinishLayer(polygonLayer); - this._addDrawnLayerProp(polygonLayer); - - // disable drawing - this.disable(); - - // fire the pm:create event and pass shape and layer - this._map.fire('pm:create', { - shape: this._shape, - layer: polygonLayer, - }); - - // clean up snapping states - this._cleanupSnapping(); - - // remove the first vertex from "other snapping layers" - this._otherSnapLayers.splice(this._tempSnapLayerIndex, 1); - delete this._tempSnapLayerIndex; - }, _createMarker(latlng, first) { // create the new marker const marker = new L.Marker(latlng, { @@ -93,4 +53,47 @@ Draw.Polygon = Draw.Line.extend({ return marker; }, + _finishShape(e) { + // if self intersection is not allowed, do not finish the shape! + if (!this.options.allowSelfIntersection) { + // Check if polygon intersects when is completed and the line between the last and the first point is drawn + this._handleSelfIntersection(true, this._layer.getLatLngs()[0]); + + if (this._doesSelfIntersect) { + 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).addTo( + this._map.pm._getContainingLayer() + ); + this._finishLayer(polygonLayer); + + // fire the pm:create event and pass shape and layer + Utils._fireEvent(this._map,'pm:create', { + shape: this._shape, + layer: polygonLayer, + }); + + // clean up snapping states + this._cleanupSnapping(); + + // remove the first vertex from "other snapping layers" + this._otherSnapLayers.splice(this._tempSnapLayerIndex, 1); + delete this._tempSnapLayerIndex; + + // disable drawing + this.disable(); + if(this.options.continueDrawing){ + this.enable(); + } + }, }); diff --git a/src/js/Draw/L.PM.Draw.Rectangle.js b/src/js/Draw/L.PM.Draw.Rectangle.js index f32dab6e..e8e52696 100644 --- a/src/js/Draw/L.PM.Draw.Rectangle.js +++ b/src/js/Draw/L.PM.Draw.Rectangle.js @@ -1,4 +1,5 @@ import Draw from './L.PM.Draw'; +import Utils from "../L.PM.Utils"; import { getTranslation } from '../helpers'; @@ -86,19 +87,20 @@ Draw.Rectangle = Draw.extend({ // sync hint marker with mouse cursor this._map.on('mousemove', this._syncHintMarker, this); - // fire drawstart event - this._map.fire('pm:drawstart', { - shape: this._shape, - workingLayer: this._layer, - }); - this._setGlobalDrawMode(); - // toggle the draw button of the Toolbar in case drawing mode got enabled without the button this._map.pm.Toolbar.toggleButton(this.toolbarButtonName, true); // an array used in the snapping mixin. // TODO: think about moving this somewhere else? this._otherSnapLayers = []; + + // fire drawstart event + Utils._fireEvent(this._map,'pm:drawstart', { + shape: this._shape, + workingLayer: this._layer, + }); + this._setGlobalDrawMode(); + }, disable() { // disable drawing mode @@ -121,10 +123,6 @@ Draw.Rectangle = Draw.extend({ // remove helping layers this._map.removeLayer(this._layerGroup); - // fire drawend event - this._map.fire('pm:drawend', { shape: this._shape }); - this._setGlobalDrawMode(); - // toggle the draw button of the Toolbar in case drawing mode got disabled without the button this._map.pm.Toolbar.toggleButton(this.toolbarButtonName, false); @@ -132,6 +130,10 @@ Draw.Rectangle = Draw.extend({ if (this.options.snappable) { this._cleanupSnapping(); } + // fire drawend event + Utils._fireEvent(this._map,'pm:drawend', { shape: this._shape }); + this._setGlobalDrawMode(); + }, enabled() { return this._enabled; @@ -224,6 +226,16 @@ Draw.Rectangle = Draw.extend({ }); } }, + _findCorners() { + const corners = this._layer.getBounds(); + + const northwest = corners.getNorthWest(); + const northeast = corners.getNorthEast(); + const southeast = corners.getSouthEast(); + const southwest = corners.getSouthWest(); + + return [northwest, northeast, southeast, southwest]; + }, _finishShape(e) { // assign the coordinate of the click to the hintMarker, that's necessary for // mobile where the marker can't follow a cursor @@ -239,28 +251,20 @@ Draw.Rectangle = Draw.extend({ // create the final rectangle layer, based on opposite corners A & B const rectangleLayer = L.rectangle([A, B], this.options.pathOptions).addTo( - this._map + this._map.pm._getContainingLayer() ); - this._setShapeForFinishLayer(rectangleLayer); - this._addDrawnLayerProp(rectangleLayer); - - // disable drawing - this.disable(); + this._finishLayer(rectangleLayer); // fire the pm:create event and pass shape and layer - this._map.fire('pm:create', { + Utils._fireEvent(this._map,'pm:create', { shape: this._shape, layer: rectangleLayer, }); - }, - _findCorners() { - const corners = this._layer.getBounds(); - const northwest = corners.getNorthWest(); - const northeast = corners.getNorthEast(); - const southeast = corners.getSouthEast(); - const southwest = corners.getSouthWest(); - - return [northwest, northeast, southeast, southwest]; + // disable drawing + this.disable(); + if(this.options.continueDrawing){ + this.enable(); + } }, }); diff --git a/src/js/Draw/L.PM.Draw.js b/src/js/Draw/L.PM.Draw.js index eabdd1bf..0966f6c0 100644 --- a/src/js/Draw/L.PM.Draw.js +++ b/src/js/Draw/L.PM.Draw.js @@ -1,4 +1,5 @@ import SnapMixin from '../Mixins/Snapping'; +import Utils from "../L.PM.Utils"; const Draw = L.Class.extend({ includes: [SnapMixin], @@ -17,12 +18,21 @@ const Draw = L.Class.extend({ }, markerStyle: { draggable: true, + icon: L.icon() }, + markerEditable: true, + continueDrawing: false, }, setOptions(options) { L.Util.setOptions(this, options); }, initialize(map) { + // Overwriting the default tooltipAnchor of the default Marker Icon, because the tooltip functionality was updated but not the anchor in the Icon + // Issue https://github.com/Leaflet/Leaflet/issues/7302 - Leaflet v1.7.1 + const defaultIcon = new L.Icon.Default(); + defaultIcon.options.tooltipAnchor = [0,0]; + this.options.markerStyle.icon = defaultIcon; + // save the map this._map = map; @@ -33,6 +43,9 @@ const Draw = L.Class.extend({ this.shapes.forEach(shape => { this[shape] = new L.PM.Draw[shape](this._map); }); + + this.Marker.setOptions({continueDrawing: true}); + this.CircleMarker.setOptions({continueDrawing: true}); }, setPathOptions(options) { this.options.pathOptions = options; @@ -83,17 +96,28 @@ const Draw = L.Class.extend({ _setGlobalDrawMode() { // extended to all PM.Draw shapes if (this._shape === "Cut") { - this._map.fire('pm:globalcutmodetoggled', { + Utils._fireEvent(this._map,'pm:globalcutmodetoggled', { enabled: !!this._enabled, map: this._map, }); } else { - this._map.fire('pm:globaldrawmodetoggled', { + Utils._fireEvent(this._map,'pm:globaldrawmodetoggled', { enabled: this._enabled, shape: this._shape, map: this._map, }); } + + const layers = Utils.findLayers(this._map); + if (this._enabled) { + layers.forEach((layer) => { + Utils.disablePopup(layer); + }) + } else { + layers.forEach((layer) => { + Utils.enablePopup(layer); + }) + } }, createNewDrawInstance(name, jsClass) { @@ -142,12 +166,18 @@ const Draw = L.Class.extend({ } return this[name] ? this[name]._shape : name; }, + _finishLayer(layer){ + // add the pm options from drawing to the new layer (edit) + layer.pm.setOptions(this.options); + // set the shape (can be a custom shape) + if(layer.pm) { + layer.pm._shape = this._shape; + } + this._addDrawnLayerProp(layer); + }, _addDrawnLayerProp(layer){ layer._drawnByGeoman = true; }, - _setShapeForFinishLayer(layer){ - layer.pm._shape = this._shape; - } }); export default Draw; diff --git a/src/js/Edit/L.PM.Edit.Circle.js b/src/js/Edit/L.PM.Edit.Circle.js index 0fc9988c..1038617d 100644 --- a/src/js/Edit/L.PM.Edit.Circle.js +++ b/src/js/Edit/L.PM.Edit.Circle.js @@ -1,5 +1,6 @@ import Edit from './L.PM.Edit'; import Utils from "../L.PM.Utils"; +import {destinationOnLine} from "../helpers"; Edit.Circle = Edit.extend({ _shape: 'Circle', @@ -9,37 +10,6 @@ Edit.Circle = Edit.extend({ // create polygon around the circle border this._updateHiddenPolyCircle(); }, - applyOptions() { - if (this.options.snappable) { - this._initSnappableMarkers(); - // sync the hintline with hint marker - this._outerMarker.on('move', this._syncHintLine, this); - this._outerMarker.on('move', this._syncCircleRadius, this); - this._centerMarker.on('move', this._moveCircle, this); - } else { - this._disableSnapping(); - } - }, - _disableSnapping() { - this._markers.forEach(marker => { - marker.off('move', this._syncHintLine, this); - marker.off('move', this._syncCircleRadius, this); - marker.off('drag', this._handleSnapping, this); - marker.off('dragend', this._cleanupSnapping, this); - }); - - this._layer.off('pm:dragstart', this._unsnap, this); - }, - toggleEdit(options) { - if (!this.enabled()) { - this.enable(options); - } else { - this.disable(); - } - }, - enabled() { - return this._enabled; - }, enable(options) { L.Util.setOptions(this, options); @@ -59,8 +29,6 @@ Edit.Circle = Edit.extend({ this.applyOptions(); - this._layer.fire('pm:enable', { layer: this._layer, shape: this.getShape() }); - // if polygon gets removed from map, disable edit mode this._layer.on('remove', e => { this.disable(e.target); @@ -68,6 +36,7 @@ Edit.Circle = Edit.extend({ // create polygon around the circle border this._updateHiddenPolyCircle(); + Utils._fireEvent(this._layer,'pm:enable', { layer: this._layer, shape: this.getShape() }); }, disable(layer = this._layer) { // if it's not enabled, it doesn't need to be disabled @@ -83,6 +52,7 @@ Edit.Circle = Edit.extend({ this._centerMarker.off('dragstart', this._fireDragStart, this); this._centerMarker.off('drag', this._fireDrag, this); this._centerMarker.off('dragend', this._fireDragEnd, this); + this._outerMarker.off('drag',this._handleOuterMarkerSnapping, this); layer.pm._enabled = false; layer.pm._helperLayers.clearLayers(); @@ -94,15 +64,25 @@ Edit.Circle = Edit.extend({ const el = layer._path ? layer._path : this._layer._renderer._container; L.DomUtil.removeClass(el, 'leaflet-pm-draggable'); - this._layer.fire('pm:disable', { layer: this._layer, shape: this.getShape() }); if (this._layerEdited) { - this._layer.fire('pm:update', { layer: this._layer, shape: this.getShape() }); + Utils._fireEvent(this._layer,'pm:update', { layer: this._layer, shape: this.getShape() }); } this._layerEdited = false; + Utils._fireEvent(this._layer,'pm:disable', { layer: this._layer, shape: this.getShape() }); return true; }, + enabled() { + return this._enabled; + }, + toggleEdit(options) { + if (!this.enabled()) { + this.enable(options); + } else { + this.disable(); + } + }, _initMarkers() { const map = this._map; @@ -126,72 +106,19 @@ Edit.Circle = Edit.extend({ this._outerMarker = this._createOuterMarker(outer); this._markers = [this._centerMarker, this._outerMarker]; this._createHintLine(this._centerMarker, this._outerMarker); - - - }, - _getLatLngOnCircle(center, radius) { - const pointA = this._map.project(center); - const pointB = L.point(pointA.x + radius, pointA.y); - - return this._map.unproject(pointB); }, - _resizeCircle() { - this._syncHintLine(); - this._syncCircleRadius(); - }, - _moveCircle(e) { - const center = e.latlng; - this._layer.setLatLng(center); - - const radius = this._layer._radius; - - const outer = this._getLatLngOnCircle(center, radius); - this._outerMarker.setLatLng(outer); - this._syncHintLine(); - - this._updateHiddenPolyCircle(); - - this._layer.fire('pm:centerplaced', { - layer: this._layer, - latlng: center, - shape: this.getShape() - }); - }, - _onMarkerDragStart(e) { - this._layer.fire('pm:markerdragstart', { - layer: this._layer, - markerEvent: e, - shape: this.getShape(), - indexPath: undefined - }); - }, - _onMarkerDragEnd(e) { - // fire edit event - this._fireEdit(); - - // fire markerdragend event - this._layer.fire('pm:markerdragend', { - layer: this._layer, - markerEvent: e, - shape: this.getShape(), - indexPath: undefined - }); - }, - _syncCircleRadius() { - const A = this._centerMarker.getLatLng(); - const B = this._outerMarker.getLatLng(); - - const distance = A.distanceTo(B); - - this._layer.setRadius(distance); - this._updateHiddenPolyCircle(); - }, - _syncHintLine() { - const A = this._centerMarker.getLatLng(); - const B = this._outerMarker.getLatLng(); - - // set coords for hintline from marker to last vertex of drawin polyline - this._hintline.setLatLngs([A, B]); + applyOptions() { + if (this.options.snappable) { + this._initSnappableMarkers(); + // update marker latlng when snapped latlng radius is out of min/max + this._outerMarker.on('drag',this._handleOuterMarkerSnapping, this); + // sync the hintline with hint marker + this._outerMarker.on('move', this._syncHintLine, this); + this._outerMarker.on('move', this._syncCircleRadius, this); + this._centerMarker.on('move', this._moveCircle, this); + } else { + this._disableSnapping(); + } }, _createHintLine(markerA, markerB) { const A = markerA.getLatLng(); @@ -232,25 +159,110 @@ Edit.Circle = Edit.extend({ marker._pmTempLayer = true; marker.on('dragstart', this._onMarkerDragStart, this); + marker.on('drag', this._onMarkerDrag, this); marker.on('dragend', this._onMarkerDragEnd, this); this._helperLayers.addLayer(marker); return marker; }, + _resizeCircle() { + this._outerMarker.setLatLng(this._getNewDestinationOfOuterMarker()); + this._syncHintLine(); + this._syncCircleRadius(); + }, + _moveCircle(e) { + const center = e.latlng; + this._layer.setLatLng(center); + + const radius = this._layer._radius; + + const outer = this._getLatLngOnCircle(center, radius); + this._outerMarker.setLatLng(outer); + this._syncHintLine(); + + this._updateHiddenPolyCircle(); + + Utils._fireEvent(this._layer,'pm:centerplaced', { + layer: this._layer, + latlng: center, + shape: this.getShape() + }); + }, + _syncCircleRadius() { + const A = this._centerMarker.getLatLng(); + const B = this._outerMarker.getLatLng(); + + const distance = A.distanceTo(B); + + if(this.options.minRadiusCircle && distance < this.options.minRadiusCircle) { + this._layer.setRadius(this.options.minRadiusCircle); + }else if(this.options.maxRadiusCircle && distance > this.options.maxRadiusCircle) { + this._layer.setRadius(this.options.maxRadiusCircle); + }else{ + this._layer.setRadius(distance); + } + + this._updateHiddenPolyCircle(); + }, + _syncHintLine() { + const A = this._centerMarker.getLatLng(); + const B = this._outerMarker.getLatLng(); + + // set coords for hintline from marker to last vertex of drawin polyline + this._hintline.setLatLngs([A, B]); + }, + _disableSnapping() { + this._markers.forEach(marker => { + marker.off('move', this._syncHintLine, this); + marker.off('move', this._syncCircleRadius, this); + marker.off('drag', this._handleSnapping, this); + marker.off('dragend', this._cleanupSnapping, this); + }); + + this._layer.off('pm:dragstart', this._unsnap, this); + }, + _onMarkerDragStart(e) { + Utils._fireEvent(this._layer,'pm:markerdragstart', { + layer: this._layer, + markerEvent: e, + shape: this.getShape(), + indexPath: undefined + }); + }, + _onMarkerDrag(e) { + Utils._fireEvent(this._layer,'pm:markerdrag', { + layer: this._layer, + markerEvent: e, + shape: this.getShape(), + indexPath: undefined + }); + }, + _onMarkerDragEnd(e) { + // fire edit event + this._fireEdit(); + + // fire markerdragend event + Utils._fireEvent(this._layer,'pm:markerdragend', { + layer: this._layer, + markerEvent: e, + shape: this.getShape(), + indexPath: undefined + }); + }, _fireEdit() { // fire edit event - this._layer.fire('pm:edit', { layer: this._layer, shape: this.getShape() }); + Utils._fireEvent(this._layer,'pm:edit', { layer: this._layer, shape: this.getShape() }); this._layerEdited = true; }, _fireDragStart() { - this._layer.fire('pm:dragstart', { layer: this._layer, shape: this.getShape() }); + Utils._fireEvent(this._layer,'pm:dragstart', { layer: this._layer, shape: this.getShape() }); }, _fireDrag(e) { - this._layer.fire('pm:drag', Object.assign({},e, {shape:this.getShape()})); + Utils._fireEvent(this._layer,'pm:drag', Object.assign({},e, {shape:this.getShape()})); }, _fireDragEnd() { - this._layer.fire('pm:dragend', { layer: this._layer, shape: this.getShape() }); + Utils._fireEvent(this._layer,'pm:dragend', { layer: this._layer, shape: this.getShape() }); }, _updateHiddenPolyCircle() { if (this._hiddenPolyCircle) { @@ -262,5 +274,36 @@ Edit.Circle = Edit.extend({ if (!this._hiddenPolyCircle._parentCopy) { this._hiddenPolyCircle._parentCopy = this._layer } + }, + _getLatLngOnCircle(center, radius) { + const pointA = this._map.project(center); + const pointB = L.point(pointA.x + radius, pointA.y); + + return this._map.unproject(pointB); + }, + _getNewDestinationOfOuterMarker(){ + const latlng = this._centerMarker.getLatLng(); + let secondLatLng = this._outerMarker.getLatLng(); + const distance = latlng.distanceTo(secondLatLng); + if(this.options.minRadiusCircle && distance < this.options.minRadiusCircle) { + secondLatLng = destinationOnLine(this._map,latlng,secondLatLng,this.options.minRadiusCircle); + }else if(this.options.maxRadiusCircle && distance > this.options.maxRadiusCircle) { + secondLatLng = destinationOnLine(this._map,latlng,secondLatLng,this.options.maxRadiusCircle); + } + return secondLatLng; + }, + _handleOuterMarkerSnapping(){ + if(this._outerMarker._snapped) { + const latlng = this._centerMarker.getLatLng(); + const secondLatLng = this._outerMarker.getLatLng(); + const distance = latlng.distanceTo(secondLatLng); + if(this.options.minRadiusCircle && distance < this.options.minRadiusCircle) { + this._outerMarker.setLatLng(this._outerMarker._orgLatLng); + } else if(this.options.maxRadiusCircle && distance > this.options.maxRadiusCircle) { + this._outerMarker.setLatLng(this._outerMarker._orgLatLng); + } + } + // calculate the new latlng of marker if radius is out of min/max + this._outerMarker.setLatLng(this._getNewDestinationOfOuterMarker()); } }); diff --git a/src/js/Edit/L.PM.Edit.CircleMarker.js b/src/js/Edit/L.PM.Edit.CircleMarker.js index c2667c0b..5936345c 100644 --- a/src/js/Edit/L.PM.Edit.CircleMarker.js +++ b/src/js/Edit/L.PM.Edit.CircleMarker.js @@ -1,5 +1,6 @@ import Edit from './L.PM.Edit'; import Utils from "../L.PM.Utils"; +import {destinationOnLine} from "../helpers"; Edit.CircleMarker = Edit.extend({ _shape: 'CircleMarker', @@ -9,54 +10,6 @@ Edit.CircleMarker = Edit.extend({ // create polygon around the circle border this._updateHiddenPolyCircle(); }, - applyOptions() { - // Use the not editable and only draggable version - if (!this.options.editable && this.options.draggable) { - this.enableLayerDrag(); - } else { - this.disableLayerDrag(); - } - - // Make it editable like a Circle - if (this.options.editable) { - this._initMarkers(); - this._map.on('move', this._syncMarkers, this); - } else { - // only update the circle border poly - this._map.on('move', this._updateHiddenPolyCircle, this); - } - - // init snapping in different ways - if (this.options.snappable) { - if (this.options.editable) { - this._initSnappableMarkers(); - // sync the hintline with hint marker - this._outerMarker.on('move', this._syncHintLine, this); - this._outerMarker.on('move', this._syncCircleRadius, this); - } else { - this._initSnappableMarkersDrag(); - } - } else if (this.options.editable) { - this._disableSnapping(); - } else { - this._disableSnappingDrag(); - } - - // enable removal for the marker - if (!this.options.preventMarkerRemoval) { - this._layer.on('contextmenu', this._removeMarker, this); - } - }, - toggleEdit(options) { - if (!this.enabled()) { - this.enable(options); - } else { - this.disable(); - } - }, - enabled() { - return this._enabled; - }, enable(options = { draggable: true, snappable: true }) { L.Util.setOptions(this, options); @@ -67,21 +20,23 @@ Edit.CircleMarker = Edit.extend({ return; } - if (!this.enabled()) { + if (this.enabled()) { // if it was already enabled, disable first // we don't block enabling again because new options might be passed this.disable(); } this.applyOptions(); - this._layer.fire('pm:enable', { layer: this._layer, shape: this.getShape() }); // change state this._enabled = true; + this._layer.on('pm:dragstart', this._onDragStart, this); this._layer.on('pm:dragend', this._onMarkerDragEnd, this); // create polygon around the circle border this._updateHiddenPolyCircle(); + + Utils._fireEvent(this._layer,'pm:enable', { layer: this._layer, shape: this.getShape() }); }, disable(layer = this._layer) { // prevent disabling if layer is being dragged @@ -100,28 +55,83 @@ Edit.CircleMarker = Edit.extend({ if (this.options.editable) { this._map.off('move', this._syncMarkers, this); + if(this._outerMarker) { + // update marker latlng when snapped latlng radius is out of min/max + this._outerMarker.on('drag', this._handleOuterMarkerSnapping, this); + } } else { this._map.off('move', this._updateHiddenPolyCircle, this); } // disable dragging, as this could have been active even without being enabled this.disableLayerDrag(); + this._layer.off('contextmenu', this._removeMarker, this); + // only fire events if it was enabled before if (!this.enabled()) { - this._layer.fire('pm:disable', { layer: this._layer, shape: this.getShape() }); - if (this._layerEdited) { - this._layer.fire('pm:update', { layer: this._layer, shape: this.getShape() }); + Utils._fireEvent(this._layer,'pm:update', { layer: this._layer, shape: this.getShape() }); } this._layerEdited = false; + Utils._fireEvent(this._layer,'pm:disable', { layer: this._layer, shape: this.getShape() }); } - this._layer.off('contextmenu', this._removeMarker, this); - layer.pm._enabled = false; return true; }, + enabled() { + return this._enabled; + }, + toggleEdit(options) { + if (!this.enabled()) { + this.enable(options); + } else { + this.disable(); + } + }, + applyOptions() { + // Use the not editable and only draggable version + if (!this.options.editable && this.options.draggable) { + this.enableLayerDrag(); + } else { + this.disableLayerDrag(); + } + + // Make it editable like a Circle + if (this.options.editable) { + this._initMarkers(); + this._map.on('move', this._syncMarkers, this); + } else { + // only update the circle border poly + this._map.on('move', this._updateHiddenPolyCircle, this); + } + + // init snapping in different ways + if (this.options.snappable) { + if (this.options.editable) { + this._initSnappableMarkers(); + if(this.options.editable){ + // update marker latlng when snapped latlng radius is out of min/max + this._outerMarker.on('drag',this._handleOuterMarkerSnapping, this); + } + // sync the hintline with hint marker + this._outerMarker.on('move', this._syncHintLine, this); + this._outerMarker.on('move', this._syncCircleRadius, this); + } else { + this._initSnappableMarkersDrag(); + } + } else if (this.options.editable) { + this._disableSnapping(); + } else { + this._disableSnappingDrag(); + } + + // enable removal for the marker + if (!this.options.preventMarkerRemoval) { + this._layer.on('contextmenu', this._removeMarker, this); + } + }, _initMarkers() { const map = this._map; @@ -170,21 +180,6 @@ Edit.CircleMarker = Edit.extend({ } return marker; }, - _moveCircle(e) { - const center = e.latlng; - this._layer.setLatLng(center); - - const radius = this._layer._radius; - const outer = this._getLatLngOnCircle(center, radius); - this._outerMarker.setLatLng(outer); - this._syncHintLine(); - - this._layer.fire('pm:centerplaced', { - layer: this._layer, - latlng: center, - shape: this.getShape() - }); - }, _createOuterMarker(latlng) { const marker = this._createMarker(latlng); marker.on('drag', this._resizeCircle, this); @@ -200,12 +195,28 @@ Edit.CircleMarker = Edit.extend({ marker._pmTempLayer = true; marker.on('dragstart', this._onMarkerDragStart, this); + marker.on('drag', this._onMarkerDrag, this); marker.on('dragend', this._onMarkerDragEnd, this); this._helperLayers.addLayer(marker); return marker; }, + _moveCircle(e) { + const center = e.latlng; + this._layer.setLatLng(center); + + const radius = this._layer._radius; + const outer = this._getLatLngOnCircle(center, radius); + this._outerMarker.setLatLng(outer); + this._syncHintLine(); + + Utils._fireEvent(this._layer,'pm:centerplaced', { + layer: this._layer, + latlng: center, + shape: this.getShape() + }); + }, _syncMarkers() { const center = this._layer.getLatLng(); const radius = this._layer._radius; @@ -216,6 +227,7 @@ Edit.CircleMarker = Edit.extend({ this._updateHiddenPolyCircle(); }, _resizeCircle() { + this._outerMarker.setLatLng(this._getNewDestinationOfOuterMarker()); this._syncHintLine(); this._syncCircleRadius(); }, @@ -224,50 +236,63 @@ Edit.CircleMarker = Edit.extend({ const B = this._outerMarker.getLatLng(); const distance = this._map.project(A).distanceTo(this._map.project(B)); + if(this.options.minRadiusCircleMarker && distance < this.options.minRadiusCircleMarker) { + this._layer.setRadius(this.options.minRadiusCircleMarker); + }else if(this.options.maxRadiusCircleMarker && distance > this.options.maxRadiusCircleMarker) { + this._layer.setRadius(this.options.maxRadiusCircleMarker); + }else{ + this._layer.setRadius(distance); + } - this._layer.setRadius(distance); this._updateHiddenPolyCircle(); }, _syncHintLine() { const A = this._centerMarker.getLatLng(); const B = this._outerMarker.getLatLng(); - // set coords for hintline from marker to last vertex of drawin polyline this._hintline.setLatLngs([A, B]); }, - _moveMarker(e) { - const center = e.latlng; - this._layer.setLatLng(center).redraw(); - }, _removeMarker() { if (this.options.editable) { this.disable(); } this._layer.remove(); - this._layer.fire('pm:remove', { layer: this._layer, shape: this.getShape() }); - this._map.fire('pm:remove', { layer: this._layer, shape: this.getShape() }); + Utils._fireEvent(this._layer,'pm:remove', { layer: this._layer, shape: this.getShape() }); + Utils._fireEvent(this._map,'pm:remove', { layer: this._layer, shape: this.getShape() }); + }, + _onDragStart(){ + this._map.pm.Draw.CircleMarker._layerIsDragging = true; }, _onMarkerDragStart(e) { - this._layer.fire('pm:markerdragstart', { + Utils._fireEvent(this._layer,'pm:markerdragstart', { markerEvent: e, layer: this._layer, shape: this.getShape(), indexPath: undefined }); }, - _fireEdit() { - // fire edit event - this._layer.fire('pm:edit', { layer: this._layer, shape: this.getShape() }); - this._layerEdited = true; + _onMarkerDrag(e) { + Utils._fireEvent(this._layer,'pm:markerdrag', { + layer: this._layer, + markerEvent: e, + shape: this.getShape(), + indexPath: undefined + }); }, _onMarkerDragEnd(e) { - this._layer.fire('pm:markerdragend', { + this._map.pm.Draw.CircleMarker._layerIsDragging = false; + Utils._fireEvent(this._layer,'pm:markerdragend', { layer: this._layer, markerEvent: e, shape: this.getShape(), indexPath: undefined }); }, + _fireEdit() { + // fire edit event + Utils._fireEvent(this._layer,'pm:edit', { layer: this._layer, shape: this.getShape() }); + this._layerEdited = true; + }, // _initSnappableMarkers when option editable is not true _initSnappableMarkersDrag() { const marker = this._layer; @@ -311,5 +336,36 @@ Edit.CircleMarker = Edit.extend({ this._hiddenPolyCircle._parentCopy = this._layer } } + }, + _getNewDestinationOfOuterMarker(){ + const latlng = this._centerMarker.getLatLng(); + let secondLatLng = this._outerMarker.getLatLng(); + const distance = this._map.project(latlng).distanceTo(this._map.project(secondLatLng)); + if(this.options.minRadiusCircleMarker && distance < this.options.minRadiusCircleMarker) { + secondLatLng = destinationOnLine(this._map,latlng,secondLatLng,this._pxRadiusToMeter(this.options.minRadiusCircleMarker)); + }else if(this.options.maxRadiusCircleMarker && distance > this.options.maxRadiusCircleMarker) { + secondLatLng = destinationOnLine(this._map,latlng,secondLatLng,this._pxRadiusToMeter(this.options.maxRadiusCircleMarker)); + } + return secondLatLng; + }, + _handleOuterMarkerSnapping(){ + if(this._outerMarker._snapped) { + const latlng = this._centerMarker.getLatLng(); + const secondLatLng = this._outerMarker.getLatLng(); + const distance = this._map.project(latlng).distanceTo(this._map.project(secondLatLng)); + if(this.options.minRadiusCircleMarker && distance < this.options.minRadiusCircleMarker) { + this._outerMarker.setLatLng(this._outerMarker._orgLatLng); + } else if(this.options.maxRadiusCircleMarker && distance > this.options.maxRadiusCircleMarker) { + this._outerMarker.setLatLng(this._outerMarker._orgLatLng); + } + } + // calculate the new latlng of marker if radius is out of min/max + this._outerMarker.setLatLng(this._getNewDestinationOfOuterMarker()); + }, + _pxRadiusToMeter(radius){ + const center = this._centerMarker.getLatLng(); + const pointA = this._map.project(center); + const pointB = L.point(pointA.x + radius, pointA.y); + return this._map.unproject(pointB).distanceTo(center); } }); diff --git a/src/js/Edit/L.PM.Edit.ImageOverlay.js b/src/js/Edit/L.PM.Edit.ImageOverlay.js new file mode 100644 index 00000000..9de25642 --- /dev/null +++ b/src/js/Edit/L.PM.Edit.ImageOverlay.js @@ -0,0 +1,74 @@ +import Edit from './L.PM.Edit'; +import Utils from "../L.PM.Utils"; + +Edit.ImageOverlay = Edit.extend({ + _shape: 'ImageOverlay', + initialize(layer) { + this._layer = layer; + this._enabled = false; + }, + toggleEdit(options) { + if (!this.enabled()) { + this.enable(options); + } else { + this.disable(); + } + }, + enabled() { + return this._enabled; + }, + enable(options = { draggable: true, snappable: true }) { + L.Util.setOptions(this, options); + this._map = this._layer._map; + // cancel when map isn't available, this happens when the polygon is removed before this fires + if (!this._map) { + return; + } + if (!this.enabled()) { + // if it was already enabled, disable first + // we don't block enabling again because new options might be passed + this.disable(); + } + + this.enableLayerDrag(); + + // change state + this._enabled = true; + + // create markers for four corners of ImageOverlay + this._otherSnapLayers = L.PM.Edit.Rectangle.prototype._findCorners.apply(this); + + Utils._fireEvent(this._layer,'pm:enable', { layer: this._layer, shape: this.getShape() }); + }, + disable(layer = this._layer) { + // prevent disabling if layer is being dragged + if (layer.pm._dragging) { + return false; + } + + // Add map if it is not already set. This happens when disable() is called before enable() + if (!this._map) { + this._map = this._layer._map; + } + // disable dragging, as this could have been active even without being enabled + this.disableLayerDrag(); + + // only fire events if it was enabled before + if (!this.enabled()) { + if (this._layerEdited) { + Utils._fireEvent(layer,'pm:update', { layer, shape: this.getShape() }); + } + this._layerEdited = false; + Utils._fireEvent(layer,'pm:disable', { layer, shape: this.getShape() }); + } + + this._layer.off('contextmenu', this._removeMarker, this); + layer.pm._enabled = false; + return true; + }, + _fireEdit() { + // fire edit event + Utils._fireEvent(this._layer,'pm:edit', { layer: this._layer, shape: this.getShape() }); + this._layerEdited = true; + }, +}); diff --git a/src/js/Edit/L.PM.Edit.LayerGroup.js b/src/js/Edit/L.PM.Edit.LayerGroup.js index 79e6f282..f162f1c3 100644 --- a/src/js/Edit/L.PM.Edit.LayerGroup.js +++ b/src/js/Edit/L.PM.Edit.LayerGroup.js @@ -14,105 +14,126 @@ Edit.LayerGroup = L.Class.extend({ // if a new layer is added to the group, reinitialize // This only works for FeatureGroups, not LayerGroups // https://github.com/Leaflet/Leaflet/issues/4861 - this._layerGroup.on('layeradd', e => { + + const addThrottle = (e) => { if (e.target._pmTempLayer) { return; } - this._layers = this.findLayers(); - // init the newly added layer - if (e.layer.pm) { - this._initLayer(e.layer); - } + const _initLayers = this._layers.filter((layer) => !layer.pm._parentLayerGroup || !(this._layerGroup._leaflet_id in layer.pm._parentLayerGroup)); + // init the newly added layers (can be multiple because of the throttle) + _initLayers.forEach((layer) => { + this._initLayer(layer); + }); // if editing was already enabled for this group, enable it again // so the new layers are enabled - if (e.target.pm.enabled()) { - this.enable(this.getOptions()); + if (_initLayers.length > 0) { + if (this.enabled()) { + this.enable(this.getOptions()); + } } - }); + }; + this._layerGroup.on('layeradd', L.Util.throttle(addThrottle, 100, this), this); + + // Remove the layergroup from the layer + this._layerGroup.on('layerremove', (e) => { + this._removeLayerFromGroup(e.target); + }, this); - // if a layer is removed from the group, calc the layers list again - this._layerGroup.on('layerremove', e => { + const removeThrottle = (e) => { if (e.target._pmTempLayer) { return; } - this._layers = this.findLayers(); - }) + }; + // if a layer is removed from the group, calc the layers list again. + // we run this as throttle because the findLayers() is a larger function + this._layerGroup.on('layerremove', L.Util.throttle(removeThrottle, 100, this), this); }, - findLayers() { - // get all layers of the layer group - let layers = this._layerGroup.getLayers(); - - // filter out layers that are no layerGroup - layers = layers.filter(layer => !(layer instanceof L.LayerGroup)); - - // filter out layers that don't have leaflet-geoman - layers = layers.filter(layer => !!layer.pm); - - // filter out everything that's leaflet-geoman specific temporary stuff - layers = layers.filter(layer => !layer._pmTempLayer); - - // return them - return layers; + enable(options, _layerIds = []) { + this._options = options; + this._layers.forEach(layer => { + if (layer instanceof L.LayerGroup) { + if (_layerIds.indexOf(layer._leaflet_id) === -1) { + _layerIds.push(layer._leaflet_id); + layer.pm.enable(options, _layerIds); + } + } else { + layer.pm.enable(options); + } + }); }, - _initLayer(layer) { - // available events - const availableEvents = [ - 'pm:edit', - 'pm:update', - 'pm:enable', - 'pm:disable', - 'pm:remove', - 'pm:dragstart', - 'pm:drag', - 'pm:dragend', - 'pm:snap', - 'pm:unsnap', - 'pm:cut', - 'pm:intersect', - 'pm:markerdragend', - 'pm:markerdragstart', - 'pm:vertexadded', - 'pm:vertexremoved', - 'pm:centerplaced', - ]; + disable(_layerIds = []) { + this._layers.forEach(layer => { + if (layer instanceof L.LayerGroup) { + if (_layerIds.indexOf(layer._leaflet_id) === -1) { + _layerIds.push(layer._leaflet_id); + layer.pm.disable(_layerIds); + } + } else { + layer.pm.disable(); + } + }); + }, + enabled(_layerIds = []) { - // listen to the events of the layers in this group - availableEvents.forEach(event => { - layer.on(event, this._fireEvent, this); + const enabled = this._layers.find((layer) => { + if (layer instanceof L.LayerGroup) { + if (_layerIds.indexOf(layer._leaflet_id) === -1) { + _layerIds.push(layer._leaflet_id); + return layer.pm.enabled(_layerIds); + } + return false; // enabled is already returned because this is not the first time, so we can return always false + } else { + return layer.pm.enabled(); + } }); - // add reference for the group to each layer inside said group - layer.pm._layerGroup = this._layerGroup; - }, - _fireEvent(e) { - this._layerGroup.fireEvent(e.type, e); + return !!enabled; }, - toggleEdit(options) { + toggleEdit(options, _layerIds = []) { this._options = options; this._layers.forEach(layer => { - layer.pm.toggleEdit(options); + if (layer instanceof L.LayerGroup) { + if (_layerIds.indexOf(layer._leaflet_id) === -1) { + _layerIds.push(layer._leaflet_id); + layer.pm.toggleEdit(options, _layerIds); + } + } else { + layer.pm.toggleEdit(options); + } }); }, - enable(options) { - this._options = options; - this._layers.forEach(layer => { - layer.pm.enable(options); - }); + _initLayer(layer) { + // add reference for the group to each layer inside said group by id, a layer can have multiple groups + const id = L.Util.stamp(this._layerGroup); + if (!layer.pm._parentLayerGroup) { + layer.pm._parentLayerGroup = {}; + } + layer.pm._parentLayerGroup[id] = this._layerGroup; }, - disable() { - this._layers.forEach(layer => { - layer.pm.disable(); - }); + _removeLayerFromGroup(layer) { + if(layer.pm && layer.pm._layerGroup) { + const id = L.Util.stamp(this._layerGroup); + delete layer.pm._layerGroup[id]; + } }, - enabled() { - const enabled = this._layers.find(layer => layer.pm.enabled()); - return !!enabled; + findLayers() { + // get all layers of the layer group + let layers = this._layerGroup.getLayers(); + // filter out layers that don't have leaflet-geoman + layers = layers.filter(layer => !!layer.pm); + // filter out everything that's leaflet-geoman specific temporary stuff + layers = layers.filter(layer => !layer._pmTempLayer); + // return them + return layers; }, dragging() { - const dragging = this._layers.find(layer => layer.pm.dragging()); - return !!dragging; + if (this._layers) { + const dragging = this._layers.find(layer => layer.pm.dragging()); + return !!dragging; + } + return false; }, getOptions() { return this._options; diff --git a/src/js/Edit/L.PM.Edit.Line.js b/src/js/Edit/L.PM.Edit.Line.js index 1dd2a8bb..551dd04e 100644 --- a/src/js/Edit/L.PM.Edit.Line.js +++ b/src/js/Edit/L.PM.Edit.Line.js @@ -3,7 +3,7 @@ import lineIntersect from '@turf/line-intersect'; import get from 'lodash/get'; import Edit from './L.PM.Edit'; import Utils from '../L.PM.Utils'; -import { isEmptyDeep } from '../helpers'; +import { isEmptyDeep, removeEmptyCoordRings } from '../helpers'; import MarkerLimits from '../Mixins/MarkerLimits'; @@ -22,25 +22,6 @@ Edit.Line = Edit.extend({ this._layer = layer; this._enabled = false; }, - - applyOptions() { - if (this.options.snappable) { - this._initSnappableMarkers(); - } else { - this._disableSnapping(); - } - }, - - toggleEdit(options) { - if (!this.enabled()) { - this.enable(options); - } else { - this.disable(); - } - - return this.enabled(); - }, - enable(options) { L.Util.setOptions(this, options); @@ -51,6 +32,7 @@ Edit.Line = Edit.extend({ return; } + // TODO: this is wrong: if already enable then go into the if if (!this.enabled()) { // if it was already enabled, disable first // we don't block enabling again because new options might be passed @@ -65,8 +47,6 @@ Edit.Line = Edit.extend({ this.applyOptions(); - this._layer.fire('pm:enable', { layer: this._layer, shape: this.getShape() }); - // if polygon gets removed from map, disable edit mode this._layer.on('remove', this._onLayerRemove, this); @@ -78,23 +58,19 @@ Edit.Line = Edit.extend({ ); } - this.cachedColor = undefined; if (!this.options.allowSelfIntersection) { - this.cachedColor = this._layer.options.color; - - this.isRed = false; + if(this._layer.options.color !== 'red') { + this.cachedColor = this._layer.options.color; + this.isRed = false; + }else{ + this.isRed = true; + } this._handleLayerStyle(); + }else{ + this.cachedColor = undefined; } + Utils._fireEvent(this._layer,'pm:enable', { layer: this._layer, shape: this.getShape() }); }, - - _onLayerRemove(e) { - this.disable(e.target); - }, - - enabled() { - return this._enabled; - }, - disable(poly = this._layer) { // if it's not enabled, it doesn't need to be disabled if (!this.enabled()) { @@ -116,8 +92,6 @@ Edit.Line = Edit.extend({ // remove onRemove listener this._layer.off('remove', this._onLayerRemove, this); - - if (!this.options.allowSelfIntersection) { this._layer.off( 'pm:vertexremoved', @@ -135,93 +109,34 @@ Edit.Line = Edit.extend({ L.DomUtil.removeClass(el, 'leaflet-pm-invalid'); } - this._layer.fire('pm:disable', { layer: this._layer, shape: this.getShape() }); - if (this._layerEdited) { - this._layer.fire('pm:update', { layer: this._layer, shape: this.getShape() }); + Utils._fireEvent(this._layer,'pm:update', { layer: this._layer, shape: this.getShape() }); } this._layerEdited = false; - + Utils._fireEvent(this._layer,'pm:disable', { layer: this._layer, shape: this.getShape() }); return true; }, - - hasSelfIntersection() { - // check for self intersection of the layer and return true/false - const selfIntersection = kinks(this._layer.toGeoJSON(15)); - return selfIntersection.features.length > 0; + enabled() { + return this._enabled; }, - - _handleSelfIntersectionOnVertexRemoval() { - // check for selfintersection again (mainly to reset the style) - this._handleLayerStyle(true); - - if (this.hasSelfIntersection()) { - // reset coordinates - this._layer.setLatLngs(this._coordsBeforeEdit); - this._coordsBeforeEdit = null; - - // re-enable markers for the new coords - this._initMarkers(); + toggleEdit(options) { + if (!this.enabled()) { + this.enable(options); + } else { + this.disable(); } + return this.enabled(); }, - - _handleLayerStyle(flash) { - const layer = this._layer; - - if (this.hasSelfIntersection()) { - if (!this.options.allowSelfIntersection && this.options.allowSelfIntersectionEdit) { - this._updateDisabledMarkerStyle(this._markers, true); - } - - if (this.isRed) { - return; - } - - // if it does self-intersect, mark or flash it red - if (flash) { - layer.setStyle({ color: 'red' }); - this.isRed = true; - - window.setTimeout(() => { - layer.setStyle({ color: this.cachedColor }); - this.isRed = false; - }, 200); - } else { - layer.setStyle({ color: 'red' }); - this.isRed = true; - } - - // fire intersect event - this._layer.fire('pm:intersect', { - layer: this._layer, - intersection: kinks(this._layer.toGeoJSON(15)), - shape: this.getShape() - }); + applyOptions() { + if (this.options.snappable) { + this._initSnappableMarkers(); } else { - // if not, reset the style to the default color - layer.setStyle({ color: this.cachedColor }); - this.isRed = false; - if (!this.options.allowSelfIntersection && this.options.allowSelfIntersectionEdit) { - this._updateDisabledMarkerStyle(this._markers, false); - } + this._disableSnapping(); } }, - _updateDisabledMarkerStyle(markers, disabled) { - markers.forEach((marker) => { - if (Array.isArray(marker)) { - return this._updateDisabledMarkerStyle(marker, disabled); - } - - if (marker._icon) { - if (disabled && !this._checkMarkerAllowedToDrag(marker)) { - L.DomUtil.addClass(marker._icon, "vertexmarker-disabled"); - } else { - L.DomUtil.removeClass(marker._icon, "vertexmarker-disabled"); - } - } - }); + _onLayerRemove(e) { + this.disable(e.target); }, - _initMarkers() { const map = this._map; const coords = this._layer.getLatLngs(); @@ -382,7 +297,7 @@ Edit.Line = Edit.extend({ // fire edit event this._fireEdit(); - this._layer.fire('pm:vertexadded', { + Utils._fireEvent(this._layer,'pm:vertexadded', { layer: this._layer, marker: newM, indexPath: this.findDeepMarkerIndex(this._markers, newM).indexPath, @@ -395,6 +310,83 @@ Edit.Line = Edit.extend({ } }, + hasSelfIntersection() { + // check for self intersection of the layer and return true/false + const selfIntersection = kinks(this._layer.toGeoJSON(15)); + return selfIntersection.features.length > 0; + }, + + _handleSelfIntersectionOnVertexRemoval() { + // check for selfintersection again (mainly to reset the style) + this._handleLayerStyle(true); + + if (this.hasSelfIntersection()) { + // reset coordinates + this._layer.setLatLngs(this._coordsBeforeEdit); + this._coordsBeforeEdit = null; + + // re-enable markers for the new coords + this._initMarkers(); + } + }, + + _handleLayerStyle(flash) { + const layer = this._layer; + + if (this.hasSelfIntersection()) { + if (!this.options.allowSelfIntersection && this.options.allowSelfIntersectionEdit) { + this._updateDisabledMarkerStyle(this._markers, true); + } + + if (this.isRed) { + return; + } + + // if it does self-intersect, mark or flash it red + if (flash) { + layer.setStyle({ color: 'red' }); + this.isRed = true; + + window.setTimeout(() => { + layer.setStyle({ color: this.cachedColor }); + this.isRed = false; + }, 200); + } else { + layer.setStyle({ color: 'red' }); + this.isRed = true; + } + + // fire intersect event + Utils._fireEvent(this._layer,'pm:intersect', { + layer: this._layer, + intersection: kinks(this._layer.toGeoJSON(15)), + shape: this.getShape() + }); + } else { + // if not, reset the style to the default color + layer.setStyle({ color: this.cachedColor }); + this.isRed = false; + if (!this.options.allowSelfIntersection && this.options.allowSelfIntersectionEdit) { + this._updateDisabledMarkerStyle(this._markers, false); + } + } + }, + _updateDisabledMarkerStyle(markers, disabled) { + markers.forEach((marker) => { + if (Array.isArray(marker)) { + return this._updateDisabledMarkerStyle(marker, disabled); + } + + if (marker._icon) { + if (disabled && !this._checkMarkerAllowedToDrag(marker)) { + L.DomUtil.addClass(marker._icon, "vertexmarker-disabled"); + } else { + L.DomUtil.removeClass(marker._icon, "vertexmarker-disabled"); + } + } + }); + }, + _removeMarker(e) { // if self intersection isn't allowed, save the coords upon dragstart // in case we need to reset the layer @@ -407,7 +399,7 @@ Edit.Line = Edit.extend({ const marker = e.target; // coords of the layer - const coords = this._layer.getLatLngs(); + let coords = this._layer.getLatLngs(); // the index path to the marker inside the multidimensional marker array const { indexPath, index, parentPath } = this.findDeepMarkerIndex( @@ -424,7 +416,7 @@ Edit.Line = Edit.extend({ const coordsRing = indexPath.length > 1 ? get(coords, parentPath) : coords; // define the markers array that is edited - const markerArr = + let markerArr = indexPath.length > 1 ? get(this._markers, parentPath) : this._markers; // remove coordinate @@ -449,15 +441,22 @@ Edit.Line = Edit.extend({ // TODO: kind of an ugly workaround maybe do it better? this.disable(); this.enable(this.options); - } - // TODO: we may should remove all empty coord-rings here as well. + } // if no coords are left, remove the layer if (isEmptyDeep(coords)) { this._layer.remove(); } + // remove all empty coord-rings + coords = removeEmptyCoordRings(coords); + this._layer.setLatLngs(coords); + // remove empty marker arrays + this._markers = removeEmptyCoordRings(this._markers); + // get new markerArr because we cleaned up coords and markers array + markerArr = indexPath.length > 1 ? get(this._markers, parentPath) : this._markers; + // now handle the middle markers // remove the marker and the middlemarkers next to it from the map if (marker._middleMarkerPrev) { @@ -470,36 +469,38 @@ Edit.Line = Edit.extend({ // remove the marker from the map this._markerGroup.removeLayer(marker); - let rightMarkerIndex; - let leftMarkerIndex; + if(markerArr) { + let rightMarkerIndex; + let leftMarkerIndex; - if (this.isPolygon()) { - // find neighbor marker-indexes - rightMarkerIndex = (index + 1) % markerArr.length; - leftMarkerIndex = (index + (markerArr.length - 1)) % markerArr.length; - } else { - // find neighbor marker-indexes - leftMarkerIndex = index - 1 < 0 ? undefined : index - 1; - rightMarkerIndex = index + 1 >= markerArr.length ? undefined : index + 1; - } + if (this.isPolygon()) { + // find neighbor marker-indexes + rightMarkerIndex = (index + 1) % markerArr.length; + leftMarkerIndex = (index + (markerArr.length - 1)) % markerArr.length; + } else { + // find neighbor marker-indexes + leftMarkerIndex = index - 1 < 0 ? undefined : index - 1; + rightMarkerIndex = index + 1 >= markerArr.length ? undefined : index + 1; + } - // don't create middlemarkers if there is only one marker left - if (rightMarkerIndex !== leftMarkerIndex) { - const leftM = markerArr[leftMarkerIndex]; - const rightM = markerArr[rightMarkerIndex]; - if (this.options.hideMiddleMarkers !== true) { - this._createMiddleMarker(leftM, rightM); + // don't create middlemarkers if there is only one marker left + if (rightMarkerIndex !== leftMarkerIndex) { + const leftM = markerArr[leftMarkerIndex]; + const rightM = markerArr[rightMarkerIndex]; + if (this.options.hideMiddleMarkers !== true) { + this._createMiddleMarker(leftM, rightM); + } } - } - // remove the marker from the markers array - markerArr.splice(index, 1); + // remove the marker from the markers array + markerArr.splice(index, 1); + } // fire edit event this._fireEdit(); // fire vertex removal event - this._layer.fire('pm:vertexremoved', { + Utils._fireEvent(this._layer,'pm:vertexremoved', { layer: this._layer, marker, indexPath, @@ -576,6 +577,58 @@ Edit.Line = Edit.extend({ return { prevMarker, nextMarker }; }, + _checkMarkerAllowedToDrag(marker) { + const { prevMarker, nextMarker } = this._getNeighborMarkers(marker); + + const prevLine = L.polyline([prevMarker.getLatLng(), marker.getLatLng()]); + const nextLine = L.polyline([marker.getLatLng(), nextMarker.getLatLng()]); + + let prevLineIntersectionLen = lineIntersect(this._layer.toGeoJSON(15), prevLine.toGeoJSON(15)).features.length; + let nextLineIntersectionLen = lineIntersect(this._layer.toGeoJSON(15), nextLine.toGeoJSON(15)).features.length; + + // The first and last line has one intersection fewer because they are not connected + if (marker.getLatLng() === this._markers[0][0].getLatLng()) { + nextLineIntersectionLen += 1; + } else if (marker.getLatLng() === this._markers[0][this._markers[0].length - 1].getLatLng()) { + prevLineIntersectionLen += 1; + } + + // <= 2 the start and end point of the line always intersecting because they have the same coords. + if (prevLineIntersectionLen <= 2 && nextLineIntersectionLen <= 2) { + return false; + } + return true; + + }, + _onMarkerDragStart(e) { + const marker = e.target; + const { indexPath } = this.findDeepMarkerIndex(this._markers, marker); + + Utils._fireEvent(this._layer,'pm:markerdragstart', { + layer: this._layer, + markerEvent: e, + indexPath, + shape: this.getShape() + }); + + // 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(); + } + + // When intersection is true while calling enable(), the cachedColor is already set + if (!this.cachedColor) { + this.cachedColor = this._layer.options.color; + } + + + if (!this.options.allowSelfIntersection && this.options.allowSelfIntersectionEdit && this.hasSelfIntersection()) { + this._markerAllowedToDrag = this._checkMarkerAllowedToDrag(marker); + } else { + this._markerAllowedToDrag = null; + } + }, _onMarkerDrag(e) { // dragged marker const marker = e.target; @@ -639,13 +692,18 @@ Edit.Line = Edit.extend({ if (!this.options.allowSelfIntersection) { this._handleLayerStyle(); } + Utils._fireEvent(this._layer,'pm:markerdrag', { + layer: this._layer, + markerEvent: e, + shape: this.getShape(), + indexPath + }); }, - _onMarkerDragEnd(e) { const marker = e.target; const { indexPath } = this.findDeepMarkerIndex(this._markers, marker); - this._layer.fire('pm:markerdragend', { + Utils._fireEvent(this._layer,'pm:markerdragend', { layer: this._layer, markerEvent: e, indexPath, @@ -674,66 +732,12 @@ Edit.Line = Edit.extend({ if (!this.options.allowSelfIntersection && this.options.allowSelfIntersectionEdit) { this._handleLayerStyle(); } - - // fire edit event this._fireEdit(); }, - _onMarkerDragStart(e) { - const marker = e.target; - const { indexPath } = this.findDeepMarkerIndex(this._markers, marker); - - this._layer.fire('pm:markerdragstart', { - layer: this._layer, - markerEvent: e, - indexPath, - shape: this.getShape() - }); - - // 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(); - } - - // When intersection is true while calling enable(), the cachedColor is already set - if (!this.cachedColor) { - this.cachedColor = this._layer.options.color; - } - - - if (!this.options.allowSelfIntersection && this.options.allowSelfIntersectionEdit && this.hasSelfIntersection()) { - this._markerAllowedToDrag = this._checkMarkerAllowedToDrag(marker); - } else { - this._markerAllowedToDrag = null; - } - }, - _checkMarkerAllowedToDrag(marker) { - const { prevMarker, nextMarker } = this._getNeighborMarkers(marker); - - const prevLine = L.polyline([prevMarker.getLatLng(), marker.getLatLng()]); - const nextLine = L.polyline([marker.getLatLng(), nextMarker.getLatLng()]); - - let prevLineIntersectionLen = lineIntersect(this._layer.toGeoJSON(15), prevLine.toGeoJSON(15)).features.length; - let nextLineIntersectionLen = lineIntersect(this._layer.toGeoJSON(15), nextLine.toGeoJSON(15)).features.length; - - // The first and last line has one intersection fewer because they are not connected - if (marker.getLatLng() === this._markers[0][0].getLatLng()) { - nextLineIntersectionLen += 1; - } else if (marker.getLatLng() === this._markers[0][this._markers[0].length - 1].getLatLng()) { - prevLineIntersectionLen += 1; - } - - // <= 2 the start and end point of the line always intersecting because they have the same coords. - if (prevLineIntersectionLen <= 2 && nextLineIntersectionLen <= 2) { - return false; - } - return true; - - }, _fireEdit() { // fire edit event this._layerEdited = true; - this._layer.fire('pm:edit', { layer: this._layer, shape: this.getShape() }); + Utils._fireEvent(this._layer,'pm:edit', { layer: this._layer, shape: this.getShape() }); }, }); diff --git a/src/js/Edit/L.PM.Edit.Marker.js b/src/js/Edit/L.PM.Edit.Marker.js index 9d01d971..e35a5c70 100644 --- a/src/js/Edit/L.PM.Edit.Marker.js +++ b/src/js/Edit/L.PM.Edit.Marker.js @@ -1,4 +1,5 @@ import Edit from './L.PM.Edit'; +import Utils from "../L.PM.Utils"; Edit.Marker = Edit.extend({ _shape: 'Marker', @@ -10,35 +11,6 @@ Edit.Marker = Edit.extend({ // register dragend event e.g. to fire pm:edit this._layer.on('dragend', this._onDragEnd, this); }, - - applyOptions() { - // console.log('apply options', this.options) - - if (this.options.snappable) { - this._initSnappableMarkers(); - } else { - this._disableSnapping(); - } - - if (this.options.draggable) { - this.enableLayerDrag(); - } else { - this.disableLayerDrag(); - } - // enable removal for the marker - if (!this.options.preventMarkerRemoval) { - this._layer.on('contextmenu', this._removeMarker, this); - } - }, - - toggleEdit(options) { - if (!this.enabled()) { - this.enable(options); - } else { - this.disable(); - } - }, - enable(options = { draggable: true }) { L.Util.setOptions(this, options); @@ -47,17 +19,11 @@ Edit.Marker = Edit.extend({ if (this.enabled()) { return; } - this._enabled = true; - - this._layer.fire('pm:enable', { layer: this._layer, shape: this.getShape() }); - this.applyOptions(); - }, + this._enabled = true; - enabled() { - return this._enabled; + Utils._fireEvent(this._layer,'pm:enable', { layer: this._layer, shape: this.getShape() }); }, - disable() { this._enabled = false; @@ -66,25 +32,52 @@ Edit.Marker = Edit.extend({ this._layer.off('contextmenu', this._removeMarker, this); - this._layer.fire('pm:disable', { layer: this._layer, shape: this.getShape() }); + Utils._fireEvent(this._layer,'pm:disable', { layer: this._layer, shape: this.getShape() }); if (this._layerEdited) { - this._layer.fire('pm:update', { layer: this._layer, shape: this.getShape() }); + Utils._fireEvent(this._layer,'pm:update', { layer: this._layer, shape: this.getShape() }); } this._layerEdited = false; }, + enabled() { + return this._enabled; + }, + toggleEdit(options) { + if (!this.enabled()) { + this.enable(options); + } else { + this.disable(); + } + }, + applyOptions() { + if (this.options.snappable) { + this._initSnappableMarkers(); + } else { + this._disableSnapping(); + } + + if (this.options.draggable) { + this.enableLayerDrag(); + } else { + this.disableLayerDrag(); + } + // enable removal for the marker + if (!this.options.preventMarkerRemoval) { + this._layer.on('contextmenu', this._removeMarker, this); + } + }, _removeMarker(e) { const marker = e.target; marker.remove(); // TODO: find out why this is fired manually, shouldn't it be catched by L.PM.Map 'layerremove'? - marker.fire('pm:remove', { layer: marker, shape: this.getShape() }); - this._map.fire('pm:remove', { layer: marker, shape: this.getShape() }); + Utils._fireEvent(marker,'pm:remove', { layer: marker, shape: this.getShape() }); + Utils._fireEvent(this._map,'pm:remove', { layer: marker, shape: this.getShape() }); }, _onDragEnd(e) { const marker = e.target; // fire the pm:edit event and pass shape and marker - marker.fire('pm:edit', { layer: this._layer, shape: this.getShape() }); + Utils._fireEvent(marker,'pm:edit', { layer: this._layer, shape: this.getShape() }); this._layerEdited = true; }, // overwrite initSnappableMarkers from Snapping.js Mixin diff --git a/src/js/Edit/L.PM.Edit.Rectangle.js b/src/js/Edit/L.PM.Edit.Rectangle.js index 8f1c7d81..dbd984e1 100644 --- a/src/js/Edit/L.PM.Edit.Rectangle.js +++ b/src/js/Edit/L.PM.Edit.Rectangle.js @@ -2,6 +2,7 @@ // https://github.com/Leaflet/Leaflet.draw/blob/master/src/edit/handler/Edit.Rectangle.js import Edit from './L.PM.Edit'; +import Utils from "../L.PM.Utils"; Edit.Rectangle = Edit.Polygon.extend({ _shape: 'Rectangle', @@ -28,10 +29,14 @@ Edit.Rectangle = Edit.Polygon.extend({ // convenience alias, for better readability [this._cornerMarkers] = this._markers; - + }, + applyOptions() { if (this.options.snappable) { this._initSnappableMarkers(); + } else { + this._disableSnapping(); } + this._addMarkerEvents(); }, // creates initial markers for coordinates @@ -45,18 +50,22 @@ Edit.Rectangle = Edit.Polygon.extend({ marker._index = index; marker._pmTempLayer = true; - marker.on('dragstart', this._onMarkerDragStart, this); - marker.on('drag', this._onMarkerDrag, this); - marker.on('dragend', this._onMarkerDragEnd, this); - marker.on('pm:snap', this._adjustRectangleForMarkerSnap, this); - if (!this.options.preventMarkerRemoval) { - marker.on('contextmenu', this._removeMarker, this); - } this._markerGroup.addLayer(marker); return marker; }, - + // Add marker events after adding the snapping events to the markers, beacause of the execution order + _addMarkerEvents(){ + this._markers[0].forEach((marker)=>{ + marker.on('dragstart', this._onMarkerDragStart, this); + marker.on('drag', this._onMarkerDrag, this); + marker.on('dragend', this._onMarkerDragEnd, this); + marker.on('pm:snap', this._adjustRectangleForMarkerSnap, this); + if (!this.options.preventMarkerRemoval) { + marker.on('contextmenu', this._removeMarker, this); + } + }); + }, // Empty callback for 'contextmenu' binding set in L.PM.Edit.Line.js's _createMarker method (AKA, right-click on marker event) // (A Rectangle is designed to always remain a "true" rectangle -- if you want it editable, use Polygon Tool instead!!!) _removeMarker() { @@ -77,7 +86,7 @@ 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._layer.fire('pm:markerdragstart', { + Utils._fireEvent(this._layer,'pm:markerdragstart', { layer: this._layer, markerEvent: e, shape: this.getShape(), @@ -98,6 +107,13 @@ Edit.Rectangle = Edit.Polygon.extend({ if (!draggedMarker._snapped) { this._adjustRectangleForMarkerMove(draggedMarker); } + + Utils._fireEvent(this._layer,'pm:markerdrag', { + layer: this._layer, + markerEvent: e, + shape: this.getShape(), + indexPath: undefined + }); }, _onMarkerDragEnd(e) { @@ -114,7 +130,7 @@ Edit.Rectangle = Edit.Polygon.extend({ // Update bounding box this._layer.setLatLngs(corners); - this._layer.fire('pm:markerdragend', { + Utils._fireEvent(this._layer,'pm:markerdragend', { layer: this._layer, markerEvent: e, shape: this.getShape(), diff --git a/src/js/Edit/L.PM.Edit.js b/src/js/Edit/L.PM.Edit.js index 90869ea3..98fd3dd1 100644 --- a/src/js/Edit/L.PM.Edit.js +++ b/src/js/Edit/L.PM.Edit.js @@ -23,7 +23,7 @@ const Edit = L.Class.extend({ }, getShape(){ return this._shape; - } + }, }); export default Edit; diff --git a/src/js/L.PM.Map.js b/src/js/L.PM.Map.js index 1933f61e..8acdec19 100644 --- a/src/js/L.PM.Map.js +++ b/src/js/L.PM.Map.js @@ -20,6 +20,8 @@ const Map = L.Class.extend({ this.globalOptions = { snappable: true, + layerGroup: undefined, + snappingOrder: ['Marker','CircleMarker','Circle','Line','Polygon','Rectangle'] }; }, setLang(lang = 'en', t, fallback = 'en') { @@ -30,7 +32,7 @@ const Map = L.Class.extend({ L.PM.activeLang = lang; this.map.pm.Toolbar.reinit(); - this.map.fire("pm:langchange", { + Utils._fireEvent(this.map,"pm:langchange", { oldLang, activeLang: lang, fallback, @@ -86,10 +88,21 @@ const Map = L.Class.extend({ ...o }; + // check if switched the editable mode for CircleMarker while drawing + let reenableCircleMarker = false; + if(this.map.pm.Draw.CircleMarker.enabled() && this.map.pm.Draw.CircleMarker.options.editable !== options.editable){ + this.map.pm.Draw.CircleMarker.disable(); + reenableCircleMarker = true; + } + // enable options for Drawing Shapes this.map.pm.Draw.shapes.forEach(shape => { this.map.pm.Draw[shape].setOptions(options) - }) + }); + + if(reenableCircleMarker){ + this.map.pm.Draw.CircleMarker.enable(); + } // enable options for Editing const layers = findLayers(this.map); @@ -126,11 +139,33 @@ const Map = L.Class.extend({ disableGlobalCutMode() { return this.Draw.Cut.disable(); }, - getGeomanLayers(){ - return findLayers(this.map); + getGeomanLayers(asGroup = false){ + const layers = findLayers(this.map); + if(!asGroup) { + return layers; + } + const group = L.featureGroup(); + group._pmTempLayer = true; + layers.forEach((layer) => { + group.addLayer(layer); + }); + return group; + }, + getGeomanDrawLayers(asGroup = false){ + const layers = findLayers(this.map).filter(l => l._drawnByGeoman === true); + if(!asGroup) { + return layers; + } + const group = L.featureGroup(); + group._pmTempLayer = true; + layers.forEach((layer) => { + group.addLayer(layer); + }); + return group; }, - getGeomanDrawLayers(){ - return findLayers(this.map).filter(l => l._drawnByGeoman === true); + // returns the map instance by default or a layergroup is set through global options + _getContainingLayer(){ + return this.globalOptions.layerGroup && this.globalOptions.layerGroup instanceof L.LayerGroup ? this.globalOptions.layerGroup : this.map; } }); diff --git a/src/js/L.PM.Utils.js b/src/js/L.PM.Utils.js index 33eac2b3..663dda01 100644 --- a/src/js/L.PM.Utils.js +++ b/src/js/L.PM.Utils.js @@ -16,7 +16,8 @@ const Utils = { layer instanceof L.Polyline || layer instanceof L.Marker || layer instanceof L.Circle || - layer instanceof L.CircleMarker + layer instanceof L.CircleMarker || + layer instanceof L.ImageOverlay ) { layers.push(layer); } @@ -41,8 +42,111 @@ const Utils = { } return L.polygon(polygon, circle.options); }, + disablePopup(layer){ + if(layer.getPopup()){ + layer._tempPopupCopy = layer.getPopup(); + layer.unbindPopup(); + } + }, + enablePopup(layer){ + if(layer._tempPopupCopy){ + layer.bindPopup(layer._tempPopupCopy); + delete layer._tempPopupCopy; + } + }, + _fireEvent(layer,type,data,propagate = false) { + layer.fire(type, data, propagate); + + // fire event to all parent layers + const {groups} = this.getAllParentGroups(layer); + groups.forEach((group) => { + group.fire(type, data, propagate); + }); + }, + getAllParentGroups(layer){ + const groupIds = []; + const groups = []; + + // get every group layer once + const loopThroughParents = (_layer) => { + for (const _id in _layer._eventParents) { + if(groupIds.indexOf(_id) === -1){ + groupIds.push(_id); + const group = _layer._eventParents[_id]; + groups.push(group); + loopThroughParents(group) + } + } + }; + + // check if the last group fetch is under 1 sec, then we use the groups from before + if(!layer._pmLastGroupFetch || !layer._pmLastGroupFetch.time || (new Date().getTime() - layer._pmLastGroupFetch.time)> 1000){ + loopThroughParents(layer); + layer._pmLastGroupFetch = { + time: new Date().getTime(), + groups, + groupIds + } ; + return { + groupIds, + groups + } + }else{ + return { + groups: layer._pmLastGroupFetch.groups, + groupIds: layer._pmLastGroupFetch.groupIds + } + } + }, createGeodesicPolygon, getTranslation, + findDeepCoordIndex(arr, latlng) { + // find latlng in arr and return its location as path + // thanks for the function, Felix Heck + let result; + + const run = path => (v, i) => { + const iRes = path.concat(i); + + if (v.lat && v.lat === latlng.lat && v.lng === latlng.lng) { + result = iRes; + return true; + } + + return Array.isArray(v) && v.some(run(iRes)); + }; + arr.some(run([])); + + let returnVal = {}; + + if (result) { + returnVal = { + indexPath: result, + index: result[result.length - 1], + parentPath: result.slice(0, result.length - 1), + }; + } + + return returnVal; + }, + _getIndexFromSegment(coords, segment) { + if (segment && segment.length === 2) { + const indexA = this.findDeepCoordIndex(coords, segment[0]); + const indexB = this.findDeepCoordIndex(coords, segment[1]); + let newIndex = Math.max(indexA.index, indexB.index); + if ((indexA.index === 0 || indexB.index === 0) && newIndex !== 1) { + newIndex+=1; + } + return { + indexA, + indexB, + newIndex, + indexPath: indexA.indexPath, + parentPath: indexA.parentPath, + }; + } + return null; + }, }; export default Utils; diff --git a/src/js/L.PM.js b/src/js/L.PM.js index a4c45cb6..f94bb446 100644 --- a/src/js/L.PM.js +++ b/src/js/L.PM.js @@ -31,6 +31,7 @@ import './Edit/L.PM.Edit.Polygon'; import './Edit/L.PM.Edit.Rectangle'; import './Edit/L.PM.Edit.Circle'; import './Edit/L.PM.Edit.CircleMarker'; +import './Edit/L.PM.Edit.ImageOverlay'; import '../css/layers.css'; import '../css/controls.css'; @@ -45,15 +46,19 @@ L.PM = L.PM || { Edit, Utils, activeLang: 'en', + optIn: false, initialize(options) { this.addInitHooks(options); }, + setOptIn(value){ + this.optIn = !!value; + }, addInitHooks(options = {}) { function initMap() { this.pm = undefined; - if (options.optIn) { + if (L.PM.optIn) { if (this.options.pmIgnore === false) { this.pm = new L.PM.Map(this); } @@ -65,8 +70,14 @@ L.PM = L.PM || { L.Map.addInitHook(initMap); function initLayerGroup() { - // doesn't need pmIgnore condition as the init hook of the individual layers will check it - this.pm = new L.PM.Edit.LayerGroup(this); + this.pm = undefined; + if (L.PM.optIn) { + if (this.options.pmIgnore === false) { + this.pm = new L.PM.Edit.LayerGroup(this); + } + } else if (!this.options.pmIgnore) { + this.pm = new L.PM.Edit.LayerGroup(this); + } } L.LayerGroup.addInitHook(initLayerGroup); @@ -74,7 +85,7 @@ L.PM = L.PM || { function initMarker() { this.pm = undefined; - if (options.optIn) { + if (L.PM.optIn) { if (this.options.pmIgnore === false) { this.pm = new L.PM.Edit.Marker(this); } @@ -82,13 +93,12 @@ L.PM = L.PM || { this.pm = new L.PM.Edit.Marker(this); } } - L.Marker.addInitHook(initMarker); function initCircleMarker() { this.pm = undefined; - if (options.optIn) { + if (L.PM.optIn) { if (this.options.pmIgnore === false) { this.pm = new L.PM.Edit.CircleMarker(this); } @@ -102,7 +112,7 @@ L.PM = L.PM || { function initPolyline() { this.pm = undefined; - if (options.optIn) { + if (L.PM.optIn) { if (this.options.pmIgnore === false) { this.pm = new L.PM.Edit.Line(this); } @@ -116,7 +126,7 @@ L.PM = L.PM || { function initPolygon() { this.pm = undefined; - if (options.optIn) { + if (L.PM.optIn) { if (this.options.pmIgnore === false) { this.pm = new L.PM.Edit.Polygon(this); } @@ -131,7 +141,7 @@ L.PM = L.PM || { function initRectangle() { this.pm = undefined; - if (options.optIn) { + if (L.PM.optIn) { if (this.options.pmIgnore === false) { this.pm = new L.PM.Edit.Rectangle(this); } @@ -145,7 +155,7 @@ L.PM = L.PM || { function initCircle() { this.pm = undefined; - if (options.optIn) { + if (L.PM.optIn) { if (this.options.pmIgnore === false) { this.pm = new L.PM.Edit.Circle(this); } @@ -155,7 +165,49 @@ L.PM = L.PM || { } L.Circle.addInitHook(initCircle); + + + function initImageOverlay() { + this.pm = undefined; + + if (L.PM.optIn) { + if (this.options.pmIgnore === false) { + this.pm = new L.PM.Edit.ImageOverlay(this); + } + } else if (!this.options.pmIgnore) { + this.pm = new L.PM.Edit.ImageOverlay(this); + } + } + + L.ImageOverlay.addInitHook(initImageOverlay); }, + reInitLayer(layer){ + if(layer instanceof L.LayerGroup){ + layer.eachLayer((_layer)=>{ + this.reInitLayer(_layer); + }) + }else if(layer.pm){ + // PM is already added to the layer + }else if(L.PM.optIn && layer.options.pmIgnore !== false){ + // Opt-In is true and pmIgnore is not false + }else if(layer.options.pmIgnore){ + // pmIgnore is true + }else if(layer instanceof L.Map){ + layer.pm = new L.PM.Map(layer); + }else if(layer instanceof L.Marker){ + layer.pm = new L.PM.Edit.Marker(layer); + }else if(layer instanceof L.Circle){ + layer.pm = new L.PM.Edit.Circle(layer); + }else if(layer instanceof L.CircleMarker){ + layer.pm = new L.PM.Edit.CircleMarker(layer); + }else if(layer instanceof L.Rectangle){ + layer.pm = new L.PM.Edit.Rectangle(layer); + }else if(layer instanceof L.Polygon){ + layer.pm = new L.PM.Edit.Polygon(layer); + }else if(layer instanceof L.Polyline){ + layer.pm = new L.PM.Edit.Line(layer); + } + } }; // initialize leaflet-geoman diff --git a/src/js/Mixins/Dragging.js b/src/js/Mixins/Dragging.js index 56e9ce57..9063bd01 100644 --- a/src/js/Mixins/Dragging.js +++ b/src/js/Mixins/Dragging.js @@ -1,3 +1,5 @@ +import Utils from "../L.PM.Utils"; + const DragMixin = { enableLayerDrag() { // before enabling layer drag, disable layer editing @@ -22,6 +24,8 @@ const DragMixin = { this._layer.dragging.enable(); } return; + }else if(this._layer instanceof L.ImageOverlay){ + this._getDOMElem().ondragstart = ()=>false; } // temporary coord variable for delta calculation @@ -68,10 +72,55 @@ const DragMixin = { // disable mousedown event this._layer.off('mousedown', this._dragMixinOnMouseDown, this); }, + dragging() { + return this._dragging; + }, + _dragMixinOnMouseDown(e) { + // cancel if mouse button is NOT the left button + if (e.originalEvent.button > 0) { + return; + } + // save current map dragging state + if (this._safeToCacheDragState) { + this._originalMapDragState = this._layer._map.dragging._enabled; + + // don't cache the state again until another mouse up is registered + this._safeToCacheDragState = false; + } + + // save for delta calculation + this._tempDragCoord = e.latlng; + + this._layer._map.on('mouseup', this._dragMixinOnMouseUp, this); + + // listen to mousemove on map (instead of polygon), + // otherwise fast mouse movements stop the drag + this._layer._map.on('mousemove', this._dragMixinOnMouseMove, this); + }, + _dragMixinOnMouseMove(e) { + const el = this._getDOMElem(); + + if (!this._dragging) { + // set state + this._dragging = true; + L.DomUtil.addClass(el, 'leaflet-pm-dragging'); + + // bring it to front to prevent drag interception + this._layer.bringToFront(); + + // disbale map drag + if (this._originalMapDragState) { + this._layer._map.dragging.disable(); + } + + // fire pm:dragstart event + this._fireDragStart(); + } + + this._onLayerDrag(e); + }, _dragMixinOnMouseUp() { - const el = this._layer._path - ? this._layer._path - : this._layer._renderer._container; + const el = this._getDOMElem(); // re-enable map drag if (this._originalMapDragState) { @@ -92,7 +141,6 @@ const DragMixin = { return false; } - // update the hidden circle border after dragging if(this._layer instanceof L.CircleMarker){ this._layer.pm._updateHiddenPolyCircle(); @@ -114,56 +162,6 @@ const DragMixin = { return true; }, - _dragMixinOnMouseMove(e) { - const el = this._layer._path - ? this._layer._path - : this._layer._renderer._container; - - if (!this._dragging) { - // set state - this._dragging = true; - L.DomUtil.addClass(el, 'leaflet-pm-dragging'); - - // bring it to front to prevent drag interception - this._layer.bringToFront(); - - // disbale map drag - if (this._originalMapDragState) { - this._layer._map.dragging.disable(); - } - - - // fire pm:dragstart event - this._fireDragStart(); - } - - this._onLayerDrag(e); - }, - _dragMixinOnMouseDown(e) { - // cancel if mouse button is NOT the left button - if (e.originalEvent.button > 0) { - return; - } - // save current map dragging state - if (this._safeToCacheDragState) { - this._originalMapDragState = this._layer._map.dragging._enabled; - - // don't cache the state again until another mouse up is registered - this._safeToCacheDragState = false; - } - - // save for delta calculation - this._tempDragCoord = e.latlng; - - this._layer._map.on('mouseup', this._dragMixinOnMouseUp, this); - - // listen to mousemove on map (instead of polygon), - // otherwise fast mouse movements stop the drag - this._layer._map.on('mousemove', this._dragMixinOnMouseMove, this); - }, - dragging() { - return this._dragging; - }, _onLayerDrag(e) { // latLng of mouse event const { latlng } = e; @@ -195,6 +193,11 @@ const DragMixin = { const newCoords = moveCoords([this._layer.getLatLng()]); // set new coordinates and redraw this._layer.setLatLng(newCoords[0]); + } else if( this._layer instanceof L.ImageOverlay){ + // create the new coordinates array + const newCoords = moveCoords([this._layer.getBounds().getNorthWest(),this._layer.getBounds().getSouthEast()]); + // set new coordinates and redraw + this._layer.setBounds(newCoords); } else { // create the new coordinates array const newCoords = moveCoords(this._layer.getLatLngs()); @@ -211,31 +214,42 @@ const DragMixin = { this._fireDrag(e); }, _fireDragStart() { - this._layer.fire('pm:dragstart', { + Utils._fireEvent(this._layer,'pm:dragstart', { layer: this._layer, shape: this.getShape() }); }, _fireDrag(e) { - this._layer.fire('pm:drag', Object.assign({},e, {shape:this.getShape()})); + Utils._fireEvent(this._layer,'pm:drag', Object.assign({},e, {shape:this.getShape()})); }, _fireDragEnd() { - this._layer.fire('pm:dragend', { + Utils._fireEvent(this._layer,'pm:dragend', { layer: this._layer, shape: this.getShape() }); }, addDraggingClass() { - const el = this._layer._path - ? this._layer._path - : this._layer._renderer._container; - L.DomUtil.addClass(el, 'leaflet-pm-draggable'); + const el = this._getDOMElem(); + if(el) { + L.DomUtil.addClass(el, 'leaflet-pm-draggable'); + } }, removeDraggingClass() { - const el = this._layer._path - ? this._layer._path - : this._layer._renderer._container; - L.DomUtil.removeClass(el, 'leaflet-pm-draggable'); + const el = this._getDOMElem(); + if(el) { + L.DomUtil.removeClass(el, 'leaflet-pm-draggable'); + } + }, + _getDOMElem(){ + let el = null; + if(this._layer._path){ + el = this._layer._path; + }else if(this._layer._renderer && this._layer._renderer._container){ + el = this._layer._renderer._container; + }else if(this._layer._image){ + el = this._layer._image; + } + return el; } }; diff --git a/src/js/Mixins/Modes/Mode.Drag.js b/src/js/Mixins/Modes/Mode.Drag.js index 4352f0f4..b7dbb27b 100644 --- a/src/js/Mixins/Modes/Mode.Drag.js +++ b/src/js/Mixins/Modes/Mode.Drag.js @@ -1,11 +1,8 @@ import Utils from '../../L.PM.Utils' -const { findLayers } = Utils +const { findLayers } = Utils; const GlobalDragMode = { - globalDragModeEnabled() { - return !!this._globalDragMode; - }, enableGlobalDragMode() { const layers = findLayers(this.map); @@ -44,11 +41,8 @@ const GlobalDragMode = { this._fireDragModeEvent(false); }, - _fireDragModeEvent(enabled) { - this.map.fire('pm:globaldragmodetoggled', { - enabled, - map: this.map, - }); + globalDragModeEnabled() { + return !!this._globalDragMode; }, toggleGlobalDragMode() { if (this.globalDragModeEnabled()) { @@ -58,7 +52,7 @@ const GlobalDragMode = { } }, reinitGlobalDragMode({ layer }) { - // do nothing if layer is not handled by leaflet so it doesn't fire unnecessarily + // do nothing if layer is not handled by leaflet so it doesn't fire unnecessarily const isRelevant = !!layer.pm && !layer._pmTempLayer; if (!isRelevant) { return; @@ -70,6 +64,12 @@ const GlobalDragMode = { this.enableGlobalDragMode(); } }, + _fireDragModeEvent(enabled) { + Utils._fireEvent(this.map,'pm:globaldragmodetoggled', { + enabled, + map: this.map, + }); + }, } -export default GlobalDragMode \ No newline at end of file +export default GlobalDragMode diff --git a/src/js/Mixins/Modes/Mode.Edit.js b/src/js/Mixins/Modes/Mode.Edit.js index fd4427a7..44214bf6 100644 --- a/src/js/Mixins/Modes/Mode.Edit.js +++ b/src/js/Mixins/Modes/Mode.Edit.js @@ -1,29 +1,15 @@ // this mixin adds a global edit mode to the map import Utils from '../../L.PM.Utils' -const { findLayers } = Utils +const { findLayers } = Utils; const GlobalEditMode = { _globalEditMode: false, - // TODO: Remove in the next major release - globalEditEnabled() { - return this.globalEditModeEnabled(); - }, - globalEditModeEnabled() { - return this._globalEditMode; - }, - setGlobalEditStatus(status) { - // set status - this._globalEditMode = status; - - // fire event - this._fireEditModeEvent(this._globalEditMode); - }, enableGlobalEditMode(o) { const options = { snappable: this._globalSnappingEnabled, ...o - } + }; const status = true; @@ -42,6 +28,9 @@ const GlobalEditMode = { this.throttledReInitEdit = L.Util.throttle(this.handleLayerAdditionInGlobalEditMode, 100, this) } + // save the added layers into the _addedLayers array, to read it later out + this._addedLayers = []; + this.map.on('layeradd', this._layerAdded, this); // handle layers that are added while in removal mode this.map.on('layeradd', this.throttledReInitEdit, this); @@ -59,16 +48,27 @@ const GlobalEditMode = { }); // cleanup layer off event - this.map.off('layeroff', this.throttledReInitEdit, this); + this.map.off('layeradd', this.throttledReInitEdit, this); // Set toolbar button to currect status this.Toolbar.toggleButton('editMode', status); this.setGlobalEditStatus(status); }, + // TODO: Remove in the next major release + globalEditEnabled() { + return this.globalEditModeEnabled(); + }, + globalEditModeEnabled() { + return this._globalEditMode; + }, + setGlobalEditStatus(status) { + // set status + this._globalEditMode = status; + // fire event + this._fireEditModeEvent(this._globalEditMode); + }, toggleGlobalEditMode(options = this.globalOptions) { - // console.log('toggle global edit mode', options); - if (this.globalEditModeEnabled()) { // disable this.disableGlobalEditMode(); @@ -77,27 +77,33 @@ const GlobalEditMode = { this.enableGlobalEditMode(options); } }, - handleLayerAdditionInGlobalEditMode({ layer }) { - // when global edit mode is enabled and a layer is added to the map, - // enable edit for that layer if it's relevant - - // do nothing if layer is not handled by leaflet so it doesn't fire unnecessarily - const isRelevant = !!layer.pm && !layer._pmTempLayer; - if (!isRelevant) { - return; - } - - if (this.globalEditModeEnabled()) { - layer.pm.enable({ ...this.globalOptions, snappable: this._globalSnappingEnabled }); - } + handleLayerAdditionInGlobalEditMode() { + const layers = this._addedLayers; + this._addedLayers = []; + layers.forEach((layer)=> { + // when global edit mode is enabled and a layer is added to the map, + // enable edit for that layer if it's relevant + + // do nothing if layer is not handled by leaflet so it doesn't fire unnecessarily + const isRelevant = !!layer.pm && !layer._pmTempLayer; + if (!isRelevant) { + return; + } + + if (this.globalEditModeEnabled()) { + layer.pm.enable({...this.globalOptions}); + } + }); + }, + _layerAdded({layer}){ + this._addedLayers.push(layer); }, _fireEditModeEvent(enabled) { - this.map.fire('pm:globaleditmodetoggled', { + Utils._fireEvent(this.map,'pm:globaleditmodetoggled', { enabled, map: this.map, }); }, - -} +}; export default GlobalEditMode diff --git a/src/js/Mixins/Modes/Mode.Removal.js b/src/js/Mixins/Modes/Mode.Removal.js index b22f99b5..448e2bd1 100644 --- a/src/js/Mixins/Modes/Mode.Removal.js +++ b/src/js/Mixins/Modes/Mode.Removal.js @@ -1,18 +1,6 @@ -const GlobalRemovalMode = { - disableGlobalRemovalMode() { - this._globalRemovalMode = false; - this.map.eachLayer(layer => { - layer.off('click', this.removeLayer, this); - }); - - // remove map handler - this.map.off('layeradd', this.throttledReInitRemoval, this); +import Utils from "../../L.PM.Utils"; - // toogle the button in the toolbar if this is called programatically - this.Toolbar.toggleButton('deleteLayer', this._globalRemovalMode); - - this._fireRemovalModeEvent(false); - }, +const GlobalRemovalMode = { enableGlobalRemovalMode() { const isRelevant = layer => layer.pm && @@ -39,11 +27,26 @@ const GlobalRemovalMode = { this._fireRemovalModeEvent(true); }, - _fireRemovalModeEvent(enabled) { - this.map.fire('pm:globalremovalmodetoggled', { - enabled, - map: this.map, + disableGlobalRemovalMode() { + this._globalRemovalMode = false; + this.map.eachLayer(layer => { + layer.off('click', this.removeLayer, this); }); + + // remove map handler + this.map.off('layeradd', this.throttledReInitRemoval, this); + + // toogle the button in the toolbar if this is called programatically + this.Toolbar.toggleButton('deleteLayer', this._globalRemovalMode); + + this._fireRemovalModeEvent(false); + }, + // TODO: Remove in the next major release + globalRemovalEnabled() { + return this.globalRemovalModeEnabled(); + }, + globalRemovalModeEnabled() { + return !!this._globalRemovalMode; }, toggleGlobalRemovalMode() { // toggle global edit mode @@ -53,15 +56,26 @@ const GlobalRemovalMode = { this.enableGlobalRemovalMode(); } }, - // TODO: Remove in the next major release - globalRemovalEnabled() { - return this.globalRemovalModeEnabled(); + reinitGlobalRemovalMode({ layer }) { + // do nothing if layer is not handled by leaflet so it doesn't fire unnecessarily + const isRelevant = !!layer.pm && !layer._pmTempLayer; + if (!isRelevant) { + return; + } + + // re-enable global removal mode if it's enabled already + if (this.globalRemovalModeEnabled()) { + this.disableGlobalRemovalMode(); + this.enableGlobalRemovalMode(); + } }, - globalRemovalModeEnabled() { - return !!this._globalRemovalMode; + _fireRemovalModeEvent(enabled) { + Utils._fireEvent(this.map,'pm:globalremovalmodetoggled', { + enabled, + map: this.map, + }); }, removeLayer(e) { - const layer = e.target; // only remove layer, if it's handled by leaflet-geoman, // not a tempLayer and not currently being dragged @@ -69,30 +83,18 @@ const GlobalRemovalMode = { !layer._pmTempLayer && (!layer.pm || !layer.pm.dragging()); if (removeable) { + layer.removeFrom(this.map.pm._getContainingLayer()); layer.remove(); if(layer instanceof L.LayerGroup){ - layer.fire('pm:remove', { layer, shape: undefined }); - this.map.fire('pm:remove', { layer, shape: undefined }); + Utils._fireEvent(layer,'pm:remove', { layer, shape: undefined }); + Utils._fireEvent(this.map,'pm:remove', { layer, shape: undefined }); }else{ - layer.fire('pm:remove', { layer, shape: layer.pm.getShape() }); - this.map.fire('pm:remove', { layer, shape: layer.pm.getShape() }); + Utils._fireEvent(layer,'pm:remove', { layer, shape: layer.pm.getShape() }); + Utils._fireEvent(this.map,'pm:remove', { layer, shape: layer.pm.getShape() }); } } }, - reinitGlobalRemovalMode({ layer }) { - // do nothing if layer is not handled by leaflet so it doesn't fire unnecessarily - const isRelevant = !!layer.pm && !layer._pmTempLayer; - if (!isRelevant) { - return; - } - - // re-enable global removal mode if it's enabled already - if (this.globalRemovalModeEnabled()) { - this.disableGlobalRemovalMode(); - this.enableGlobalRemovalMode(); - } - }, -} +}; export default GlobalRemovalMode diff --git a/src/js/Mixins/Snapping.js b/src/js/Mixins/Snapping.js index 4663a658..0fa0b010 100644 --- a/src/js/Mixins/Snapping.js +++ b/src/js/Mixins/Snapping.js @@ -1,4 +1,5 @@ import Utils from '../L.PM.Utils'; +import {isEmptyDeep, prioritiseSort} from "../helpers"; const SnapMixin = { _initSnappableMarkers() { @@ -30,15 +31,16 @@ const SnapMixin = { marker.on('dragend', this._cleanupSnapping, this); }); }, - _unsnap() { - // delete the last snap - delete this._snapLatLng; - }, _cleanupSnapping() { // 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; + if (this.throttledList) { + this._map.off('layeradd', this.throttledList, this); + this.throttledList = undefined; + } + // remove map event this._map.off('pm:remove', this._handleSnapLayerRemoval, this); @@ -48,17 +50,10 @@ const SnapMixin = { }); } }, - _handleSnapLayerRemoval({ layer }) { - // find the layers index in snaplist - const index = this._snapList.findIndex( - e => e._leaflet_id === layer._leaflet_id - ); - // remove it from the snaplist - this._snapList.splice(index, 1); - }, _handleSnapping(e) { - function throttledList() { - return L.Util.throttle(this._createSnapList, 100, this); + + if (!this.throttledList) { + this.throttledList = L.Util.throttle(this._createSnapList, 100, this); } // if snapping is disabled via holding ALT during drag, stop right here @@ -73,8 +68,8 @@ const SnapMixin = { this._createSnapList(); // re-create the snaplist again when a layer is added during draw - this._map.off('layeradd', throttledList, this); - this._map.on('layeradd', throttledList, this); + this._map.off('layeradd', this.throttledList, this); + this._map.on('layeradd', this.throttledList, this); } // if there are no layers to snap to, stop here @@ -91,7 +86,7 @@ const SnapMixin = { ); // if no layers found. Can happen when circle is the only visible layer on the map and the hidden snapping-border circle layer is also on the map - if(Object.keys(closestLayer).length === 0){ + if (Object.keys(closestLayer).length === 0) { return false; } @@ -122,19 +117,21 @@ const SnapMixin = { distance: closestLayer.distance, }; - eventInfo.marker.fire('pm:snapdrag', eventInfo); - this._layer.fire('pm:snapdrag', eventInfo); + Utils._fireEvent(eventInfo.marker,'pm:snapdrag', eventInfo); + Utils._fireEvent(this._layer,'pm:snapdrag', eventInfo); if (closestLayer.distance < minDistance) { // snap the marker + marker._orgLatLng = marker.getLatLng(); marker.setLatLng(snapLatLng); marker._snapped = true; + marker._snapInfo = eventInfo; const triggerSnap = () => { this._snapLatLng = snapLatLng; - marker.fire('pm:snap', eventInfo); - this._layer.fire('pm:snap', eventInfo); + Utils._fireEvent(marker,'pm:snap', eventInfo); + Utils._fireEvent(this._layer,'pm:snap', eventInfo); }; // check if the snapping position differs from the last snap @@ -155,66 +152,12 @@ const SnapMixin = { marker._snapped = false; // and fire unsnap event - eventInfo.marker.fire('pm:unsnap', eventInfo); - this._layer.fire('pm:unsnap', eventInfo); + Utils._fireEvent(eventInfo.marker,'pm:unsnap', eventInfo); + Utils._fireEvent(this._layer,'pm:unsnap', eventInfo); } return true; }, - - // 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 - _checkPrioritiySnapping(closestLayer) { - const map = this._map; - - // A and B are the points of the closest segment to P (the marker position we want to snap) - const A = closestLayer.segment[0]; - const B = closestLayer.segment[1]; - - // C is the point we would snap to on the segment. - // The closest point on the closest segment of the closest polygon to P. That's right. - const C = closestLayer.latlng; - - // distances from A to C and B to C to check which one is closer to C - const distanceAC = this._getDistance(map, A, C); - const distanceBC = this._getDistance(map, B, C); - - // closest latlng of A and B to C - let closestVertexLatLng = distanceAC < distanceBC ? A : B; - - // distance between closestVertexLatLng and C - let shortestDistance = distanceAC < distanceBC ? distanceAC : distanceBC; - - // snap to middle (M) of segment if option is enabled - if (this.options.snapMiddle) { - const M = Utils.calcMiddleLatLng(map, A, B); - const distanceMC = this._getDistance(map, M, C); - - if (distanceMC < distanceAC && distanceMC < distanceBC) { - // M is the nearest vertex - closestVertexLatLng = M; - shortestDistance = distanceMC; - } - } - - // the distance that needs to be undercut to trigger priority - const priorityDistance = this.options.snapDistance; - - // the latlng we ultemately want to snap to - let snapLatlng; - - // if C is closer to the closestVertexLatLng (A, B or M) than the snapDistance, - // the closestVertexLatLng has priority over C as the snapping point. - if (shortestDistance < priorityDistance) { - snapLatlng = closestVertexLatLng; - } else { - snapLatlng = C; - } - - // return the copy of snapping point - return Object.assign({}, snapLatlng); - }, - _createSnapList() { let layers = []; const debugIndicatorLines = []; @@ -229,18 +172,24 @@ const SnapMixin = { if ( (layer instanceof L.Polyline || layer instanceof L.Marker || - layer instanceof L.CircleMarker) && - layer.options.snapIgnore !== true + layer instanceof L.CircleMarker || + layer instanceof L.ImageOverlay) && + layer.options.snapIgnore !== true && + ( + (!L.PM.optIn && !layer.options.pmIgnore) || // if optIn is not set / true and pmIgnore is not set / true (default) + (L.PM.optIn && layer.options.pmIgnore === false) // if optIn is true and pmIgnore is false + ) ) { - // adds a hidden polygon which matches the border of the circle - if ((layer instanceof L.Circle || layer instanceof L.CircleMarker) && layer.pm._hiddenPolyCircle) { + if ((layer instanceof L.Circle || layer instanceof L.CircleMarker) && layer.pm && layer.pm._hiddenPolyCircle) { layers.push(layer.pm._hiddenPolyCircle); + } else if (layer instanceof L.ImageOverlay) { + layer = L.rectangle(layer.getBounds()); } layers.push(layer); // this is for debugging - const debugLine = L.polyline([], { color: 'red', pmIgnore: true }); + const debugLine = L.polyline([], {color: 'red', pmIgnore: true}); debugLine._pmTempLayer = true; debugIndicatorLines.push(debugLine); if (layer instanceof L.Circle || layer instanceof L.CircleMarker) { @@ -257,7 +206,7 @@ const SnapMixin = { // also remove everything that has no coordinates yet layers = layers.filter( - layer => layer._latlng || (layer._latlngs && layer._latlngs.length > 0) + layer => layer._latlng || (layer._latlngs && !isEmptyDeep(layer._latlngs)) ); // finally remove everything that's leaflet-geoman specific temporary stuff @@ -272,8 +221,17 @@ const SnapMixin = { this.debugIndicatorLines = debugIndicatorLines; }, + _handleSnapLayerRemoval({layer}) { + // find the layers index in snaplist + const index = this._snapList.findIndex( + e => e._leaflet_id === layer._leaflet_id + ); + // remove it from the snaplist + this._snapList.splice(index, 1); + }, _calcClosestLayer(latlng, layers) { // the closest polygon to our dragged marker latlng + let closestLayers = []; let closestLayer = {}; // loop through the layers @@ -291,18 +249,21 @@ const SnapMixin = { // 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 + results.distance <= closestLayer.distance ) { + if (results.distance < closestLayer.distance) { + closestLayers = []; + } closestLayer = results; closestLayer.layer = layer; + closestLayers.push(closestLayer); } }); // return the closest layer and it's data - // if there is no closest layer, return undefined - return closestLayer; + // if there is no closest layer, return an empty object + return this._getClosestLayerByPriority(closestLayers); }, - _calcLayerDistances(latlng, layer) { const map = this._map; @@ -384,7 +345,83 @@ const SnapMixin = { distance: shortestDistance, }; }, + _getClosestLayerByPriority(layers) { + // 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); + + const shapes = ['Marker', 'CircleMarker', 'Circle', 'Line', 'Polygon', 'Rectangle']; + const order = this._map.pm.globalOptions.snappingOrder || []; + + let lastIndex = 0; + const prioOrder = {}; + // merge user-preferred priority with default priority + order.concat(shapes).forEach((shape) => { + if (!prioOrder[shape]) { + lastIndex += 1; + prioOrder[shape] = lastIndex; + } + }); + + // sort layers by priority + layers.sort(prioritiseSort('instanceofShape', prioOrder)); + return layers[0] || {}; + }, + // 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 + _checkPrioritiySnapping(closestLayer) { + const map = this._map; + + // A and B are the points of the closest segment to P (the marker position we want to snap) + const A = closestLayer.segment[0]; + const B = closestLayer.segment[1]; + + // C is the point we would snap to on the segment. + // The closest point on the closest segment of the closest polygon to P. That's right. + const C = closestLayer.latlng; + + // distances from A to C and B to C to check which one is closer to C + const distanceAC = this._getDistance(map, A, C); + const distanceBC = this._getDistance(map, B, C); + + // closest latlng of A and B to C + let closestVertexLatLng = distanceAC < distanceBC ? A : B; + + // distance between closestVertexLatLng and C + let shortestDistance = distanceAC < distanceBC ? distanceAC : distanceBC; + + // snap to middle (M) of segment if option is enabled + if (this.options.snapMiddle) { + const M = Utils.calcMiddleLatLng(map, A, B); + const distanceMC = this._getDistance(map, M, C); + + if (distanceMC < distanceAC && distanceMC < distanceBC) { + // M is the nearest vertex + closestVertexLatLng = M; + shortestDistance = distanceMC; + } + } + + // the distance that needs to be undercut to trigger priority + const priorityDistance = this.options.snapDistance; + + // the latlng we ultemately want to snap to + let snapLatlng; + // if C is closer to the closestVertexLatLng (A, B or M) than the snapDistance, + // the closestVertexLatLng has priority over C as the snapping point. + if (shortestDistance < priorityDistance) { + snapLatlng = closestVertexLatLng; + } else { + snapLatlng = C; + } + + // return the copy of snapping point + return Object.assign({}, snapLatlng); + }, + _unsnap() { + // delete the last snap + delete this._snapLatLng; + }, _getClosestPointOnSegment(map, latlng, latlngA, latlngB) { let maxzoom = map.getMaxZoom(); if (maxzoom === Infinity) { diff --git a/src/js/Toolbar/L.Controls.js b/src/js/Toolbar/L.Controls.js index 8fddebed..fe9356a7 100644 --- a/src/js/Toolbar/L.Controls.js +++ b/src/js/Toolbar/L.Controls.js @@ -1,4 +1,5 @@ import { getTranslation } from '../helpers'; +import Utils from "../L.PM.Utils"; const PMButton = L.Control.extend({ options: { @@ -136,8 +137,23 @@ const PMButton = L.Control.extend({ actionNode.innerHTML = action.text; - if (action.onClick) { - L.DomEvent.addListener(actionNode, 'click', action.onClick, this); + if(!button.disabled) { + if (action.onClick) { + const actionClick = () => { + let btnName = ""; + const {buttons} = this._map.pm.Toolbar; + for (const btn in buttons) { + if (buttons[btn]._button === button) { + btnName = btn; + break; + } + } + Utils._fireEvent(this._map,'pm:actionclick', {text: action.text, action, btnName, button}); + }; + + L.DomEvent.addListener(actionNode, 'click', actionClick, this); + L.DomEvent.addListener(actionNode, 'click', action.onClick, this); + } } L.DomEvent.disableClickPropagation(actionNode); }); @@ -158,14 +174,30 @@ const PMButton = L.Control.extend({ if (button.className) { L.DomUtil.addClass(image, button.className); } - // before the actual click, trigger a click on currently toggled buttons to - // untoggle them and their functionality - L.DomEvent.addListener(newButton, 'click', () => { - if (this._button.disableOtherButtons) { - this._map.pm.Toolbar.triggerClickOnToggledButtons(this); - } - }); - L.DomEvent.addListener(newButton, 'click', this._triggerClick, this); + if(!button.disabled) { + // before the actual click, trigger a click on currently toggled buttons to + // untoggle them and their functionality + L.DomEvent.addListener(newButton, 'click', () => { + if (this._button.disableOtherButtons) { + this._map.pm.Toolbar.triggerClickOnToggledButtons(this); + } + let btnName = ""; + const {buttons} = this._map.pm.Toolbar; + for (const btn in buttons) { + if (buttons[btn]._button === button) { + btnName = btn; + break; + } + } + Utils._fireEvent(this._map,'pm:buttonclick', {btnName, button}); + }); + L.DomEvent.addListener(newButton, 'click', this._triggerClick, this); + } + + if(button.disabled){ + L.DomUtil.addClass(newButton, 'pm-disabled'); + L.DomUtil.addClass(image, 'pm-disabled'); + } L.DomEvent.disableClickPropagation(newButton); return buttonContainer; diff --git a/src/js/Toolbar/L.PM.Toolbar.js b/src/js/Toolbar/L.PM.Toolbar.js index ffb44060..9ac09e99 100644 --- a/src/js/Toolbar/L.PM.Toolbar.js +++ b/src/js/Toolbar/L.PM.Toolbar.js @@ -429,21 +429,7 @@ const Toolbar = L.Class.extend({ options = { name: options }; } - const shapeMapping = { - "Marker": "drawMarker", - "Circle": "drawCircle", - "Polygon": "drawPolygon", - "Rectangle": "drawRectangle", - "Polyline": "drawPolyline", - "Line": "drawPolyline", - "CircleMarker": "drawCircleMarker", - "Edit": "editMode", - "Drag": "dragMode", - "Cut": "cutPolygon", - "Removal": "removalMode" - }; - - const instance = shapeMapping[copyInstance] ? shapeMapping[copyInstance] : copyInstance; + const instance = this._btnNameMapping(copyInstance); if (!options.name) { throw new TypeError( @@ -492,7 +478,9 @@ const Toolbar = L.Class.extend({ options.block = ""; } - if (options.className.indexOf('control-icon') === -1) { + if (!options.className){ + options.className = 'control-icon'; + }else if(options.className.indexOf('control-icon') === -1) { options.className = `control-icon ${options.className}`; } @@ -509,6 +497,7 @@ const Toolbar = L.Class.extend({ cssToggle: options.toggle, position: this.options.position, actions: options.actions || [], + disabled: !!options.disabled, }; if (this.options[options.name] !== false) { @@ -521,19 +510,7 @@ const Toolbar = L.Class.extend({ }, changeControlOrder(order = []) { - const shapeMapping = { - "Marker": "drawMarker", - "Circle": "drawCircle", - "Polygon": "drawPolygon", - "Rectangle": "drawRectangle", - "Polyline": "drawPolyline", - "Line": "drawPolyline", - "CircleMarker": "drawCircleMarker", - "Edit": "editMode", - "Drag": "dragMode", - "Cut": "cutPolygon", - "Removal": "removalMode" - }; + const shapeMapping = this._shapeMapping(); const _order = []; order.forEach((shape) => { @@ -597,21 +574,7 @@ const Toolbar = L.Class.extend({ return order; }, changeActionsOfControl(name, actions) { - const shapeMapping = { - "Marker": "drawMarker", - "Circle": "drawCircle", - "Polygon": "drawPolygon", - "Rectangle": "drawRectangle", - "Polyline": "drawPolyline", - "Line": "drawPolyline", - "CircleMarker": "drawCircleMarker", - "Edit": "editMode", - "Drag": "dragMode", - "Cut": "cutPolygon", - "Removal": "removalMode" - }; - - const btnName = shapeMapping[name] ? shapeMapping[name] : name; + const btnName = this._btnNameMapping(name); if (!btnName) { throw new TypeError( @@ -631,6 +594,30 @@ const Toolbar = L.Class.extend({ } this.buttons[btnName]._button.actions = actions; this.changeControlOrder(); + }, + setButtonDisabled(name,state){ + const btnName = this._btnNameMapping(name); + this.buttons[btnName]._button.disabled = !!state; + this._showHideButtons(); + }, + _shapeMapping(){ + return { + "Marker": "drawMarker", + "Circle": "drawCircle", + "Polygon": "drawPolygon", + "Rectangle": "drawRectangle", + "Polyline": "drawPolyline", + "Line": "drawPolyline", + "CircleMarker": "drawCircleMarker", + "Edit": "editMode", + "Drag": "dragMode", + "Cut": "cutPolygon", + "Removal": "removalMode" + } + }, + _btnNameMapping(name){ + const shapeMapping = this._shapeMapping(); + return shapeMapping[name] ? shapeMapping[name] : name; } }); diff --git a/src/js/helpers/index.js b/src/js/helpers/index.js index d15014bb..4ad24d3d 100644 --- a/src/js/helpers/index.js +++ b/src/js/helpers/index.js @@ -22,6 +22,15 @@ export function isEmptyDeep(l) { return !flatten(l).length; } +export function removeEmptyCoordRings(arr) { + return arr.reduce((result, item)=>{ + if(item.length !== 0){ + result.push( Array.isArray(item) ? removeEmptyCoordRings(item) : item ); + } + return result; + }, []); +} + // Code from https://stackoverflow.com/a/24153998/8283938 function destinationVincenty(lonlat, brng, dist) { // rewritten to work with leaflet const VincentyConstants = { @@ -30,7 +39,7 @@ function destinationVincenty(lonlat, brng, dist) { // rewritten to work with lea f: 1 / 298.257223563 }; - const { a, b, f } = VincentyConstants; + const {a, b, f} = VincentyConstants; const lon1 = lonlat.lng; const lat1 = lonlat.lat; const s = dist; @@ -75,6 +84,7 @@ function destinationVincenty(lonlat, brng, dist) { // rewritten to work with lea return L.latLng(lamFunc, lat2a); } + export function createGeodesicPolygon(origin, radius, sides, rotation) { let angle; let newLonlat; @@ -90,3 +100,105 @@ export function createGeodesicPolygon(origin, radius, sides, rotation) { return points; } + +/* Copied from L.GeometryUtil */ +function destination(latlng, heading, distance) { + heading = (heading + 360) % 360; + const rad = Math.PI / 180; + const radInv = 180 / Math.PI; + const R = 6378137; // approximation of Earth's radius + const lon1 = latlng.lng * rad; + const lat1 = latlng.lat * rad; + const rheading = heading * rad; + const sinLat1 = Math.sin(lat1); + const cosLat1 = Math.cos(lat1); + const cosDistR = Math.cos(distance / R); + const sinDistR = Math.sin(distance / R); + const lat2 = Math.asin(sinLat1 * cosDistR + cosLat1 * sinDistR * Math.cos(rheading)); + let lon2 = lon1 + Math.atan2(Math.sin(rheading) * sinDistR * cosLat1, cosDistR - sinLat1 * Math.sin(lat2)); + lon2 *= radInv; + lon2 = lon2 > 180 ? lon2 - 360 : lon2 < -180 ? lon2 + 360 : lon2; + return L.latLng([lat2 * radInv, lon2]); +} +/* Copied from L.GeometryUtil */ +function angle(map, latlngA, latlngB) { + const pointA = map.latLngToContainerPoint(latlngA); + const pointB = map.latLngToContainerPoint(latlngB); + let angleDeg = Math.atan2(pointB.y - pointA.y, pointB.x - pointA.x) * 180 / Math.PI + 90; + angleDeg += angleDeg < 0 ? 360 : 0; + return angleDeg; + +} + +export function destinationOnLine(map, latlngA, latlngB, distance) { + const angleDeg = angle(map, latlngA, latlngB); + return destination(latlngA, angleDeg, distance); +} + + +// this function is used with the .sort(prioritiseSort(key, sortingOrder)) function of arrays +export function prioritiseSort(key, _sortingOrder, order = 'asc') { + /* the sorting order has all possible keys (lowercase) with the index and then it is sorted by the key on the object */ + + if(!_sortingOrder || Object.keys(_sortingOrder).length === 0) { + return (a,b)=>a-b; // default sort method + } + + // change the keys to lowercase + const keys = Object.keys(_sortingOrder); + let objKey; + let n = keys.length; + const sortingOrder={}; + while (n--) { + objKey = keys[n]; + sortingOrder[objKey.toLowerCase()] = _sortingOrder[objKey]; + } + + function getShape(layer){ + if(layer instanceof L.Marker){ + return "Marker"; + }else if(layer instanceof L.Circle){ + return "Circle"; + }else if(layer instanceof L.CircleMarker){ + return "CircleMarker"; + }else if(layer instanceof L.Rectangle){ + return "Rectangle"; + }else if(layer instanceof L.Polygon){ + return "Polygon"; + }else if(layer instanceof L.Polyline){ + return "Line"; + }else{ + return undefined; + } + } + + return (a, b) => { + let keyA; + let keyB; + if(key === "instanceofShape"){ + keyA = getShape(a.layer).toLowerCase(); + keyB = getShape(b.layer).toLowerCase(); + if(!keyA || !keyB) return 0; + }else{ + /* eslint-disable-next-line no-prototype-builtins */ + if (!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) return 0; + keyA = a[key].toLowerCase(); + keyB = b[key].toLowerCase(); + } + + const first = + keyA in sortingOrder + ? sortingOrder[keyA] + : Number.MAX_SAFE_INTEGER; + + const second = + keyB in sortingOrder + ? sortingOrder[keyB] + : Number.MAX_SAFE_INTEGER; + + let result = 0; + if (first < second) result = -1; + else if (first > second) result = 1; + return order === 'desc' ? (result * -1) : result; + } +}