google-maps-area-editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AreaEditor.js +709 -0
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/math.js +65 -0
- package/package.json +26 -0
package/AreaEditor.js
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import { toRadians, toDegrees, Vector2 } from './math.js';
|
|
2
|
+
|
|
3
|
+
export const STATE = Object.freeze({
|
|
4
|
+
INITIAL: "initial",
|
|
5
|
+
DRAWABLE: "drawable",
|
|
6
|
+
DRAWING: "drawing",
|
|
7
|
+
EDITING: "editing",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export class AreaType {
|
|
11
|
+
#name;
|
|
12
|
+
#color;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} name - Type identifier (used for serialization/deserialization)
|
|
16
|
+
* @param {string} color - Display color (CSS color string)
|
|
17
|
+
*/
|
|
18
|
+
constructor(name, color) {
|
|
19
|
+
this.#name = name;
|
|
20
|
+
this.#color = color;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @type {string} */
|
|
24
|
+
get name() { return this.#name; }
|
|
25
|
+
/** @type {string} */
|
|
26
|
+
get color() { return this.#color; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SVG_PATH = {
|
|
30
|
+
CIRCLE: "M0,-5 a5,5 0 1,0 0,10 a5,5 0 1,0 0,-10",
|
|
31
|
+
DOUBLE_CIRCLE: "M0,-5 a5,5 0 1,0 0,10 a5,5 0 1,0 0,-10 M0,-2.5 a2.5,2.5 0 1,0 0,5 a2.5,2.5 0 1,0 0,-5",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function createMarkerSvg(path, scale) {
|
|
35
|
+
// Match original google.maps.Marker Symbol rendering:
|
|
36
|
+
// path coordinates scaled by `scale`, stroke always 1px
|
|
37
|
+
const size = Math.ceil((5 * scale + 1) * 2);
|
|
38
|
+
const half = size / 2;
|
|
39
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
40
|
+
svg.setAttribute("width", size);
|
|
41
|
+
svg.setAttribute("height", size);
|
|
42
|
+
svg.setAttribute("viewBox", `${-half} ${-half} ${size} ${size}`);
|
|
43
|
+
svg.style.display = "block";
|
|
44
|
+
svg.style.transform = "translateY(50%)";
|
|
45
|
+
const pathEl = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
46
|
+
pathEl.setAttribute("d", path);
|
|
47
|
+
pathEl.setAttribute("fill", "#ffffff");
|
|
48
|
+
pathEl.setAttribute("stroke", "#000000");
|
|
49
|
+
pathEl.setAttribute("stroke-width", "1");
|
|
50
|
+
pathEl.setAttribute("vector-effect", "non-scaling-stroke");
|
|
51
|
+
if (scale !== 1) pathEl.setAttribute("transform", `scale(${scale})`);
|
|
52
|
+
svg.appendChild(pathEl);
|
|
53
|
+
return svg;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createDraggableContent(size) {
|
|
57
|
+
const div = document.createElement("div");
|
|
58
|
+
div.style.width = `${size}px`;
|
|
59
|
+
div.style.height = `${size}px`;
|
|
60
|
+
div.style.transform = "translateY(50%)";
|
|
61
|
+
return div;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class AreaEditor {
|
|
65
|
+
#map;
|
|
66
|
+
#drawing;
|
|
67
|
+
/** @type {Area[]} */
|
|
68
|
+
#areas = [];
|
|
69
|
+
/** @type {Area|null} */
|
|
70
|
+
#editingArea = null;
|
|
71
|
+
#currentState = STATE.INITIAL;
|
|
72
|
+
#areaType;
|
|
73
|
+
#eventLayer;
|
|
74
|
+
/** @type {Map<string, AreaType>} */
|
|
75
|
+
#typesMap;
|
|
76
|
+
#mapClickListener;
|
|
77
|
+
|
|
78
|
+
onStateChange = (state) => {};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Creates an AreaEditor. Automatically loads required Google Maps libraries (geometry, marker).
|
|
82
|
+
* @param {google.maps.Map} map
|
|
83
|
+
* @param {Object} [options]
|
|
84
|
+
* @param {AreaType[]} options.types - Available area types
|
|
85
|
+
* @returns {Promise<AreaEditor>}
|
|
86
|
+
*/
|
|
87
|
+
static async create(map, options = {}) {
|
|
88
|
+
await google.maps.importLibrary("geometry");
|
|
89
|
+
await google.maps.importLibrary("marker");
|
|
90
|
+
return new AreaEditor(map, options);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {google.maps.Map} map
|
|
95
|
+
* @param {Object} [options]
|
|
96
|
+
* @param {AreaType[]} options.types - Available area types
|
|
97
|
+
*/
|
|
98
|
+
constructor(map, options = {}) {
|
|
99
|
+
this.#map = map;
|
|
100
|
+
this.#typesMap = new Map((options.types || []).map(t => [t.name, t]));
|
|
101
|
+
this.#drawing = new Drawing(this.#map);
|
|
102
|
+
|
|
103
|
+
this.#eventLayer = document.createElement("div");
|
|
104
|
+
this.#eventLayer.style.position = "absolute";
|
|
105
|
+
this.#eventLayer.style.top = "0";
|
|
106
|
+
this.#eventLayer.style.left = "0";
|
|
107
|
+
this.#eventLayer.style.width = "100%";
|
|
108
|
+
this.#eventLayer.style.height = "100%";
|
|
109
|
+
this.#eventLayer.style.zIndex = "10";
|
|
110
|
+
this.#eventLayer.style.cursor = "crosshair";
|
|
111
|
+
this.#eventLayer.style.display = "none";
|
|
112
|
+
|
|
113
|
+
const mapDiv = this.#map.getDiv();
|
|
114
|
+
mapDiv.appendChild(this.#eventLayer);
|
|
115
|
+
|
|
116
|
+
this.#mapClickListener = this.#map.addListener("click", () => {
|
|
117
|
+
this.endEditArea();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
this.#initEvents();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#setState(state) {
|
|
124
|
+
this.#currentState = state;
|
|
125
|
+
|
|
126
|
+
if (state === STATE.DRAWABLE || state === STATE.DRAWING) {
|
|
127
|
+
this.#eventLayer.style.display = "block";
|
|
128
|
+
} else {
|
|
129
|
+
this.#eventLayer.style.display = "none";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (typeof this.onStateChange === "function") {
|
|
133
|
+
this.onStateChange(state);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#isState(state) {
|
|
138
|
+
return this.#currentState === state;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#initEvents() {
|
|
142
|
+
this.#eventLayer.addEventListener("mousedown", event => {
|
|
143
|
+
if (event.button === 0) {
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
if (this.#isState(STATE.DRAWABLE)) {
|
|
146
|
+
this.#drawing.begin(event.offsetX, event.offsetY);
|
|
147
|
+
this.#setState(STATE.DRAWING);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
this.#eventLayer.addEventListener("mousemove", event => {
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
if (this.#isState(STATE.DRAWING)) {
|
|
155
|
+
this.#drawing.move(event.offsetX, event.offsetY);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
this.#eventLayer.addEventListener("mouseup", event => {
|
|
160
|
+
if (event.button === 0) {
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
if (this.#isState(STATE.DRAWING)) {
|
|
163
|
+
this.#drawing.end(event.offsetX, event.offsetY);
|
|
164
|
+
this.#setState(STATE.DRAWABLE);
|
|
165
|
+
const area = this.#drawing.toArea(this.#areaType);
|
|
166
|
+
if (area) {
|
|
167
|
+
area.onclick = () => {
|
|
168
|
+
this.beginEditArea(area, false);
|
|
169
|
+
};
|
|
170
|
+
this.#areas.push(area);
|
|
171
|
+
this.beginEditArea(area, false);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
this.#eventLayer.addEventListener("mouseout", event => {
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
if (this.#isState(STATE.DRAWING)) {
|
|
180
|
+
this.#drawing.end(event.offsetX, event.offsetY);
|
|
181
|
+
this.#setState(STATE.DRAWABLE);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- Public API ---
|
|
187
|
+
|
|
188
|
+
setAreaType(type) {
|
|
189
|
+
this.endEditArea();
|
|
190
|
+
this.#areaType = type;
|
|
191
|
+
this.#setState(STATE.DRAWABLE);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
cancelAdd() {
|
|
195
|
+
if (this.#isState(STATE.DRAWING)) {
|
|
196
|
+
this.#drawing.end();
|
|
197
|
+
}
|
|
198
|
+
this.#setState(STATE.INITIAL);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
loadAreas(jsonString) {
|
|
202
|
+
this.#clearAreas();
|
|
203
|
+
if (!jsonString) return;
|
|
204
|
+
const parsed = JSON.parse(jsonString);
|
|
205
|
+
this.#areas = parsed.map(a => {
|
|
206
|
+
const type = this.#typesMap.get(a.type);
|
|
207
|
+
if (!type) {
|
|
208
|
+
throw new Error(`Unknown area type: "${a.type}"`);
|
|
209
|
+
}
|
|
210
|
+
const area = Area.fromUIArea(this.#map, type, a);
|
|
211
|
+
area.onclick = () => {
|
|
212
|
+
this.beginEditArea(area, false);
|
|
213
|
+
};
|
|
214
|
+
return area;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
exportAreasJSON() {
|
|
219
|
+
return JSON.stringify(this.#areas.map(a => a.toUIArea()));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
beginEditArea(area, mapCenter) {
|
|
223
|
+
if (this.#editingArea) {
|
|
224
|
+
this.#editingArea.editable = false;
|
|
225
|
+
}
|
|
226
|
+
area.editable = true;
|
|
227
|
+
this.#editingArea = area;
|
|
228
|
+
this.#setState(STATE.EDITING);
|
|
229
|
+
if (mapCenter) {
|
|
230
|
+
this.#map.setCenter(this.#editingArea.center);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
endEditArea() {
|
|
235
|
+
if (this.#editingArea) {
|
|
236
|
+
this.#editingArea.editable = false;
|
|
237
|
+
this.#editingArea = null;
|
|
238
|
+
this.#setState(STATE.INITIAL);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
editNext() {
|
|
243
|
+
if (this.#areas.length) {
|
|
244
|
+
const i = this.#areas.indexOf(this.#editingArea);
|
|
245
|
+
this.beginEditArea(this.#areas[(i + 1 + this.#areas.length) % this.#areas.length], true);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
editPrev() {
|
|
250
|
+
if (this.#areas.length) {
|
|
251
|
+
const i = this.#areas.indexOf(this.#editingArea);
|
|
252
|
+
this.beginEditArea(this.#areas[(Math.max(i, 0) - 1 + this.#areas.length) % this.#areas.length], true);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
removeEditingArea() {
|
|
257
|
+
if (this.#editingArea) {
|
|
258
|
+
const i = this.#areas.indexOf(this.#editingArea);
|
|
259
|
+
if (i >= 0) {
|
|
260
|
+
this.#editingArea.destroy();
|
|
261
|
+
this.#editingArea = null;
|
|
262
|
+
this.#areas.splice(i, 1);
|
|
263
|
+
this.#setState(STATE.INITIAL);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
destroy() {
|
|
269
|
+
this.cancelAdd();
|
|
270
|
+
this.endEditArea();
|
|
271
|
+
this.#clearAreas();
|
|
272
|
+
this.#drawing.destroy();
|
|
273
|
+
google.maps.event.removeListener(this.#mapClickListener);
|
|
274
|
+
this.#eventLayer.remove();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// --- Private helpers ---
|
|
278
|
+
|
|
279
|
+
#clearAreas() {
|
|
280
|
+
this.#areas.forEach(a => a.destroy());
|
|
281
|
+
this.#areas = [];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Internal helper classes and functions
|
|
286
|
+
|
|
287
|
+
function toLatLng(map, x, y) {
|
|
288
|
+
const projection = map.getProjection();
|
|
289
|
+
const bounds = map.getBounds();
|
|
290
|
+
const ne = projection.fromLatLngToPoint(bounds.getNorthEast());
|
|
291
|
+
const sw = projection.fromLatLngToPoint(bounds.getSouthWest());
|
|
292
|
+
const scale = 1 << map.getZoom();
|
|
293
|
+
return projection.fromPointToLatLng(new google.maps.Point(x / scale + sw.x, y / scale + ne.y));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function toVector(latLngFrom, latLngTo) {
|
|
297
|
+
return Vector2.create(
|
|
298
|
+
toRadians(google.maps.geometry.spherical.computeHeading(latLngFrom, latLngTo)),
|
|
299
|
+
google.maps.geometry.spherical.computeDistanceBetween(latLngFrom, latLngTo)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function computeOffset(from, distance, angle) {
|
|
304
|
+
return google.maps.geometry.spherical.computeOffset(from, Math.abs(distance), toDegrees(angle + (distance < 0 ? Math.PI : 0)));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function latLngArrayToBounds(points) {
|
|
308
|
+
const bounds = new google.maps.LatLngBounds();
|
|
309
|
+
points.forEach(p => bounds.extend(p));
|
|
310
|
+
return bounds;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
class Drawing {
|
|
314
|
+
#map;
|
|
315
|
+
#rectangle;
|
|
316
|
+
#drawing = false;
|
|
317
|
+
#origin;
|
|
318
|
+
#diagonal;
|
|
319
|
+
|
|
320
|
+
constructor(map) {
|
|
321
|
+
this.#map = map;
|
|
322
|
+
this.#rectangle = new google.maps.Rectangle({
|
|
323
|
+
strokeColor: "#000000",
|
|
324
|
+
strokeOpacity: 1.0,
|
|
325
|
+
strokeWeight: 1,
|
|
326
|
+
fillColor: "#000000",
|
|
327
|
+
fillOpacity: 0.1,
|
|
328
|
+
clickable: false,
|
|
329
|
+
map: this.#map,
|
|
330
|
+
zIndex: 1000,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
begin(x, y) {
|
|
335
|
+
if (!this.#drawing) {
|
|
336
|
+
this.#origin = toLatLng(this.#map, x, y);
|
|
337
|
+
this.#diagonal = this.#origin;
|
|
338
|
+
this.#showBounds();
|
|
339
|
+
this.#drawing = true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
move(x, y) {
|
|
344
|
+
if (this.#drawing) {
|
|
345
|
+
this.#diagonal = toLatLng(this.#map, x, y);
|
|
346
|
+
this.#showBounds();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
end(x, y) {
|
|
351
|
+
if (this.#drawing) {
|
|
352
|
+
if (Number.isFinite(x) && Number.isFinite(y)) {
|
|
353
|
+
this.#diagonal = toLatLng(this.#map, x, y);
|
|
354
|
+
}
|
|
355
|
+
this.#hideBounds();
|
|
356
|
+
this.#drawing = false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
toArea(type) {
|
|
361
|
+
return this.#origin
|
|
362
|
+
&& this.#diagonal
|
|
363
|
+
&& !this.#origin.equals(this.#diagonal)
|
|
364
|
+
&& Area.create(
|
|
365
|
+
this.#map,
|
|
366
|
+
type,
|
|
367
|
+
google.maps.geometry.spherical.interpolate(this.#origin, this.#diagonal, 0.5),
|
|
368
|
+
toVector(this.#origin, this.#diagonal)
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
#showBounds() {
|
|
373
|
+
const bounds = latLngArrayToBounds([this.#origin, this.#diagonal]);
|
|
374
|
+
this.#rectangle.setBounds(bounds);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#hideBounds() {
|
|
378
|
+
this.#rectangle.setBounds(null);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
destroy() {
|
|
382
|
+
this.#rectangle.setMap(null);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
class Area {
|
|
387
|
+
static create(map, type, center, vDiagonal) {
|
|
388
|
+
return new Area(map, type, center, Math.abs(vDiagonal.y), Math.abs(vDiagonal.x), 0);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
static fromUIArea(map, type, area) {
|
|
392
|
+
return new Area(
|
|
393
|
+
map,
|
|
394
|
+
type,
|
|
395
|
+
new google.maps.LatLng(area.latitude, area.longitude),
|
|
396
|
+
area.width,
|
|
397
|
+
area.height,
|
|
398
|
+
area.angle
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
#map;
|
|
403
|
+
#type;
|
|
404
|
+
#center;
|
|
405
|
+
#width;
|
|
406
|
+
#height;
|
|
407
|
+
#angle;
|
|
408
|
+
#distN;
|
|
409
|
+
#distE;
|
|
410
|
+
#polygon;
|
|
411
|
+
#corners = [[1, 1], [-1, 1], [-1, -1], [1, -1]];
|
|
412
|
+
#anchors;
|
|
413
|
+
#rotator;
|
|
414
|
+
#editable = false;
|
|
415
|
+
#listeners = [];
|
|
416
|
+
onclick = () => { };
|
|
417
|
+
|
|
418
|
+
constructor(map, type, center, width, height, angle) {
|
|
419
|
+
this.#map = map;
|
|
420
|
+
this.#type = type;
|
|
421
|
+
this.#center = center;
|
|
422
|
+
this.#width = width;
|
|
423
|
+
this.#height = height;
|
|
424
|
+
this.#angle = angle;
|
|
425
|
+
this.#distN = height / 2;
|
|
426
|
+
this.#distE = width / 2;
|
|
427
|
+
|
|
428
|
+
this.#polygon = new google.maps.Polygon({
|
|
429
|
+
strokeColor: type.color,
|
|
430
|
+
strokeOpacity: 1.0,
|
|
431
|
+
strokeWeight: 1,
|
|
432
|
+
fillColor: type.color,
|
|
433
|
+
fillOpacity: 0.1,
|
|
434
|
+
clickable: true,
|
|
435
|
+
draggable: false,
|
|
436
|
+
map: this.#map,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
this.#listeners.push(this.#polygon.addListener("click", () => this.onclick()));
|
|
440
|
+
|
|
441
|
+
this.#listeners.push(this.#polygon.addListener("drag", () => {
|
|
442
|
+
const path = this.#polygon.getPath();
|
|
443
|
+
this.#center = google.maps.geometry.spherical.interpolate(path.getAt(0), path.getAt(2), 0.5);
|
|
444
|
+
this.#showAnchors();
|
|
445
|
+
this.#showRotator();
|
|
446
|
+
}));
|
|
447
|
+
|
|
448
|
+
this.#anchors = [
|
|
449
|
+
new AreaAnchor(this.#map, 1, 1), new AreaAnchor(this.#map, 0, 1), new AreaAnchor(this.#map, -1, 1),
|
|
450
|
+
new AreaAnchor(this.#map, -1, 0), new AreaAnchor(this.#map, -1, -1), new AreaAnchor(this.#map, 0, -1),
|
|
451
|
+
new AreaAnchor(this.#map, 1, -1), new AreaAnchor(this.#map, 1, 0)
|
|
452
|
+
];
|
|
453
|
+
|
|
454
|
+
this.#rotator = new AreaRotator(this.#map);
|
|
455
|
+
|
|
456
|
+
this.#showBounds();
|
|
457
|
+
this.#showAnchors();
|
|
458
|
+
|
|
459
|
+
this.#anchors.forEach(a => {
|
|
460
|
+
a.onresize = (center, height, width) => {
|
|
461
|
+
this.#center = center;
|
|
462
|
+
if (Number.isFinite(height)) {
|
|
463
|
+
this.#height = height;
|
|
464
|
+
this.#distN = height / 2;
|
|
465
|
+
}
|
|
466
|
+
if (Number.isFinite(width)) {
|
|
467
|
+
this.#width = width;
|
|
468
|
+
this.#distE = width / 2;
|
|
469
|
+
}
|
|
470
|
+
this.#showBounds();
|
|
471
|
+
this.#showAnchors();
|
|
472
|
+
this.#showRotator();
|
|
473
|
+
};
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
this.#showRotator();
|
|
477
|
+
this.#rotator.onrotate = angle => {
|
|
478
|
+
this.#angle = angle;
|
|
479
|
+
this.#showBounds();
|
|
480
|
+
this.#showAnchors();
|
|
481
|
+
this.#showRotator();
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
this.#listeners.push(this.#map.addListener("zoom_changed", () => {
|
|
485
|
+
this.#showRotator();
|
|
486
|
+
}));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
get center() { return this.#center; }
|
|
490
|
+
get bounds() { return this.#polygon.getPath().getArray(); }
|
|
491
|
+
get editable() { return this.#editable; }
|
|
492
|
+
set editable(editable) {
|
|
493
|
+
this.#editable = editable;
|
|
494
|
+
this.#showBounds();
|
|
495
|
+
this.#showAnchors();
|
|
496
|
+
this.#showRotator();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
destroy() {
|
|
500
|
+
this.#listeners.forEach(l => google.maps.event.removeListener(l));
|
|
501
|
+
this.#rotator.destroy();
|
|
502
|
+
this.#anchors.forEach(a => a.destroy());
|
|
503
|
+
this.#polygon.setMap(null);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
toUIArea() {
|
|
507
|
+
return {
|
|
508
|
+
type: this.#type.name,
|
|
509
|
+
latitude: this.#center.lat(),
|
|
510
|
+
longitude: this.#center.lng(),
|
|
511
|
+
width: this.#width,
|
|
512
|
+
height: this.#height,
|
|
513
|
+
angle: this.#angle,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
#showBounds() {
|
|
518
|
+
this.#polygon.setPath(this.#corners.map(c => {
|
|
519
|
+
const v = new Vector2(this.#distN * c[0], this.#distE * c[1]);
|
|
520
|
+
return computeOffset(this.#center, v.magnitude, v.angle + this.#angle);
|
|
521
|
+
}));
|
|
522
|
+
this.#polygon.setOptions({
|
|
523
|
+
draggable: this.#editable,
|
|
524
|
+
zIndex: this.#editable ? 101 : 100,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
#showAnchors() {
|
|
529
|
+
this.#anchors.forEach(a => a.show(this.#center, this.#distN, this.#distE, this.#angle, this.#editable));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
#showRotator() {
|
|
533
|
+
this.#rotator.show(this.#center, this.#distN, this.#angle, this.#editable);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
class AreaAnchor {
|
|
538
|
+
#map;
|
|
539
|
+
#timesN;
|
|
540
|
+
#timesE;
|
|
541
|
+
#marker;
|
|
542
|
+
#draggable;
|
|
543
|
+
#origin;
|
|
544
|
+
#vectorN;
|
|
545
|
+
#vectorE;
|
|
546
|
+
#dragging;
|
|
547
|
+
onresize = (center, height, width) => { };
|
|
548
|
+
|
|
549
|
+
constructor(map, timesN, timesE) {
|
|
550
|
+
this.#map = map;
|
|
551
|
+
this.#timesN = timesN;
|
|
552
|
+
this.#timesE = timesE;
|
|
553
|
+
|
|
554
|
+
this.#marker = new google.maps.marker.AdvancedMarkerElement({
|
|
555
|
+
map: this.#map,
|
|
556
|
+
content: createMarkerSvg(SVG_PATH.CIRCLE, 1.0),
|
|
557
|
+
gmpClickable: false,
|
|
558
|
+
zIndex: 200,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const createDraggable = () => {
|
|
562
|
+
const marker = new google.maps.marker.AdvancedMarkerElement({
|
|
563
|
+
map: this.#map,
|
|
564
|
+
content: createDraggableContent(12),
|
|
565
|
+
gmpDraggable: true,
|
|
566
|
+
zIndex: 201,
|
|
567
|
+
});
|
|
568
|
+
marker.style.opacity = "0";
|
|
569
|
+
const listeners = [];
|
|
570
|
+
listeners.push(marker.addListener("dragstart", () => {
|
|
571
|
+
this.#dragging = this.#draggable;
|
|
572
|
+
this.#draggable = createDraggable();
|
|
573
|
+
}));
|
|
574
|
+
listeners.push(marker.addListener("drag", event => doResize(event)));
|
|
575
|
+
listeners.push(marker.addListener("dragend", event => {
|
|
576
|
+
listeners.forEach(l => google.maps.event.removeListener(l));
|
|
577
|
+
this.#dragging.map = null;
|
|
578
|
+
this.#dragging = null;
|
|
579
|
+
doResize(event);
|
|
580
|
+
}));
|
|
581
|
+
return marker;
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
this.#draggable = createDraggable();
|
|
585
|
+
|
|
586
|
+
const doResize = event => {
|
|
587
|
+
if (this.#dragging) {
|
|
588
|
+
const v = toVector(this.#origin, event.latLng);
|
|
589
|
+
const height = this.#timesN ? this.#vectorN.dot(v) : null;
|
|
590
|
+
const width = this.#timesE ? this.#vectorE.dot(v) : null;
|
|
591
|
+
const cv = this.#timesN && this.#timesE ? v :
|
|
592
|
+
this.#timesN ? this.#vectorN.times(height) :
|
|
593
|
+
this.#vectorE.times(width);
|
|
594
|
+
this.onresize(computeOffset(this.#origin, cv.magnitude / 2, cv.angle), height, width);
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
show(center, distN, distE, angle, editable) {
|
|
600
|
+
const vn = new Vector2(distN * this.#timesN, 0).rot(angle);
|
|
601
|
+
const ve = new Vector2(0, distE * this.#timesE).rot(angle);
|
|
602
|
+
const v = vn.add(ve);
|
|
603
|
+
const position = computeOffset(center, v.magnitude, v.angle);
|
|
604
|
+
this.#marker.position = position;
|
|
605
|
+
this.#marker.map = editable ? this.#map : null;
|
|
606
|
+
this.#draggable.position = position;
|
|
607
|
+
this.#draggable.map = editable ? this.#map : null;
|
|
608
|
+
if (!this.#dragging) {
|
|
609
|
+
this.#origin = computeOffset(center, v.magnitude, v.angle + Math.PI);
|
|
610
|
+
this.#vectorN = vn.times(Math.sign(distN)).normalized;
|
|
611
|
+
this.#vectorE = ve.times(Math.sign(distE)).normalized;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
destroy() {
|
|
616
|
+
// TODO: Ideally should also remove event listeners from draggable markers
|
|
617
|
+
if (this.#marker) this.#marker.map = null;
|
|
618
|
+
if (this.#draggable) this.#draggable.map = null;
|
|
619
|
+
if (this.#dragging) this.#dragging.map = null;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
class AreaRotator {
|
|
624
|
+
#map;
|
|
625
|
+
#line;
|
|
626
|
+
#marker;
|
|
627
|
+
#draggable;
|
|
628
|
+
#dragging;
|
|
629
|
+
#center;
|
|
630
|
+
#inverse;
|
|
631
|
+
onrotate = angle => { };
|
|
632
|
+
|
|
633
|
+
constructor(map) {
|
|
634
|
+
this.#map = map;
|
|
635
|
+
this.#line = new google.maps.Polyline({
|
|
636
|
+
strokeColor: "#000000",
|
|
637
|
+
strokeOpacity: 1.0,
|
|
638
|
+
strokeWeight: 1.0,
|
|
639
|
+
map: this.#map,
|
|
640
|
+
clickable: false,
|
|
641
|
+
draggable: false,
|
|
642
|
+
zIndex: 190,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
this.#marker = new google.maps.marker.AdvancedMarkerElement({
|
|
646
|
+
map: this.#map,
|
|
647
|
+
content: createMarkerSvg(SVG_PATH.DOUBLE_CIRCLE, 2.0),
|
|
648
|
+
gmpClickable: false,
|
|
649
|
+
zIndex: 200,
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
const createDraggable = () => {
|
|
653
|
+
const marker = new google.maps.marker.AdvancedMarkerElement({
|
|
654
|
+
map: this.#map,
|
|
655
|
+
content: createDraggableContent(22),
|
|
656
|
+
gmpDraggable: true,
|
|
657
|
+
zIndex: 201,
|
|
658
|
+
});
|
|
659
|
+
marker.style.opacity = "0";
|
|
660
|
+
const listeners = [];
|
|
661
|
+
listeners.push(marker.addListener("dragstart", () => {
|
|
662
|
+
this.#dragging = this.#draggable;
|
|
663
|
+
this.#draggable = createDraggable();
|
|
664
|
+
}));
|
|
665
|
+
listeners.push(marker.addListener("drag", event => doRotate(event)));
|
|
666
|
+
listeners.push(marker.addListener("dragend", event => {
|
|
667
|
+
listeners.forEach(l => google.maps.event.removeListener(l));
|
|
668
|
+
this.#dragging.map = null;
|
|
669
|
+
this.#dragging = null;
|
|
670
|
+
doRotate(event);
|
|
671
|
+
}));
|
|
672
|
+
return marker;
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
this.#draggable = createDraggable();
|
|
676
|
+
|
|
677
|
+
const doRotate = event => {
|
|
678
|
+
if (this.#dragging) {
|
|
679
|
+
let v = toVector(this.#center, event.latLng);
|
|
680
|
+
if (this.#inverse) {
|
|
681
|
+
v = v.inverse;
|
|
682
|
+
}
|
|
683
|
+
this.onrotate(v.angle);
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
show(center, distN, angle, editable) {
|
|
689
|
+
const scale = 1 << this.#map.getZoom();
|
|
690
|
+
const positionN = computeOffset(center, distN, angle);
|
|
691
|
+
const position = computeOffset(positionN, 5000000 * Math.sign(distN) / scale, angle);
|
|
692
|
+
this.#line.setPath([positionN, position]);
|
|
693
|
+
this.#line.setVisible(editable);
|
|
694
|
+
this.#marker.position = position;
|
|
695
|
+
this.#marker.map = editable ? this.#map : null;
|
|
696
|
+
this.#draggable.position = position;
|
|
697
|
+
this.#draggable.map = editable ? this.#map : null;
|
|
698
|
+
this.#center = center;
|
|
699
|
+
this.#inverse = distN < 0;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
destroy() {
|
|
703
|
+
// TODO: Ideally should also remove event listeners from draggable markers
|
|
704
|
+
this.#line?.setMap(null);
|
|
705
|
+
if (this.#marker) this.#marker.map = null;
|
|
706
|
+
if (this.#draggable) this.#draggable.map = null;
|
|
707
|
+
if (this.#dragging) this.#dragging.map = null;
|
|
708
|
+
}
|
|
709
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hiroyukashi
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# google-maps-area-editor
|
|
2
|
+
|
|
3
|
+
A rectangular area editor component for Google Maps JavaScript API.
|
|
4
|
+
Allows users to draw, resize, rotate, and manage rectangular areas on a Google Map.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- Draw rectangular areas by mouse drag
|
|
9
|
+
- Resize areas via corner and edge anchors
|
|
10
|
+
- Rotate areas via a rotation handle
|
|
11
|
+
- Multiple area type support with custom colors
|
|
12
|
+
- Serialize / deserialize areas as JSON
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- Google Maps JavaScript API (v3.56+) with an API key
|
|
17
|
+
- The library uses `AdvancedMarkerElement` internally (loaded automatically by `AreaEditor.create()`). The map **must** be initialized with a `mapId`. For testing, you can use `mapId: 'DEMO_MAP_ID'`.
|
|
18
|
+
- The map **must** have `isFractionalZoomEnabled: false` set. The internal pixel-to-coordinate conversion uses `1 << map.getZoom()`, which requires integer zoom levels. Fractional zoom also causes excessive event firing that leads to rendering issues.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<script type="module">
|
|
24
|
+
import { AreaEditor, AreaType, STATE } from './AreaEditor.js';
|
|
25
|
+
|
|
26
|
+
// Define your area types
|
|
27
|
+
const AREA_TYPES = {
|
|
28
|
+
usual: new AreaType("usual", "#00bbdd"),
|
|
29
|
+
dangerous: new AreaType("dangerous", "#ff0000"),
|
|
30
|
+
custom: new AreaType("custom", "#9900ff"),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const { Map } = await google.maps.importLibrary("maps");
|
|
34
|
+
|
|
35
|
+
const map = new Map(document.getElementById("map"), {
|
|
36
|
+
center: { lat: 35.681236, lng: 139.767125 },
|
|
37
|
+
zoom: 18,
|
|
38
|
+
mapId: 'DEMO_MAP_ID', // Required for AdvancedMarkerElement
|
|
39
|
+
isFractionalZoomEnabled: false,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Create editor (automatically loads geometry & marker libraries)
|
|
43
|
+
const editor = await AreaEditor.create(map, {
|
|
44
|
+
types: Object.values(AREA_TYPES),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Listen for state changes
|
|
48
|
+
editor.onStateChange = state => {
|
|
49
|
+
console.log("State:", state); // STATE.INITIAL | STATE.DRAWABLE | STATE.DRAWING | STATE.EDITING
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Start drawing
|
|
53
|
+
editor.setAreaType(AREA_TYPES.usual);
|
|
54
|
+
|
|
55
|
+
// Cancel drawing mode
|
|
56
|
+
editor.cancelAdd();
|
|
57
|
+
|
|
58
|
+
// Navigate / remove editing area
|
|
59
|
+
editor.editNext();
|
|
60
|
+
editor.editPrev();
|
|
61
|
+
editor.removeEditingArea();
|
|
62
|
+
|
|
63
|
+
// Load / export
|
|
64
|
+
editor.loadAreas(jsonString);
|
|
65
|
+
const json = editor.exportAreasJSON();
|
|
66
|
+
|
|
67
|
+
// Clean up
|
|
68
|
+
editor.destroy();
|
|
69
|
+
</script>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## API
|
|
73
|
+
|
|
74
|
+
### `AreaType`
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
new AreaType(name, color)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- `name` (string) — Identifier used for serialization / deserialization.
|
|
81
|
+
- `color` (string) — CSS color string for the area's stroke and fill.
|
|
82
|
+
|
|
83
|
+
### `AreaEditor`
|
|
84
|
+
|
|
85
|
+
#### Static
|
|
86
|
+
|
|
87
|
+
| Method | Description |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `AreaEditor.create(map, options?)` | Async factory. Loads required Google Maps libraries and returns an `AreaEditor` instance. |
|
|
90
|
+
|
|
91
|
+
#### Constructor Options
|
|
92
|
+
|
|
93
|
+
| Option | Type | Description |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `types` | `AreaType[]` | Array of area types available for use. Required for `loadAreas()` to resolve type names. |
|
|
96
|
+
|
|
97
|
+
#### Instance Methods
|
|
98
|
+
|
|
99
|
+
| Method | Description |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `setAreaType(type)` | Enter drawing mode for the given `AreaType`. |
|
|
102
|
+
| `cancelAdd()` | Cancel drawing mode. |
|
|
103
|
+
| `loadAreas(jsonString)` | Load areas from JSON. Destroys any existing areas first. |
|
|
104
|
+
| `exportAreasJSON()` | Returns areas as a JSON string. |
|
|
105
|
+
| `editNext()` | Edit the next area. |
|
|
106
|
+
| `editPrev()` | Edit the previous area. |
|
|
107
|
+
| `removeEditingArea()` | Remove the currently editing area. |
|
|
108
|
+
| `destroy()` | Remove all areas, event listeners, and the overlay from the map. |
|
|
109
|
+
|
|
110
|
+
#### Callback
|
|
111
|
+
|
|
112
|
+
| Property | Description |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `onStateChange` | `(state: string) => void` — Called when editor state changes. |
|
|
115
|
+
|
|
116
|
+
### `STATE`
|
|
117
|
+
|
|
118
|
+
Exported constants for state comparison:
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
STATE.INITIAL // "initial"
|
|
122
|
+
STATE.DRAWABLE // "drawable"
|
|
123
|
+
STATE.DRAWING // "drawing"
|
|
124
|
+
STATE.EDITING // "editing"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## JSON Format
|
|
128
|
+
|
|
129
|
+
Each area is serialized as:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"type": "usual",
|
|
134
|
+
"latitude": 35.681236,
|
|
135
|
+
"longitude": 139.767125,
|
|
136
|
+
"width": 100,
|
|
137
|
+
"height": 50,
|
|
138
|
+
"angle": 0.5
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- `type` — Matches the `name` of an `AreaType`.
|
|
143
|
+
- `latitude` / `longitude` — Center of the area.
|
|
144
|
+
- `width` / `height` — Size in meters.
|
|
145
|
+
- `angle` — Rotation angle in radians.
|
|
146
|
+
|
|
147
|
+
## Demo
|
|
148
|
+
|
|
149
|
+
> **Note:** Because this project uses ES modules (`<script type="module">`), you cannot run the demo by directly opening `index.html` in your browser (via the `file://` protocol) due to CORS restrictions. Please use a local web server (e.g., VS Code Live Server, `npx serve`, or `python -m http.server`) to view it.
|
|
150
|
+
|
|
151
|
+
Open `index.html` in a browser to try the editor. To use your own API key:
|
|
152
|
+
|
|
153
|
+
1. Copy `config.example.js` to `config.local.js`
|
|
154
|
+
2. Replace `YOUR_API_KEY` with your Google Maps API key
|
|
155
|
+
3. `config.local.js` is gitignored and will not be committed
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
package/math.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export function toRadians(degrees) {
|
|
2
|
+
return degrees * Math.PI / 180;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function toDegrees(radians) {
|
|
6
|
+
return radians * 180 / Math.PI;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class Vector2 {
|
|
10
|
+
/** @type {number} */
|
|
11
|
+
#x;
|
|
12
|
+
/** @type {number} */
|
|
13
|
+
#y;
|
|
14
|
+
/** @type {number} */
|
|
15
|
+
#magnitude;
|
|
16
|
+
/** @type {number} */
|
|
17
|
+
#angle;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {number} x
|
|
21
|
+
* @param {number} y
|
|
22
|
+
*/
|
|
23
|
+
constructor(x, y) {
|
|
24
|
+
this.#x = x;
|
|
25
|
+
this.#y = y;
|
|
26
|
+
this.#magnitude = Math.sqrt(Math.pow(x, 2.0) + Math.pow(y, 2.0));
|
|
27
|
+
this.#angle = Math.atan2(y, x);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get x() { return this.#x; }
|
|
31
|
+
get y() { return this.#y; }
|
|
32
|
+
get magnitude() { return this.#magnitude; }
|
|
33
|
+
get angle() { return this.#angle; }
|
|
34
|
+
|
|
35
|
+
get normalized() {
|
|
36
|
+
return new Vector2(this.#x / this.#magnitude, this.#y / this.#magnitude);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get inverse() {
|
|
40
|
+
return this.times(-1.0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
times(n) {
|
|
44
|
+
return new Vector2(this.#x * n, this.#y * n);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
add(v) {
|
|
48
|
+
return new Vector2(this.#x + v.#x, this.#y + v.#y);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
rot(angle) {
|
|
52
|
+
return new Vector2(
|
|
53
|
+
this.#x * Math.cos(angle) - this.#y * Math.sin(angle),
|
|
54
|
+
this.#x * Math.sin(angle) + this.#y * Math.cos(angle)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
dot(v) {
|
|
59
|
+
return this.#x * v.#x + this.#y * v.#y;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static create(angle, magnitude) {
|
|
63
|
+
return new Vector2(magnitude * Math.cos(angle), magnitude * Math.sin(angle));
|
|
64
|
+
}
|
|
65
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "google-maps-area-editor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "An intuitive, draggable, and rotatable area editor component for the Google Maps JavaScript API.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./AreaEditor.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"AreaEditor.js",
|
|
11
|
+
"math.js"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"google-maps",
|
|
15
|
+
"area",
|
|
16
|
+
"editor",
|
|
17
|
+
"rectangle",
|
|
18
|
+
"polygon"
|
|
19
|
+
],
|
|
20
|
+
"author": "hiroyukashi",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/hiroyukashi/google-maps-area-editor.git"
|
|
25
|
+
}
|
|
26
|
+
}
|