├── .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 | 
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 | 
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 | };
--------------------------------------------------------------------------------