├── .gitignore ├── LICENSE ├── README.md ├── demo ├── 03_image_bin_masked.png ├── demo.html ├── nyc_1911_crop.jpg ├── rotate │ ├── 01.png │ └── 02.png └── scale │ ├── 01.png │ ├── 02.png │ ├── 03.png │ └── 04.png ├── docs ├── tx_center.gif └── tx_demo1.gif ├── package-lock.json ├── package.json ├── src ├── demo.js └── index.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # IDE: 64 | .idea/ 65 | 66 | dist/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 drykovanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TxRectMode mapbox-gl-draw custom mode 2 | 3 | This is the custom [mapbox-gl-draw mode](https://github.com/mapbox/mapbox-gl-draw/blob/master/docs/MODES.md) which allows to rotate and scale rectangle polygons. 4 | 5 | Live demo is [here](https://drykovanov.github.io/TxRectMode/demo/demo.html) 6 | 7 | ![Demo gif](/docs/tx_demo1.gif) 8 | 9 | ## Features: 10 | * rotate/scale polygons 11 | * options to choose rotation pivot and scale center 12 | * discrete rotation whith SHIFT button pressed 13 | * demo how to transform image 14 | 15 | ## Installation: 16 | ``` 17 | npm install git+https://github.com/drykovanov/mapbox-gl-draw-rotate-scale-rect-mode#0.1.10 18 | ``` 19 | 20 | ## Usage examples: 21 | First, init [MapboxDraw](https://github.com/mapbox/mapbox-gl-draw/blob/master/docs/API.md) with _TxRectMode_ and styling provided. 22 | 23 | There is an example of styling in [demo.js](/src/demo.js) and icon set for [scaling](/demo/scale/) and [rotation](/demo/rotate/). 24 | 25 | ```js 26 | import { TxRectMode, TxCenter } from 'mapbox-gl-draw-rotate-scale-rect-mode'; 27 | ... 28 | const draw = new MapboxDraw({ 29 | displayControlsDefault: false, 30 | controls: { 31 | }, 32 | 33 | modes: Object.assign({ 34 | tx_poly: TxRectMode, 35 | }, MapboxDraw.modes), 36 | 37 | styles: drawStyle, 38 | }); 39 | ``` 40 | 41 | 42 | Second, create your rectangle polygon (with [turf](https://turfjs.org/docs/#polygon)) and provide it's _featureId_ to `changeMode()`: 43 | ```js 44 | 45 | const coordinates = [cUL,cUR,cLR,cLL,cUL]; 46 | const poly = turf.polygon([coordinates]); 47 | poly.id = ; 48 | 49 | draw.add(poly); 50 | 51 | draw.changeMode('tx_poly', { 52 | featureId: poly.id, // required 53 | }); 54 | ``` 55 | 56 | 57 | `changeMode('tx_poly', ...)` accepts the following options: 58 | * `rotatePivot` - change rotation pivot to the middle of the opposite polygon side 59 | * `scaleCenter` - change scaling center to the opposite vertex 60 | * `singleRotationPoint` - set true to show only one rotation widget 61 | * `rotationPointRadius` - offset rotation point from feature perimeter 62 | * `canScale` - set false to disable scaling 63 | * `canRotate` - set false to disable rotation 64 | * `canTrash` - set false to disable feature delete 65 | * `canSelectFeatures` - set false to forbid exiting the mode 66 | ```js 67 | draw.changeMode('tx_poly', { 68 | featureId: poly.id, // required 69 | 70 | canScale: false, 71 | canRotate: true, // only rotation enabled 72 | canTrash: false, // disable feature delete 73 | 74 | rotatePivot: TxCenter.Center, // rotate around center 75 | scaleCenter: TxCenter.Opposite, // scale around opposite vertex 76 | 77 | singleRotationPoint: true, // only one rotation point 78 | rotationPointRadius: 1.2, // offset rotation point 79 | 80 | canSelectFeatures: true, 81 | }); 82 | ``` 83 | See how scaling and rotation around opposite side works: 84 | 85 | ![Demo gif](/docs/tx_center.gif) 86 | -------------------------------------------------------------------------------- /demo/03_image_bin_masked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drykovanov/mapbox-gl-draw-rotate-scale-rect-mode/7b1a6cfdd3584f2c9682b9510440371f23643716/demo/03_image_bin_masked.png -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rotate/scale mapbox-gl-draw mode demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 |
22 | 23 | 46 | 47 | -------------------------------------------------------------------------------- /demo/nyc_1911_crop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drykovanov/mapbox-gl-draw-rotate-scale-rect-mode/7b1a6cfdd3584f2c9682b9510440371f23643716/demo/nyc_1911_crop.jpg -------------------------------------------------------------------------------- /demo/rotate/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drykovanov/mapbox-gl-draw-rotate-scale-rect-mode/7b1a6cfdd3584f2c9682b9510440371f23643716/demo/rotate/01.png -------------------------------------------------------------------------------- /demo/rotate/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drykovanov/mapbox-gl-draw-rotate-scale-rect-mode/7b1a6cfdd3584f2c9682b9510440371f23643716/demo/rotate/02.png -------------------------------------------------------------------------------- /demo/scale/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drykovanov/mapbox-gl-draw-rotate-scale-rect-mode/7b1a6cfdd3584f2c9682b9510440371f23643716/demo/scale/01.png -------------------------------------------------------------------------------- /demo/scale/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drykovanov/mapbox-gl-draw-rotate-scale-rect-mode/7b1a6cfdd3584f2c9682b9510440371f23643716/demo/scale/02.png -------------------------------------------------------------------------------- /demo/scale/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drykovanov/mapbox-gl-draw-rotate-scale-rect-mode/7b1a6cfdd3584f2c9682b9510440371f23643716/demo/scale/03.png -------------------------------------------------------------------------------- /demo/scale/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drykovanov/mapbox-gl-draw-rotate-scale-rect-mode/7b1a6cfdd3584f2c9682b9510440371f23643716/demo/scale/04.png -------------------------------------------------------------------------------- /docs/tx_center.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drykovanov/mapbox-gl-draw-rotate-scale-rect-mode/7b1a6cfdd3584f2c9682b9510440371f23643716/docs/tx_center.gif -------------------------------------------------------------------------------- /docs/tx_demo1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drykovanov/mapbox-gl-draw-rotate-scale-rect-mode/7b1a6cfdd3584f2c9682b9510440371f23643716/docs/tx_demo1.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mapbox-gl-draw-rotate-scale-rect-mode", 3 | "version": "0.1.10", 4 | "description": "mapbox-gl-draw plugin to scale and rotate rectangle on the map", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "dev": "npx webpack --progress", 8 | "build": "npx webpack", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/drykovanov/mapbox-gl-draw-rotate-scale-rect-mode.git" 14 | }, 15 | "author": "drykovanov", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/drykovanov/mapbox-gl-draw-rotate-scale-rect-mode/issues" 19 | }, 20 | "homepage": "https://github.com/drykovanov/mapbox-gl-draw-rotate-scale-rect-mode#readme", 21 | "dependencies": { 22 | "mapbox-gl": "^1.5.0", 23 | "@mapbox/mapbox-gl-draw": "^1.1.2", 24 | "@turf/bearing": "^6.0.1", 25 | "@turf/center": "^6.0.1", 26 | "@turf/centroid": "^6.0.2", 27 | "@turf/destination": "^6.0.1", 28 | "@turf/distance": "^6.0.1", 29 | "@turf/helpers": "^6.1.4", 30 | "@turf/midpoint": "^5.1.5", 31 | "@turf/transform-rotate": "^5.1.5", 32 | "@turf/transform-scale": "^5.1.5" 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "^7.9.0", 36 | "@babel/preset-env": "^7.9.0", 37 | "babel-loader": "^8.1.0", 38 | "webpack": "^4.42.0", 39 | "webpack-cli": "^3.3.11" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/demo.js: -------------------------------------------------------------------------------- 1 | // ----Demo---- 2 | import MapboxDraw from "@mapbox/mapbox-gl-draw"; 3 | import { polygon } from "@turf/helpers"; 4 | import {TxRectMode, TxCenter } from "./index"; 5 | 6 | // var demoParams = { 7 | // mapCenter: [-73.93, 40.73], 8 | // mapZoom: 9, 9 | // imageUrl: 'nyc_1911_crop.jpg', 10 | // imageWidth: 421, 11 | // imageHeight: 671, 12 | // }; 13 | export function TxRectModeDemo(demoParams) { 14 | this._demoParams = demoParams; 15 | this._nextFeatureId = 1; 16 | } 17 | 18 | TxRectModeDemo.prototype.start = function() { 19 | mapboxgl.accessToken = 'pk.eyJ1IjoiZHJ5a292YW5vdiIsImEiOiJjazM0OG9hYW4wenR4M2xtajVseW1qYjY3In0.YnbkeuaBiSaDOn7eYDAXsQ'; 20 | this._map = new mapboxgl.Map({ 21 | container: 'map', // container id 22 | style: 'mapbox://styles/mapbox/streets-v11', // stylesheet location 23 | center: this._demoParams.mapCenter, 24 | zoom: this._demoParams.mapZoom, // starting zoom 25 | // fadeDuration: 0 // 26 | }); 27 | 28 | this._map.on('load', this._onMapLoad.bind(this)); 29 | }; 30 | 31 | TxRectModeDemo.prototype._onMapLoad = function(event) { 32 | this._map.loadImage('rotate/01.png', function(error, image) { 33 | if (error) throw error; 34 | this._map.addImage('rotate', image); 35 | }.bind(this)); 36 | 37 | this._map.loadImage('scale/01.png', function(error, image) { 38 | if (error) throw error; 39 | this._map.addImage('scale', image); 40 | }.bind(this)); 41 | 42 | this._draw = new MapboxDraw({ 43 | displayControlsDefault: false, 44 | controls: { 45 | polygon: true, 46 | // trash: true 47 | }, 48 | 49 | userProperties: true, // pass user properties to mapbox-gl-draw internal features 50 | 51 | modes: Object.assign({ 52 | tx_poly: TxRectMode, 53 | }, MapboxDraw.modes), 54 | 55 | styles: drawStyle, 56 | }); 57 | 58 | // XXX how to make overlay render under mapbox-gl-draw widgets? 59 | this._createDemoOverlay(); 60 | 61 | this._map.addControl(this._draw, 'top-right'); 62 | 63 | this._createDemoFeatures(); 64 | 65 | this._map.on('data', this._onData.bind(this)); 66 | 67 | this._map.on('draw.selectionchange', this._onDrawSelection.bind(this)); 68 | 69 | this._map.on('click', this._onClick.bind(this)); 70 | this._map.on('touchstart', this._onClick.bind(this)); 71 | 72 | this._txEdit(1); 73 | }; 74 | 75 | TxRectModeDemo.prototype._onClick = function(e) { 76 | var features = this._map.queryRenderedFeatures(e.point); 77 | if (features.length > 0) { 78 | var feature = features[0].toJSON(); 79 | if (feature.geometry.type == 'Polygon' && feature.properties.id) { 80 | this._txEdit(feature.properties.id); 81 | } 82 | } 83 | }; 84 | 85 | TxRectModeDemo.prototype._txEdit = function(featureId) { 86 | this._draw.changeMode('tx_poly', { 87 | featureId: featureId, // required 88 | 89 | canTrash: false, 90 | 91 | canScale: true, 92 | canRotate: true, // only rotation enabled 93 | 94 | singleRotationPoint: true, 95 | rotationPointRadius: 1.2, // extend rotation point outside polygon 96 | 97 | rotatePivot: TxCenter.Center, // rotate around center 98 | scaleCenter: TxCenter.Opposite, // scale around opposite vertex 99 | 100 | canSelectFeatures: true, 101 | }); 102 | }; 103 | 104 | 105 | TxRectModeDemo.prototype._computeRect = function(center, size) { 106 | 107 | const cUL = this._map.unproject ([center[0] - size[0]/2, center[1] - size[1]/2]).toArray(); 108 | const cUR = this._map.unproject ([center[0] + size[0]/2, center[1] - size[1]/2]).toArray(); 109 | const cLR = this._map.unproject ([center[0] + size[0]/2, center[1] + size[1]/2]).toArray(); 110 | const cLL = this._map.unproject ([center[0] - size[0]/2, center[1] + size[1]/2]).toArray(); 111 | 112 | return [cUL,cUR,cLR,cLL,cUL]; 113 | }; 114 | 115 | TxRectModeDemo.prototype._createDemoFeatures = function() { 116 | if (this._overlayPoly) 117 | this._draw.add(this._overlayPoly); 118 | 119 | 120 | const canvas = this._map.getCanvas(); 121 | // Get the device pixel ratio, falling back to 1. 122 | // var dpr = window.devicePixelRatio || 1; 123 | // Get the size of the canvas in CSS pixels. 124 | var rect = canvas.getBoundingClientRect(); 125 | const w = rect.width; 126 | const h = rect.height; 127 | 128 | const cPoly = this._computeRect([1 * w/5, h/3], [100, 180]); 129 | const poly = polygon([cPoly]); 130 | poly.id = this._nextFeatureId++; 131 | this._draw.add(poly); 132 | 133 | }; 134 | 135 | TxRectModeDemo.prototype._createDemoOverlay = function() { 136 | var im_w = this._demoParams.imageWidth; 137 | var im_h = this._demoParams.imageHeight; 138 | 139 | 140 | const canvas = this._map.getCanvas(); 141 | // Get the device pixel ratio, falling back to 1. 142 | // var dpr = window.devicePixelRatio || 1; 143 | // Get the size of the canvas in CSS pixels. 144 | var rect = canvas.getBoundingClientRect(); 145 | const w = rect.width; 146 | const h = rect.height; 147 | // console.log('canvas: ' + w + 'x' + h); 148 | 149 | while (im_w >= (0.8 * w) || im_h >= (0.8 * h)) { 150 | im_w = Math.round(0.8 * im_w); 151 | im_h = Math.round(0.8 * im_h); 152 | } 153 | 154 | const cPoly = this._computeRect([w/2, h/2], [im_w, im_h]); 155 | const cBox = cPoly.slice(0, 4); 156 | 157 | this._map.addSource("test-overlay", { 158 | "type": "image", 159 | "url": this._demoParams.imageUrl, 160 | "coordinates": cBox 161 | }); 162 | 163 | this._map.addLayer({ 164 | "id": "test-overlay-layer", 165 | "type": "raster", 166 | "source": "test-overlay", 167 | "paint": { 168 | "raster-opacity": 0.90, 169 | "raster-fade-duration": 0 170 | }, 171 | }); 172 | 173 | const poly = polygon([cPoly]); 174 | poly.id = this._nextFeatureId++; 175 | poly.properties.overlaySourceId = 'test-overlay'; 176 | poly.properties.type = 'overlay'; 177 | this._overlayPoly = poly; 178 | }; 179 | 180 | TxRectModeDemo.prototype._onDrawSelection = function(e) { 181 | const {features, points} = e; 182 | if (features.length <= 0) { 183 | return; 184 | } 185 | 186 | var feature = features[0]; 187 | if (feature.geometry.type == 'Polygon' && feature.id) { 188 | this._txEdit(feature.id); 189 | } 190 | }; 191 | 192 | TxRectModeDemo.prototype._onData = function(e) { 193 | if (e.sourceId && e.sourceId.startsWith('mapbox-gl-draw-')) { 194 | // console.log(e.sourceId); 195 | if (e.type && e.type == 'data' 196 | && e.source.data 197 | // && e.sourceDataType && e.sourceDataType == 'content' 198 | && e.sourceDataType == undefined 199 | && e.isSourceLoaded 200 | ) { 201 | // var source = this.map.getSource(e.sourceId); 202 | //var geojson = source._data; 203 | var geojson = e.source.data; 204 | if (geojson && geojson.features && geojson.features.length > 0 205 | && geojson.features[0].properties 206 | && geojson.features[0].properties.user_overlaySourceId) { 207 | this._drawUpdateOverlayByFeature(geojson.features[0]); 208 | } 209 | } 210 | } 211 | }; 212 | 213 | TxRectModeDemo.prototype._drawUpdateOverlayByFeature = function(feature) { 214 | var coordinates = feature.geometry.coordinates[0].slice(0, 4); 215 | var overlaySourceId = feature.properties.user_overlaySourceId; 216 | this._map.getSource(overlaySourceId).setCoordinates(coordinates); 217 | }; 218 | 219 | var drawStyle = [ 220 | { 221 | 'id': 'gl-draw-polygon-fill-inactive', 222 | 'type': 'fill', 223 | 'filter': ['all', 224 | ['==', 'active', 'false'], 225 | ['==', '$type', 'Polygon'], 226 | ['!=', 'user_type', 'overlay'], 227 | ['!=', 'mode', 'static'] 228 | ], 229 | 'paint': { 230 | 'fill-color': '#3bb2d0', 231 | 'fill-outline-color': '#3bb2d0', 232 | 'fill-opacity': 0.7 233 | } 234 | }, 235 | { 236 | 'id': 'gl-draw-polygon-fill-active', 237 | 'type': 'fill', 238 | 'filter': ['all', 239 | ['==', 'active', 'true'], 240 | ['==', '$type', 'Polygon'], 241 | ['!=', 'user_type', 'overlay'], 242 | ], 243 | 'paint': { 244 | 'fill-color': '#fbb03b', 245 | 'fill-outline-color': '#fbb03b', 246 | 'fill-opacity': 0.7 247 | } 248 | }, 249 | 250 | 251 | { 252 | 'id': 'gl-draw-overlay-polygon-fill-inactive', 253 | 'type': 'fill', 254 | 'filter': ['all', 255 | ['==', 'active', 'false'], 256 | ['==', '$type', 'Polygon'], 257 | ['==', 'user_type', 'overlay'], 258 | ['!=', 'mode', 'static'] 259 | ], 260 | 'paint': { 261 | 'fill-color': '#3bb2d0', 262 | 'fill-outline-color': '#3bb2d0', 263 | 'fill-opacity': 0.01 264 | } 265 | }, 266 | { 267 | 'id': 'gl-draw-overlay-polygon-fill-active', 268 | 'type': 'fill', 269 | 'filter': ['all', 270 | ['==', 'active', 'true'], 271 | ['==', '$type', 'Polygon'], 272 | ['==', 'user_type', 'overlay'], 273 | ], 274 | 'paint': { 275 | 'fill-color': '#fbb03b', 276 | 'fill-outline-color': '#fbb03b', 277 | 'fill-opacity': 0.01 278 | } 279 | }, 280 | 281 | { 282 | 'id': 'gl-draw-polygon-stroke-inactive', 283 | 'type': 'line', 284 | 'filter': ['all', 285 | ['==', 'active', 'false'], 286 | ['==', '$type', 'Polygon'], 287 | ['!=', 'user_type', 'overlay'], 288 | ['!=', 'mode', 'static'] 289 | ], 290 | 'layout': { 291 | 'line-cap': 'round', 292 | 'line-join': 'round' 293 | }, 294 | 'paint': { 295 | 'line-color': '#3bb2d0', 296 | 'line-width': 2 297 | } 298 | }, 299 | 300 | { 301 | 'id': 'gl-draw-polygon-stroke-active', 302 | 'type': 'line', 303 | 'filter': ['all', 304 | ['==', 'active', 'true'], 305 | ['==', '$type', 'Polygon'], 306 | ], 307 | 'layout': { 308 | 'line-cap': 'round', 309 | 'line-join': 'round' 310 | }, 311 | 'paint': { 312 | 'line-color': '#fbb03b', 313 | 'line-dasharray': [0.2, 2], 314 | 'line-width': 2 315 | } 316 | }, 317 | 318 | // { 319 | // 'id': 'gl-draw-polygon-midpoint', 320 | // 'type': 'circle', 321 | // 'filter': ['all', 322 | // ['==', '$type', 'Point'], 323 | // ['==', 'meta', 'midpoint']], 324 | // 'paint': { 325 | // 'circle-radius': 3, 326 | // 'circle-color': '#fbb03b' 327 | // } 328 | // }, 329 | 330 | { 331 | 'id': 'gl-draw-line-inactive', 332 | 'type': 'line', 333 | 'filter': ['all', 334 | ['==', 'active', 'false'], 335 | ['==', '$type', 'LineString'], 336 | ['!=', 'mode', 'static'] 337 | ], 338 | 'layout': { 339 | 'line-cap': 'round', 340 | 'line-join': 'round' 341 | }, 342 | 'paint': { 343 | 'line-color': '#3bb2d0', 344 | 'line-width': 2 345 | } 346 | }, 347 | { 348 | 'id': 'gl-draw-line-active', 349 | 'type': 'line', 350 | 'filter': ['all', 351 | ['==', '$type', 'LineString'], 352 | ['==', 'active', 'true'] 353 | ], 354 | 'layout': { 355 | 'line-cap': 'round', 356 | 'line-join': 'round' 357 | }, 358 | 'paint': { 359 | 'line-color': '#fbb03b', 360 | 'line-dasharray': [0.2, 2], 361 | 'line-width': 2 362 | } 363 | }, 364 | { 365 | 'id': 'gl-draw-polygon-and-line-vertex-stroke-inactive', 366 | 'type': 'circle', 367 | 'filter': ['all', 368 | ['==', 'meta', 'vertex'], 369 | ['==', '$type', 'Point'], 370 | ['!=', 'mode', 'static'] 371 | ], 372 | 'paint': { 373 | 'circle-radius': 4, 374 | 'circle-color': '#fff' 375 | } 376 | }, 377 | { 378 | 'id': 'gl-draw-polygon-and-line-vertex-inactive', 379 | 'type': 'circle', 380 | 'filter': ['all', 381 | ['==', 'meta', 'vertex'], 382 | ['==', '$type', 'Point'], 383 | ['!=', 'mode', 'static'] 384 | ], 385 | 'paint': { 386 | 'circle-radius': 2, 387 | 'circle-color': '#fbb03b' 388 | } 389 | }, 390 | 391 | { 392 | 'id': 'gl-draw-polygon-and-line-vertex-scale-icon', 393 | 'type': 'symbol', 394 | 'filter': ['all', 395 | ['==', 'meta', 'vertex'], 396 | ['==', '$type', 'Point'], 397 | ['!=', 'mode', 'static'], 398 | ['has', 'heading'] 399 | ], 400 | 'layout': { 401 | 'icon-image': 'scale', 402 | 'icon-allow-overlap': true, 403 | 'icon-ignore-placement': true, 404 | 'icon-rotation-alignment': 'map', 405 | 'icon-rotate': ['get', 'heading'] 406 | }, 407 | 'paint': { 408 | 'icon-opacity': 1.0, 409 | 'icon-opacity-transition': { 410 | 'delay': 0, 411 | 'duration': 0 412 | } 413 | } 414 | }, 415 | 416 | 417 | { 418 | 'id': 'gl-draw-point-point-stroke-inactive', 419 | 'type': 'circle', 420 | 'filter': ['all', 421 | ['==', 'active', 'false'], 422 | ['==', '$type', 'Point'], 423 | ['==', 'meta', 'feature'], 424 | ['!=', 'mode', 'static'] 425 | ], 426 | 'paint': { 427 | 'circle-radius': 5, 428 | 'circle-opacity': 1, 429 | 'circle-color': '#fff' 430 | } 431 | }, 432 | { 433 | 'id': 'gl-draw-point-inactive', 434 | 'type': 'circle', 435 | 'filter': ['all', 436 | ['==', 'active', 'false'], 437 | ['==', '$type', 'Point'], 438 | ['==', 'meta', 'feature'], 439 | ['!=', 'mode', 'static'] 440 | ], 441 | 'paint': { 442 | 'circle-radius': 3, 443 | 'circle-color': '#3bb2d0' 444 | } 445 | }, 446 | { 447 | 'id': 'gl-draw-point-stroke-active', 448 | 'type': 'circle', 449 | 'filter': ['all', 450 | ['==', '$type', 'Point'], 451 | ['==', 'active', 'true'], 452 | ['!=', 'meta', 'midpoint'] 453 | ], 454 | 'paint': { 455 | 'circle-radius': 4, 456 | 'circle-color': '#fff' 457 | } 458 | }, 459 | { 460 | 'id': 'gl-draw-point-active', 461 | 'type': 'circle', 462 | 'filter': ['all', 463 | ['==', '$type', 'Point'], 464 | ['!=', 'meta', 'midpoint'], 465 | ['==', 'active', 'true']], 466 | 'paint': { 467 | 'circle-radius': 2, 468 | 'circle-color': '#fbb03b' 469 | } 470 | }, 471 | { 472 | 'id': 'gl-draw-polygon-fill-static', 473 | 'type': 'fill', 474 | 'filter': ['all', ['==', 'mode', 'static'], ['==', '$type', 'Polygon']], 475 | 'paint': { 476 | 'fill-color': '#404040', 477 | 'fill-outline-color': '#404040', 478 | 'fill-opacity': 0.1 479 | } 480 | }, 481 | { 482 | 'id': 'gl-draw-polygon-stroke-static', 483 | 'type': 'line', 484 | 'filter': ['all', ['==', 'mode', 'static'], ['==', '$type', 'Polygon']], 485 | 'layout': { 486 | 'line-cap': 'round', 487 | 'line-join': 'round' 488 | }, 489 | 'paint': { 490 | 'line-color': '#404040', 491 | 'line-width': 2 492 | } 493 | }, 494 | { 495 | 'id': 'gl-draw-line-static', 496 | 'type': 'line', 497 | 'filter': ['all', ['==', 'mode', 'static'], ['==', '$type', 'LineString']], 498 | 'layout': { 499 | 'line-cap': 'round', 500 | 'line-join': 'round' 501 | }, 502 | 'paint': { 503 | 'line-color': '#404040', 504 | 'line-width': 2 505 | } 506 | }, 507 | { 508 | 'id': 'gl-draw-point-static', 509 | 'type': 'circle', 510 | 'filter': ['all', ['==', 'mode', 'static'], ['==', '$type', 'Point']], 511 | 'paint': { 512 | 'circle-radius': 5, 513 | 'circle-color': '#404040' 514 | } 515 | }, 516 | 517 | // { 518 | // 'id': 'gl-draw-polygon-rotate-point', 519 | // 'type': 'circle', 520 | // 'filter': ['all', 521 | // ['==', '$type', 'Point'], 522 | // ['==', 'meta', 'rotate_point']], 523 | // 'paint': { 524 | // 'circle-radius': 5, 525 | // 'circle-color': '#fbb03b' 526 | // } 527 | // }, 528 | 529 | { 530 | 'id': 'gl-draw-line-rotate-point', 531 | 'type': 'line', 532 | 'filter': ['all', 533 | ['==', 'meta', 'midpoint'], 534 | ['==', '$type', 'LineString'], 535 | ['!=', 'mode', 'static'] 536 | // ['==', 'active', 'true'] 537 | ], 538 | 'layout': { 539 | 'line-cap': 'round', 540 | 'line-join': 'round' 541 | }, 542 | 'paint': { 543 | 'line-color': '#fbb03b', 544 | 'line-dasharray': [0.2, 2], 545 | 'line-width': 2 546 | } 547 | }, 548 | { 549 | 'id': 'gl-draw-polygon-rotate-point-stroke', 550 | 'type': 'circle', 551 | 'filter': ['all', 552 | ['==', 'meta', 'midpoint'], 553 | ['==', '$type', 'Point'], 554 | ['!=', 'mode', 'static'] 555 | ], 556 | 'paint': { 557 | 'circle-radius': 4, 558 | 'circle-color': '#fff' 559 | } 560 | }, 561 | { 562 | 'id': 'gl-draw-polygon-rotate-point', 563 | 'type': 'circle', 564 | 'filter': ['all', 565 | ['==', 'meta', 'midpoint'], 566 | ['==', '$type', 'Point'], 567 | ['!=', 'mode', 'static'] 568 | ], 569 | 'paint': { 570 | 'circle-radius': 2, 571 | 'circle-color': '#fbb03b' 572 | } 573 | }, 574 | { 575 | 'id': 'gl-draw-polygon-rotate-point-icon', 576 | 'type': 'symbol', 577 | 'filter': ['all', 578 | ['==', 'meta', 'midpoint'], 579 | ['==', '$type', 'Point'], 580 | ['!=', 'mode', 'static'] 581 | ], 582 | 'layout': { 583 | 'icon-image': 'rotate', 584 | 'icon-allow-overlap': true, 585 | 'icon-ignore-placement': true, 586 | 'icon-rotation-alignment': 'map', 587 | 'icon-rotate': ['get', 'heading'] 588 | }, 589 | 'paint': { 590 | 'icon-opacity': 1.0, 591 | 'icon-opacity-transition': { 592 | 'delay': 0, 593 | 'duration': 0 594 | } 595 | } 596 | }, 597 | ]; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import MapboxDraw from '@mapbox/mapbox-gl-draw'; 2 | 3 | import * as Constants from '@mapbox/mapbox-gl-draw/src/constants'; 4 | import doubleClickZoom from '@mapbox/mapbox-gl-draw/src/lib/double_click_zoom'; 5 | import createSupplementaryPoints from '@mapbox/mapbox-gl-draw/src/lib/create_supplementary_points'; 6 | import * as CommonSelectors from '@mapbox/mapbox-gl-draw/src/lib/common_selectors'; 7 | import moveFeatures from '@mapbox/mapbox-gl-draw/src/lib/move_features'; 8 | 9 | import {lineString, point} from '@turf/helpers'; 10 | import bearing from '@turf/bearing'; 11 | // import centroid from '@turf/centroid'; 12 | import center from '@turf/center'; 13 | import midpoint from '@turf/midpoint'; 14 | import distance from '@turf/distance'; 15 | import destination from '@turf/destination'; 16 | import transformRotate from '@turf/transform-rotate'; 17 | import transformScale from '@turf/transform-scale'; 18 | 19 | export const TxRectMode = {}; 20 | 21 | export const TxCenter = { 22 | Center: 0, // rotate or scale around center of polygon 23 | Opposite: 1, // rotate or scale around opposite side of polygon 24 | }; 25 | 26 | function parseTxCenter(value, defaultTxCenter = TxCenter.Center) { 27 | if (value == undefined || value == null) 28 | return defaultTxCenter; 29 | 30 | if (value === TxCenter.Center || value === TxCenter.Opposite) 31 | return value; 32 | 33 | if (value == 'center') 34 | return TxCenter.Center; 35 | 36 | if (value == 'opposite') 37 | return TxCenter.Opposite; 38 | 39 | throw Error('Invalid TxCenter: ' + value); 40 | } 41 | 42 | /* 43 | opts = { 44 | featureId: ..., 45 | 46 | canScale: default true, 47 | canRotate: default true, 48 | 49 | rotatePivot: default 'center' or 'opposite', 50 | scaleCenter: default 'center' or 'opposite', 51 | 52 | canSelectFeatures: default true, // can exit to simple_select mode 53 | } 54 | */ 55 | TxRectMode.onSetup = function(opts) { 56 | const featureId = 57 | (opts.featureIds && Array.isArray(opts.featureIds) && opts.featureIds.length > 0) ? 58 | opts.featureIds[0] : opts.featureId; 59 | 60 | const feature = this.getFeature(featureId); 61 | 62 | if (!feature) { 63 | throw new Error('You must provide a valid featureId to enter tx_poly mode'); 64 | } 65 | 66 | if (feature.type != Constants.geojsonTypes.POLYGON) { 67 | throw new TypeError('tx_poly mode can only handle polygons'); 68 | } 69 | if (feature.coordinates === undefined 70 | || feature.coordinates.length != 1 71 | || feature.coordinates[0].length <= 2) { 72 | throw new TypeError('tx_poly mode can only handle polygons'); 73 | } 74 | 75 | const state = { 76 | featureId, 77 | feature, 78 | 79 | canTrash: opts.canTrash != undefined ? opts.canTrash : true, 80 | 81 | canScale: opts.canScale != undefined ? opts.canScale : true, 82 | canRotate: opts.canRotate != undefined ? opts.canRotate : true, 83 | 84 | singleRotationPoint: opts.singleRotationPoint != undefined ? opts.singleRotationPoint : false, 85 | rotationPointRadius: opts.rotationPointRadius != undefined ? opts.rotationPointRadius : 1.0, 86 | 87 | rotatePivot: parseTxCenter(opts.rotatePivot, TxCenter.Center), 88 | scaleCenter: parseTxCenter(opts.scaleCenter, TxCenter.Center), 89 | 90 | canSelectFeatures: opts.canSelectFeatures != undefined ? opts.canSelectFeatures : true, 91 | // selectedFeatureMode: opts.selectedFeatureMode != undefined ? opts.selectedFeatureMode : 'simple_select', 92 | 93 | dragMoveLocation: opts.startPos || null, 94 | dragMoving: false, 95 | canDragMove: false, 96 | selectedCoordPaths: opts.coordPath ? [opts.coordPath] : [] 97 | }; 98 | 99 | if (!(state.canRotate || state.canScale)) { 100 | console.warn('Non of canScale or canRotate is true'); 101 | } 102 | 103 | this.setSelectedCoordinates(this.pathsToCoordinates(featureId, state.selectedCoordPaths)); 104 | this.setSelected(featureId); 105 | doubleClickZoom.disable(this); 106 | 107 | this.setActionableState({ 108 | combineFeatures: false, 109 | uncombineFeatures: false, 110 | trash: state.canTrash 111 | }); 112 | 113 | return state; 114 | }; 115 | 116 | TxRectMode.toDisplayFeatures = function(state, geojson, push) { 117 | if (state.featureId === geojson.properties.id) { 118 | geojson.properties.active = Constants.activeStates.ACTIVE; 119 | push(geojson); 120 | 121 | 122 | var suppPoints = createSupplementaryPoints(geojson, { 123 | map: this.map, 124 | midpoints: false, 125 | selectedPaths: state.selectedCoordPaths 126 | }); 127 | 128 | if (state.canScale) { 129 | this.computeBisectrix(suppPoints); 130 | suppPoints.forEach(push); 131 | } 132 | 133 | if (state.canRotate) { 134 | var rotPoints = this.createRotationPoints(state, geojson, suppPoints); 135 | rotPoints.forEach(push); 136 | } 137 | } else { 138 | geojson.properties.active = Constants.activeStates.INACTIVE; 139 | push(geojson); 140 | } 141 | 142 | // this.fireActionable(state); 143 | this.setActionableState({ 144 | combineFeatures: false, 145 | uncombineFeatures: false, 146 | trash: state.canTrash 147 | }); 148 | 149 | // this.fireUpdate(); 150 | }; 151 | 152 | TxRectMode.onStop = function() { 153 | doubleClickZoom.enable(this); 154 | this.clearSelectedCoordinates(); 155 | }; 156 | 157 | // TODO why I need this? 158 | TxRectMode.pathsToCoordinates = function(featureId, paths) { 159 | return paths.map(coord_path => { return { feature_id: featureId, coord_path }; }); 160 | }; 161 | 162 | TxRectMode.computeBisectrix = function(points) { 163 | for (var i1 = 0; i1 < points.length; i1++) { 164 | var i0 = (i1 - 1 + points.length) % points.length; 165 | var i2 = (i1 + 1) % points.length; 166 | // console.log('' + i0 + ' -> ' + i1 + ' -> ' + i2); 167 | 168 | var l1 = lineString([points[i0].geometry.coordinates, points[i1].geometry.coordinates]); 169 | var l2 = lineString([points[i1].geometry.coordinates, points[i2].geometry.coordinates]); 170 | var a1 = bearing(points[i0].geometry.coordinates, points[i1].geometry.coordinates); 171 | var a2 = bearing(points[i2].geometry.coordinates, points[i1].geometry.coordinates); 172 | // console.log('a1 = ' +a1 + ', a2 = ' + a2); 173 | 174 | var a = (a1 + a2)/2.0; 175 | 176 | if (a < 0.0) 177 | a += 360; 178 | if (a > 360) 179 | a -= 360; 180 | 181 | points[i1].properties.heading = a; 182 | } 183 | 184 | }; 185 | 186 | TxRectMode._createRotationPoint = function(rotationWidgets, featureId, v1, v2, rotCenter, radiusScale) { 187 | var cR0 = midpoint(v1, v2).geometry.coordinates; 188 | var heading = bearing(rotCenter, cR0); 189 | var distance0 = distance(rotCenter, cR0); 190 | var distance1 = radiusScale * distance0; // TODO depends on map scale 191 | var cR1 = destination(rotCenter, distance1, heading, {}).geometry.coordinates; 192 | 193 | rotationWidgets.push({ 194 | type: Constants.geojsonTypes.FEATURE, 195 | properties: { 196 | meta: Constants.meta.MIDPOINT, 197 | parent: featureId, 198 | lng: cR1[0], 199 | lat: cR1[1], 200 | coord_path: v1.properties.coord_path, 201 | heading: heading, 202 | }, 203 | geometry: { 204 | type: Constants.geojsonTypes.POINT, 205 | coordinates: cR1 206 | } 207 | } 208 | ); 209 | }; 210 | 211 | TxRectMode.createRotationPoints = function(state, geojson, suppPoints) { 212 | const { type, coordinates } = geojson.geometry; 213 | const featureId = geojson.properties && geojson.properties.id; 214 | 215 | let rotationWidgets = []; 216 | if (type != Constants.geojsonTypes.POLYGON) { 217 | return ; 218 | } 219 | 220 | var corners = suppPoints.slice(0); 221 | corners[corners.length] = corners[0]; 222 | 223 | var v1 = null; 224 | 225 | var rotCenter = this.computeRotationCenter(state, geojson); 226 | 227 | if (state.singleRotationPoint) { 228 | this._createRotationPoint(rotationWidgets, featureId, corners[0], corners[1], rotCenter, state.rotationPointRadius); 229 | } else { 230 | corners.forEach((v2) => { 231 | if (v1 != null) { 232 | this._createRotationPoint(rotationWidgets, featureId, v1, v2, rotCenter, state.rotationPointRadius); 233 | } 234 | 235 | v1 = v2; 236 | }); 237 | } 238 | 239 | return rotationWidgets; 240 | }; 241 | 242 | TxRectMode.startDragging = function(state, e) { 243 | this.map.dragPan.disable(); 244 | state.canDragMove = true; 245 | state.dragMoveLocation = e.lngLat; 246 | }; 247 | 248 | TxRectMode.stopDragging = function(state) { 249 | this.map.dragPan.enable(); 250 | state.dragMoving = false; 251 | state.canDragMove = false; 252 | state.dragMoveLocation = null; 253 | }; 254 | 255 | const isRotatePoint = CommonSelectors.isOfMetaType(Constants.meta.MIDPOINT); 256 | const isVertex = CommonSelectors.isOfMetaType(Constants.meta.VERTEX); 257 | 258 | TxRectMode.onTouchStart = TxRectMode.onMouseDown = function(state, e) { 259 | if (isVertex(e)) return this.onVertex(state, e); 260 | if (isRotatePoint(e)) return this.onRotatePoint(state, e); 261 | if (CommonSelectors.isActiveFeature(e)) return this.onFeature(state, e); 262 | // if (isMidpoint(e)) return this.onMidpoint(state, e); 263 | }; 264 | 265 | const TxMode = { 266 | Scale: 1, 267 | Rotate: 2, 268 | }; 269 | 270 | TxRectMode.onVertex = function(state, e) { 271 | // console.log('onVertex()'); 272 | // convert internal MapboxDraw feature to valid GeoJSON: 273 | this.computeAxes(state, state.feature.toGeoJSON()); 274 | 275 | this.startDragging(state, e); 276 | const about = e.featureTarget.properties; 277 | state.selectedCoordPaths = [about.coord_path]; 278 | state.txMode = TxMode.Scale; 279 | }; 280 | 281 | TxRectMode.onRotatePoint = function(state, e) { 282 | // console.log('onRotatePoint()'); 283 | // convert internal MapboxDraw feature to valid GeoJSON: 284 | this.computeAxes(state, state.feature.toGeoJSON()); 285 | 286 | this.startDragging(state, e); 287 | const about = e.featureTarget.properties; 288 | state.selectedCoordPaths = [about.coord_path]; 289 | state.txMode = TxMode.Rotate; 290 | }; 291 | 292 | TxRectMode.onFeature = function(state, e) { 293 | state.selectedCoordPaths = []; 294 | this.startDragging(state, e); 295 | }; 296 | 297 | TxRectMode.coordinateIndex = function(coordPaths) { 298 | if (coordPaths.length >= 1) { 299 | var parts = coordPaths[0].split('.'); 300 | return parseInt(parts[parts.length - 1]); 301 | } else { 302 | return 0; 303 | } 304 | }; 305 | 306 | TxRectMode.computeRotationCenter = function(state, polygon) { 307 | var center0 = center(polygon); 308 | return center0; 309 | }; 310 | 311 | TxRectMode.computeAxes = function(state, polygon) { 312 | // TODO check min 3 points 313 | const center0 = this.computeRotationCenter(state, polygon); 314 | const corners = polygon.geometry.coordinates[0].slice(0); 315 | 316 | const n = corners.length-1; 317 | const iHalf = Math.floor(n/2); 318 | 319 | // var c0 = corners[corners.length - 1]; 320 | // var headings = corners.map((c1) => { 321 | // var rotPoint = midpoint(point(c0),point(c1)); 322 | // var heading = bearing(center0, rotPoint); 323 | // c0 = c1; 324 | // return heading; 325 | // }); 326 | // headings = headings.slice(1); 327 | 328 | var rotateCenters = []; 329 | var headings = []; 330 | 331 | for (var i1 = 0; i1 < n; i1++) { 332 | var i0 = i1 - 1; 333 | if (i0 < 0) 334 | i0 += n; 335 | 336 | const c0 = corners[i0]; 337 | const c1 = corners[i1]; 338 | const rotPoint = midpoint(point(c0),point(c1)); 339 | 340 | var rotCenter = center0; 341 | if (TxCenter.Opposite === state.rotatePivot) { 342 | var i3 = (i1 + iHalf) % n; // opposite corner 343 | var i2 = i3 - 1; 344 | if (i2 < 0) 345 | i2 += n; 346 | 347 | const c2 = corners[i2]; 348 | const c3 = corners[i3]; 349 | rotCenter = midpoint(point(c2),point(c3)); 350 | } 351 | 352 | rotateCenters[i1] = rotCenter.geometry.coordinates; 353 | headings[i1] = bearing(rotCenter, rotPoint); 354 | } 355 | 356 | state.rotation = { 357 | feature0: polygon, // initial feature state 358 | centers: rotateCenters, 359 | headings: headings, // rotation start heading for each point 360 | }; 361 | 362 | // compute current distances from centers for scaling 363 | 364 | 365 | 366 | var scaleCenters = []; 367 | var distances = []; 368 | for (var i = 0; i < n; i++) { 369 | var c1 = corners[i]; 370 | var c0 = center0.geometry.coordinates; 371 | if (TxCenter.Opposite === state.scaleCenter) { 372 | var i2 = (i + iHalf) % n; // opposite corner 373 | c0 = corners[i2]; 374 | } 375 | scaleCenters[i] = c0; 376 | distances[i] = distance( point(c0), point(c1), { units: 'meters'}); 377 | } 378 | 379 | // var distances = polygon.geometry.coordinates[0].map((c) => 380 | // turf.distance(center, turf.point(c), { units: 'meters'}) ); 381 | 382 | state.scaling = { 383 | feature0: polygon, // initial feature state 384 | centers: scaleCenters, 385 | distances: distances 386 | }; 387 | }; 388 | 389 | TxRectMode.onDrag = function(state, e) { 390 | if (state.canDragMove !== true) return; 391 | state.dragMoving = true; 392 | e.originalEvent.stopPropagation(); 393 | 394 | const delta = { 395 | lng: e.lngLat.lng - state.dragMoveLocation.lng, 396 | lat: e.lngLat.lat - state.dragMoveLocation.lat 397 | }; 398 | if (state.selectedCoordPaths.length > 0 && state.txMode) { 399 | switch (state.txMode) { 400 | case TxMode.Rotate: 401 | this.dragRotatePoint(state, e, delta); 402 | break; 403 | case TxMode.Scale: 404 | this.dragScalePoint(state, e, delta); 405 | break; 406 | } 407 | } else { 408 | this.dragFeature(state, e, delta); 409 | } 410 | 411 | 412 | state.dragMoveLocation = e.lngLat; 413 | }; 414 | 415 | TxRectMode.dragRotatePoint = function(state, e, delta) { 416 | // console.log('dragRotateVertex: ' + e.lngLat + ' -> ' + state.dragMoveLocation); 417 | 418 | if (state.rotation === undefined || state.rotation == null) { 419 | console.error('state.rotation required'); 420 | return ; 421 | } 422 | 423 | var polygon = state.feature.toGeoJSON(); 424 | var m1 = point([e.lngLat.lng, e.lngLat.lat]); 425 | 426 | 427 | const n = state.rotation.centers.length; 428 | var cIdx = (this.coordinateIndex(state.selectedCoordPaths) + 1) % n; 429 | // TODO validate cIdx 430 | var cCenter = state.rotation.centers[cIdx]; 431 | var center = point(cCenter); 432 | 433 | var heading1 = bearing(center, m1); 434 | 435 | var heading0 = state.rotation.headings[cIdx]; 436 | var rotateAngle = heading1 - heading0; // in degrees 437 | if (CommonSelectors.isShiftDown(e)) { 438 | rotateAngle = 5.0 * Math.round(rotateAngle / 5.0); 439 | } 440 | 441 | var rotatedFeature = transformRotate(state.rotation.feature0, 442 | rotateAngle, 443 | { 444 | pivot: center, 445 | mutate: false, 446 | }); 447 | 448 | state.feature.incomingCoords(rotatedFeature.geometry.coordinates); 449 | // TODO add option for this: 450 | this.fireUpdate(); 451 | }; 452 | 453 | TxRectMode.dragScalePoint = function(state, e, delta) { 454 | if (state.scaling === undefined || state.scaling == null) { 455 | console.error('state.scaling required'); 456 | return ; 457 | } 458 | 459 | var polygon = state.feature.toGeoJSON(); 460 | 461 | var cIdx = this.coordinateIndex(state.selectedCoordPaths); 462 | // TODO validate cIdx 463 | 464 | var cCenter = state.scaling.centers[cIdx]; 465 | var center = point(cCenter); 466 | var m1 = point([e.lngLat.lng, e.lngLat.lat]); 467 | 468 | var dist = distance(center, m1, { units: 'meters'}); 469 | var scale = dist / state.scaling.distances[cIdx]; 470 | 471 | if (CommonSelectors.isShiftDown(e)) { 472 | // TODO discrete scaling 473 | scale = 0.05 * Math.round(scale / 0.05); 474 | } 475 | 476 | var scaledFeature = transformScale(state.scaling.feature0, 477 | scale, 478 | { 479 | origin: cCenter, 480 | mutate: false, 481 | }); 482 | 483 | state.feature.incomingCoords(scaledFeature.geometry.coordinates); 484 | // TODO add option for this: 485 | this.fireUpdate(); 486 | }; 487 | 488 | TxRectMode.dragFeature = function(state, e, delta) { 489 | moveFeatures(this.getSelected(), delta); 490 | state.dragMoveLocation = e.lngLat; 491 | // TODO add option for this: 492 | this.fireUpdate(); 493 | }; 494 | 495 | TxRectMode.fireUpdate = function() { 496 | this.map.fire(Constants.events.UPDATE, { 497 | action: Constants.updateActions.CHANGE_COORDINATES, 498 | features: this.getSelected().map(f => f.toGeoJSON()) 499 | }); 500 | }; 501 | 502 | TxRectMode.onMouseOut = function(state) { 503 | // As soon as you mouse leaves the canvas, update the feature 504 | if (state.dragMoving) { 505 | this.fireUpdate(); 506 | } 507 | }; 508 | 509 | TxRectMode.onTouchEnd = TxRectMode.onMouseUp = function(state) { 510 | if (state.dragMoving) { 511 | this.fireUpdate(); 512 | } 513 | this.stopDragging(state); 514 | }; 515 | 516 | TxRectMode.clickActiveFeature = function (state) { 517 | state.selectedCoordPaths = []; 518 | this.clearSelectedCoordinates(); 519 | state.feature.changed(); 520 | }; 521 | 522 | TxRectMode.onClick = function(state, e) { 523 | if (CommonSelectors.noTarget(e)) return this.clickNoTarget(state, e); 524 | if (CommonSelectors.isActiveFeature(e)) return this.clickActiveFeature(state, e); 525 | if (CommonSelectors.isInactiveFeature(e)) return this.clickInactive(state, e); 526 | this.stopDragging(state); 527 | }; 528 | 529 | TxRectMode.clickNoTarget = function (state, e) { 530 | if (state.canSelectFeatures) 531 | this.changeMode(Constants.modes.SIMPLE_SELECT); 532 | }; 533 | 534 | TxRectMode.clickInactive = function (state, e) { 535 | if (state.canSelectFeatures) 536 | this.changeMode(Constants.modes.SIMPLE_SELECT, { 537 | featureIds: [e.featureTarget.properties.id] 538 | }); 539 | }; 540 | 541 | TxRectMode.onTrash = function() { 542 | // TODO check state.canTrash 543 | this.deleteFeature(this.getSelectedIds()); 544 | // this.fireActionable(); 545 | }; 546 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | 5 | devtool: "source-map", 6 | mode: 'development', 7 | 8 | entry: [ 9 | './src/index.js', 10 | './src/demo.js' 11 | ], 12 | output: { 13 | // library: 'app', 14 | // libraryTarget: 'umd', 15 | libraryTarget: 'var', 16 | library: 'TxRectMode', 17 | filename: 'mapbox-gl-draw-rotate-scale-rect-mode.js', 18 | path: path.resolve(__dirname, 'dist') 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.(js|jsx)$/, 24 | exclude: /node_modules/, 25 | use: { 26 | loader: "babel-loader" 27 | } 28 | } 29 | ] 30 | }, 31 | watch: true, 32 | 33 | node: { 34 | // https://github.com/mapbox/mapbox-gl-draw/issues/626 35 | fs: "empty" 36 | } 37 | }; --------------------------------------------------------------------------------