From c163e65a94c496b9ba5f449c1b34b9e9b99c7781 Mon Sep 17 00:00:00 2001 From: dqunbp Date: Mon, 8 Jun 2020 20:33:05 +0300 Subject: [PATCH] feat(styles): expose default draw styles - expose draw style merged with default draw styles - use area in sq. meters - refactor imports/exports BREAKING CHANGE: Calculate area in sq. meters --- README.md | 44 +++++-- example/index.html | 17 ++- example/index.js | 73 ++++-------- src/draw-rectangle-mode.js | 208 ++++++++++++++++++++++++++++++++ src/draw-rectangle-styles.js | 47 ++++++++ src/main.js | 223 ++--------------------------------- 6 files changed, 338 insertions(+), 274 deletions(-) create mode 100644 src/draw-rectangle-mode.js create mode 100644 src/draw-rectangle-styles.js diff --git a/README.md b/README.md index ebc5708..cbf0826 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,53 @@ ## Features -- One click drawing - _two click drawing also supported_ +- One/two click drawing - Mobile compabillity - Area square restriction **Optional** ## Install ```bash -npm install --save mapbox-gl-draw-rectangle-restrict-area +npm install --save @mapbox/mapbox-gl-draw mapbox-gl-draw-rectangle-restrict-area ``` -> Don't forget install the peer dependencies. +## Usage -```bash -npm install --save @mapbox/mapbox-gl-draw +```js +import MapboxDraw from "@mapbox/mapbox-gl-draw"; +import DrawRectangle, { + DrawStyles, +} from "mapbox-gl-draw-rectangle-restrict-area"; + +const map = new mapboxgl.Map({ + container: "map", // container id + style: "mapbox://styles/mapbox/streets-v11", + center: [-91.874, 42.76], // starting position + zoom: 12, // starting zoom + modes: Object.assign(MapboxDraw.modes, { + draw_rectangle: DrawRectangle, + }), +}); + +const draw = new MapboxDraw({ + userProperties: true, + displayControlsDefault: false, + styles: DrawStyles, +}); +map.addControl(draw); + +// when mode drawing should be activated +draw.changeMode("draw_rectangle", { + areaLimit: 5 * 1_000_000, // 5 km2, optional + escapeKeyStopsDrawing: true, // default true + allowCreateExceeded: false, // default false + exceedCallsOnEachMove: false, // default false + exceedCallback: (area) => console.log("exceeded!", area), // optional + areaChangedCallback: (area) => console.log("updated", area), // optional +}); ``` -## Usage - -See [example](https://github.com/dqunbp/mapbox-gl-draw-rectangle-restrict-area/blob/master/example/index.js) +[Example](https://github.com/dqunbp/mapbox-gl-draw-rectangle-restrict-area/blob/master/example/index.js) ## License diff --git a/example/index.html b/example/index.html index 4e78254..d8ba5a1 100644 --- a/example/index.html +++ b/example/index.html @@ -20,6 +20,7 @@ body { margin: 0; padding: 0; + font-family: Arial, Helvetica, sans-serif; } #map { position: absolute; @@ -36,17 +37,31 @@ font-family: Arial, Helvetica, sans-serif; font-size: 18px; border: 2px dashed black; - background: lightgreen; + background: lightskyblue; padding: 25px; border-radius: 5px; cursor: pointer; } + + #area-container { + position: absolute; + background-color: whitesmoke; + border: 2px dashed black; + border-radius: 5px; + padding: 15px 25px; + margin: 15px; + top: 0; + left: 0; + } Draw Rectangle Example
+
+

+
diff --git a/example/index.js b/example/index.js index cc909d9..023ae16 100644 --- a/example/index.js +++ b/example/index.js @@ -1,6 +1,7 @@ import MapboxDraw from "@mapbox/mapbox-gl-draw"; -import DrawRectangle from "mapbox-gl-draw-rectangle-restrict-area"; -import defaultDrawThemes from "@mapbox/mapbox-gl-draw/src/lib/theme"; +import DrawRectangle, { + DrawStyles, +} from "mapbox-gl-draw-rectangle-restrict-area"; const OSM_STYLE = { version: 8, @@ -24,72 +25,38 @@ const OSM_STYLE = { ], }; -function updateDrawThemes(themes) { - return defaultDrawThemes - .filter((theme) => !themes.map(({ id }) => id).includes(theme.id)) - .concat(themes); -} - -export const drawStyles = updateDrawThemes([ - { - id: "gl-draw-polygon-fill-active", - type: "fill", - filter: ["all", ["==", "active", "true"], ["==", "$type", "Polygon"]], - paint: { - "fill-color": [ - "case", - ["!", ["to-boolean", ["get", "user_size_exceed"]]], - "#fbb03b", - "#ff0000", - ], - "fill-opacity": 0.2, - }, - }, - { - id: "gl-draw-polygon-stroke-active", - type: "line", - filter: ["all", ["==", "active", "true"], ["==", "$type", "Polygon"]], - layout: { - "line-cap": "round", - "line-join": "round", - }, - paint: { - "line-color": [ - "case", - ["!", ["to-boolean", ["get", "user_size_exceed"]]], - "#fbb03b", - "#ff0000", - ], - "line-dasharray": [0.2, 2], - "line-width": 2, - }, - }, -]); - const map = new mapboxgl.Map({ container: "map", // container id style: OSM_STYLE, center: [-91.874, 42.76], // starting position zoom: 12, // starting zoom modes: Object.assign(MapboxDraw.modes, { - draw_polygon: DrawRectangle, + draw_rectangle: DrawRectangle, }), }); const draw = new MapboxDraw({ userProperties: true, displayControlsDefault: false, - styles: drawStyles, + styles: DrawStyles, }); map.addControl(draw); + +const currenArea = document.getElementById("area"); +currenArea.textContent = "Area 0 m2"; + +function onAreaChanged(area) { + currenArea.textContent = `Area ${area.toFixed(2)} m2`; +} + document.getElementById("draw-rectangle").addEventListener("click", () => { console.log("let's draw!"); - draw.changeMode("draw_polygon", { - areaLimit: 5, // required, km2 - escapeKeyStopsDrawing: true, // optional - allowCreateExceeded: false, // optional - exceedCallsOnEachMove: false, // optional, true - calls exceedCallback on each mouse move - exceedCallback: (area) => console.log("exceeded!", area), // optional - areaChangedCallback: (area) => console.log("area updated", area), // optional + draw.changeMode("draw_rectangle", { + areaLimit: 5 * 1_000_000, // 5 km2, optional + escapeKeyStopsDrawing: true, // default true + allowCreateExceeded: false, // default false + exceedCallsOnEachMove: false, // default false - calls exceedCallback on each mouse move + exceedCallback: (area) => console.log(`area exceeded! ${area.toFixed(2)}`), // optional + areaChangedCallback: onAreaChanged, }); }); diff --git a/src/draw-rectangle-mode.js b/src/draw-rectangle-mode.js new file mode 100644 index 0000000..5451044 --- /dev/null +++ b/src/draw-rectangle-mode.js @@ -0,0 +1,208 @@ +import area from "@turf/area"; +import Constants from "@mapbox/mapbox-gl-draw/src/constants"; +import CommonSelectors from "@mapbox/mapbox-gl-draw/src/lib/common_selectors"; +import createVertex from "@mapbox/mapbox-gl-draw/src/lib/create_vertex"; +import { getIneractionSwitch } from "./switchIteractions"; + +const doubleClickZoom = getIneractionSwitch("doubleClickZoom"); +const dragPan = getIneractionSwitch("dragPan"); + +function getArea(feature) { + return area(feature); +} + +const DrawRectangle = {}; + +DrawRectangle.onSetup = function ({ + areaLimit, + areaChangedCallback, + exceedCallback, + exceedCallsOnEachMove = false, + allowCreateExceeded = false, + escapeKeyStopsDrawing = true, +}) { + const rectangle = this.newFeature({ + type: Constants.geojsonTypes.FEATURE, + properties: {}, + geometry: { + type: Constants.geojsonTypes.POLYGON, + coordinates: [[]], + }, + }); + this.addFeature(rectangle); + + this.clearSelectedFeatures(); + + // Disable iteractions + doubleClickZoom.disable(this); + dragPan.disable(this); + + // Update cursor + this.updateUIClasses({ mouse: Constants.cursors.ADD }); + this.activateUIButton(Constants.types.POLYGON); + this.setActionableState({ trash: true }); + + // Setup mode options + if (areaLimit) this.areaLimit = areaLimit; + this.allowCreateExceeded = allowCreateExceeded; + this.exceedCallsOnEachMove = exceedCallsOnEachMove; + this.escapeStopsDrawing = escapeKeyStopsDrawing; + if (exceedCallback) this.exceedCallback = exceedCallback; + if (areaChangedCallback) this.areaChangedCallback = areaChangedCallback; + + return { + rectangle, + dragMoving: false, + sizeExceeded: false, + currentArea: 0, + }; +}; + +DrawRectangle.onClick = function (state, e) { + // on first click, save clicked point coords as starting for rectangle + if (!state.startPoint) { + const startPoint = [e.lngLat.lng, e.lngLat.lat]; + state.startPoint = startPoint; + this.updateUIClasses({ mouse: Constants.cursors.ADD }); + state.rectangle.updateCoordinate(`0.0`, e.lngLat.lng, e.lngLat.lat); + state.rectangle.updateCoordinate(`0.1`, e.lngLat.lng, e.lngLat.lat); + } else if ( + state.startPoint && + state.startPoint[0] !== e.lngLat.lng && + state.startPoint[1] !== e.lngLat.lat && + state.dragMoving && + (!state.sizeExceeded || this.allowCreateExceeded) + ) { + this.updateUIClasses({ mouse: "pointer" }); + state.endPoint = [e.lngLat.lng, e.lngLat.lat]; + this.changeMode(Constants.modes.SIMPLE_SELECT, { + featureIds: [state.rectangle.id], + }); + } +}; + +DrawRectangle.onMouseUp = DrawRectangle.onClick; +DrawRectangle.onMouseDown = DrawRectangle.onClick; +DrawRectangle.onTouchStart = DrawRectangle.onClick; +DrawRectangle.onTouchEnd = function (state, e) { + if ( + state.startPoint && + state.startPoint[0] !== e.lngLat.lng && + state.startPoint[1] !== e.lngLat.lat + ) { + DrawRectangle.onMouseMove(state, e); + this.updateUIClasses({ mouse: "pointer" }); + state.endPoint = [e.lngLat.lng, e.lngLat.lat]; + this.changeMode(Constants.modes.SIMPLE_SELECT, { + featureIds: [state.rectangle.id], + }); + } else DrawRectangle.onClick(state, e); +}; + +DrawRectangle.onTap = function (state, e) { + if (!state.startPoint) this.onClick(state, e); +}; + +DrawRectangle.onMouseMove = function (state, e) { + state.dragMoving = true; + if (CommonSelectors.isVertex(e)) { + this.updateUIClasses({ mouse: Constants.cursors.POINTER }); + } + if (state.startPoint) { + state.rectangle.updateCoordinate("0.1", e.lngLat.lng, state.startPoint[1]); // maxX, minY + state.rectangle.updateCoordinate("0.2", e.lngLat.lng, e.lngLat.lat); // maxX, maxY + state.rectangle.updateCoordinate("0.3", state.startPoint[0], e.lngLat.lat); // minX,maxY + state.rectangle.updateCoordinate( + "0.4", + state.startPoint[0], + state.startPoint[1] + ); + } else { + state.rectangle.updateCoordinate(`0.0`, e.lngLat.lng, e.lngLat.lat); + } + if (this.areaLimit) { + let area = getArea(state.rectangle); + if (area > 0) + if (state.currentArea !== area && this.areaChangedCallback) + this.areaChangedCallback(area); + state.currentArea = area; + if (area > this.areaLimit) { + if (!state.sizeExceeded || this.exceedCallsOnEachMove) + if (this.exceedCallback) this.exceedCallback(area); + state.sizeExceeded = true; + state.rectangle.properties.size_exceed = true; + } else { + state.sizeExceeded = false; + state.rectangle.properties.size_exceed = false; + } + } +}; +DrawRectangle.onDrag = DrawRectangle.onMouseMove; +DrawRectangle.onTouchMove = DrawRectangle.onMouseMove; + +DrawRectangle.onStop = function (state) { + this.updateUIClasses({ mouse: Constants.cursors.NONE }); + + // Enable iteractions + doubleClickZoom.enable(this); + dragPan.enable(this); + this.activateUIButton(); + + // check to see if we've deleted this feature + if (this.getFeature(state.rectangle.id) === undefined) return; + + //remove last added coordinate + state.rectangle.removeCoordinate("0.5"); + if (state.rectangle.isValid()) { + this.map.fire(Constants.events.CREATE, { + features: [state.rectangle.toGeoJSON()], + }); + } else { + this.deleteFeature([state.rectangle.id], { silent: true }); + this.changeMode(Constants.modes.SIMPLE_SELECT, {}, { silent: true }); + } +}; + +DrawRectangle.onKeyUp = function (state, e) { + if (CommonSelectors.isEscapeKey(e)) + if (this.escapeStopsDrawing) { + this.deleteFeature([state.rectangle.id], { silent: true }); + this.changeMode(Constants.modes.SIMPLE_SELECT); + } else if (CommonSelectors.isEnterKey(e)) { + this.changeMode(Constants.modes.SIMPLE_SELECT, { + featureIds: [state.rectangle.id], + }); + } +}; + +DrawRectangle.onTrash = function (state) { + this.deleteFeature([state.rectangle.id], { silent: true }); + this.changeMode(Constants.modes.SIMPLE_SELECT); +}; + +DrawRectangle.toDisplayFeatures = function (state, geojson, display) { + const isActivePolygon = geojson.properties.id === state.rectangle.id; + geojson.properties.active = isActivePolygon + ? Constants.activeStates.ACTIVE + : Constants.activeStates.INACTIVE; + if (!isActivePolygon) return display(geojson); + + if (geojson.geometry.coordinates.length === 0) return; + const coordinateCount = geojson.geometry.coordinates[0].length; + + if (coordinateCount < 3) return; + display( + createVertex( + state.rectangle.id, + geojson.geometry.coordinates[0][0], + "0.0", + false + ) + ); + geojson.properties.meta = Constants.meta.FEATURE; + + if (!state.startPoint) return; + return display(geojson); +}; + +export default DrawRectangle; diff --git a/src/draw-rectangle-styles.js b/src/draw-rectangle-styles.js new file mode 100644 index 0000000..5013ffa --- /dev/null +++ b/src/draw-rectangle-styles.js @@ -0,0 +1,47 @@ +import defaultDrawThemes from "@mapbox/mapbox-gl-draw/src/lib/theme"; + +const ActivePolygonStyles = [ + { + id: "gl-draw-polygon-fill-active", + type: "fill", + filter: ["all", ["==", "active", "true"], ["==", "$type", "Polygon"]], + paint: { + "fill-color": [ + "case", + ["!", ["to-boolean", ["get", "user_size_exceed"]]], + "#fbb03b", + "#ff0000", + ], + "fill-opacity": 0.2, + }, + }, + { + id: "gl-draw-polygon-stroke-active", + type: "line", + filter: ["all", ["==", "active", "true"], ["==", "$type", "Polygon"]], + layout: { + "line-cap": "round", + "line-join": "round", + }, + paint: { + "line-color": [ + "case", + ["!", ["to-boolean", ["get", "user_size_exceed"]]], + "#fbb03b", + "#ff0000", + ], + "line-dasharray": [0.2, 2], + "line-width": 2, + }, + }, +]; + +function overrideDefaultStyles(themes) { + return defaultDrawThemes + .filter((theme) => !themes.map(({ id }) => id).includes(theme.id)) + .concat(themes); +} + +const overrided = overrideDefaultStyles(ActivePolygonStyles); + +export { overrided as default, overrideDefaultStyles, ActivePolygonStyles }; diff --git a/src/main.js b/src/main.js index dfbd2d0..da88557 100644 --- a/src/main.js +++ b/src/main.js @@ -1,213 +1,12 @@ -import area from "@turf/area"; -import { convertArea } from "@turf/helpers"; - -import Constants from "@mapbox/mapbox-gl-draw/src/constants"; -import CommonSelectors from "@mapbox/mapbox-gl-draw/src/lib/common_selectors"; -import createVertex from "@mapbox/mapbox-gl-draw/src/lib/create_vertex"; - -import { getIneractionSwitch } from "./switchIteractions"; - -const doubleClickZoom = getIneractionSwitch("doubleClickZoom"); -const dragPan = getIneractionSwitch("dragPan"); - -function convertToKm2(value) { - return convertArea(value, "meters", "kilometers"); -} -function getArea(feature) { - return convertToKm2(area(feature)); -} - -const DrawRectangle = {}; - -DrawRectangle.onSetup = function ({ - areaLimit = 510100001, - areaChangedCallback = function () {}, - exceedCallback = function () {}, - exceedCallsOnEachMove = false, - allowCreateExceeded = false, - escapeKeyStopsDrawing = true, -}) { - const rectangle = this.newFeature({ - type: Constants.geojsonTypes.FEATURE, - properties: {}, - geometry: { - type: Constants.geojsonTypes.POLYGON, - coordinates: [[]], - }, - }); - this.addFeature(rectangle); - - this.clearSelectedFeatures(); - - // Disable iteractions - doubleClickZoom.disable(this); - dragPan.disable(this); - - // Update cursor - this.updateUIClasses({ mouse: Constants.cursors.ADD }); - this.activateUIButton(Constants.types.POLYGON); - this.setActionableState({ trash: true }); - - // Setup mode options - this.areaLimit = areaLimit; - this.exceedCallback = exceedCallback; - this.allowCreateExceeded = allowCreateExceeded; - this.exceedCallsOnEachMove = exceedCallsOnEachMove; - this.escapeStopsDrawing = escapeKeyStopsDrawing; - this.areaChangedCallback = areaChangedCallback; - - return { - rectangle, - dragMoving: false, - sizeExceeded: false, - currentArea: 0, - }; -}; - -DrawRectangle.onClick = function (state, e) { - // on first click, save clicked point coords as starting for rectangle - if (!state.startPoint) { - const startPoint = [e.lngLat.lng, e.lngLat.lat]; - state.startPoint = startPoint; - this.updateUIClasses({ mouse: Constants.cursors.ADD }); - state.rectangle.updateCoordinate(`0.0`, e.lngLat.lng, e.lngLat.lat); - state.rectangle.updateCoordinate(`0.1`, e.lngLat.lng, e.lngLat.lat); - } else if ( - state.startPoint && - state.startPoint[0] !== e.lngLat.lng && - state.startPoint[1] !== e.lngLat.lat && - state.dragMoving && - (!state.sizeExceeded || this.allowCreateExceeded) - ) { - this.updateUIClasses({ mouse: "pointer" }); - state.endPoint = [e.lngLat.lng, e.lngLat.lat]; - this.changeMode(Constants.modes.SIMPLE_SELECT, { - featureIds: [state.rectangle.id], - }); - } -}; - -DrawRectangle.onMouseUp = DrawRectangle.onClick; -DrawRectangle.onMouseDown = DrawRectangle.onClick; -DrawRectangle.onTouchStart = DrawRectangle.onClick; -DrawRectangle.onTouchEnd = function (state, e) { - if ( - state.startPoint && - state.startPoint[0] !== e.lngLat.lng && - state.startPoint[1] !== e.lngLat.lat - ) { - DrawRectangle.onMouseMove(state, e); - this.updateUIClasses({ mouse: "pointer" }); - state.endPoint = [e.lngLat.lng, e.lngLat.lat]; - this.changeMode(Constants.modes.SIMPLE_SELECT, { - featureIds: [state.rectangle.id], - }); - } else DrawRectangle.onClick(state, e); -}; - -DrawRectangle.onTap = function (state, e) { - if (!state.startPoint) this.onClick(state, e); +import DrawRectangle from "./draw-rectangle-mode"; +import Styles, { + overrideDefaultStyles, + ActivePolygonStyles, +} from "./draw-rectangle-styles"; + +export { + overrideDefaultStyles, + ActivePolygonStyles, + Styles as DrawStyles, + DrawRectangle as default, }; - -DrawRectangle.onMouseMove = function (state, e) { - state.dragMoving = true; - if (CommonSelectors.isVertex(e)) { - this.updateUIClasses({ mouse: Constants.cursors.POINTER }); - } - if (state.startPoint) { - state.rectangle.updateCoordinate("0.1", e.lngLat.lng, state.startPoint[1]); // maxX, minY - state.rectangle.updateCoordinate("0.2", e.lngLat.lng, e.lngLat.lat); // maxX, maxY - state.rectangle.updateCoordinate("0.3", state.startPoint[0], e.lngLat.lat); // minX,maxY - state.rectangle.updateCoordinate( - "0.4", - state.startPoint[0], - state.startPoint[1] - ); - } else { - state.rectangle.updateCoordinate(`0.0`, e.lngLat.lng, e.lngLat.lat); - } - if (this.areaLimit) { - let area = getArea(state.rectangle); - if (area > 0) - if (state.currentArea !== area) this.areaChangedCallback(area); - state.currentArea = area; - if (area > this.areaLimit) { - if (!state.sizeExceeded || this.exceedCallsOnEachMove) - this.exceedCallback(area); - state.sizeExceeded = true; - state.rectangle.properties.size_exceed = true; - } else { - state.sizeExceeded = false; - state.rectangle.properties.size_exceed = false; - } - } -}; -DrawRectangle.onDrag = DrawRectangle.onMouseMove; -DrawRectangle.onTouchMove = DrawRectangle.onMouseMove; - -DrawRectangle.onStop = function (state) { - this.updateUIClasses({ mouse: Constants.cursors.NONE }); - - // Enable iteractions - doubleClickZoom.enable(this); - dragPan.enable(this); - this.activateUIButton(); - - // check to see if we've deleted this feature - if (this.getFeature(state.rectangle.id) === undefined) return; - - //remove last added coordinate - state.rectangle.removeCoordinate("0.5"); - if (state.rectangle.isValid()) { - this.map.fire(Constants.events.CREATE, { - features: [state.rectangle.toGeoJSON()], - }); - } else { - this.deleteFeature([state.rectangle.id], { silent: true }); - this.changeMode(Constants.modes.SIMPLE_SELECT, {}, { silent: true }); - } -}; - -DrawRectangle.onKeyUp = function (state, e) { - if (CommonSelectors.isEscapeKey(e)) - if (this.escapeStopsDrawing) { - this.deleteFeature([state.rectangle.id], { silent: true }); - this.changeMode(Constants.modes.SIMPLE_SELECT); - } else if (CommonSelectors.isEnterKey(e)) { - this.changeMode(Constants.modes.SIMPLE_SELECT, { - featureIds: [state.rectangle.id], - }); - } -}; - -DrawRectangle.onTrash = function (state) { - this.deleteFeature([state.rectangle.id], { silent: true }); - this.changeMode(Constants.modes.SIMPLE_SELECT); -}; - -DrawRectangle.toDisplayFeatures = function (state, geojson, display) { - const isActivePolygon = geojson.properties.id === state.rectangle.id; - geojson.properties.active = isActivePolygon - ? Constants.activeStates.ACTIVE - : Constants.activeStates.INACTIVE; - if (!isActivePolygon) return display(geojson); - - if (geojson.geometry.coordinates.length === 0) return; - const coordinateCount = geojson.geometry.coordinates[0].length; - - if (coordinateCount < 3) return; - display( - createVertex( - state.rectangle.id, - geojson.geometry.coordinates[0][0], - "0.0", - false - ) - ); - geojson.properties.meta = Constants.meta.FEATURE; - - if (!state.startPoint) return; - return display(geojson); -}; - -export default DrawRectangle;