@fieldia/map 0.14.0 → 0.15.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/README.md +74 -7
- package/package.json +5 -4
- package/src/index.d.mts +3 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +5 -1
- package/src/index.js.map +1 -1
- package/src/index.mjs +2 -0
- package/src/index.mjs.map +1 -1
- package/src/lib/area.js +29 -246
- package/src/lib/area.js.map +1 -1
- package/src/lib/area.mjs +29 -246
- package/src/lib/area.mjs.map +1 -1
- package/src/lib/card.d.mts +45 -0
- package/src/lib/card.d.ts +45 -0
- package/src/lib/card.js +89 -0
- package/src/lib/card.js.map +1 -0
- package/src/lib/card.mjs +85 -0
- package/src/lib/card.mjs.map +1 -0
- package/src/lib/cluster.d.mts +12 -0
- package/src/lib/cluster.d.ts +12 -0
- package/src/lib/cluster.js +60 -0
- package/src/lib/cluster.js.map +1 -0
- package/src/lib/cluster.mjs +57 -0
- package/src/lib/cluster.mjs.map +1 -0
- package/src/lib/drawing.d.mts +36 -0
- package/src/lib/drawing.d.ts +36 -0
- package/src/lib/drawing.js +267 -0
- package/src/lib/drawing.js.map +1 -0
- package/src/lib/drawing.mjs +263 -0
- package/src/lib/drawing.mjs.map +1 -0
- package/src/lib/kinds.js +9 -2
- package/src/lib/kinds.js.map +1 -1
- package/src/lib/kinds.mjs +9 -2
- package/src/lib/kinds.mjs.map +1 -1
- package/src/lib/labels.d.mts +32 -0
- package/src/lib/labels.d.ts +32 -0
- package/src/lib/labels.js +88 -1
- package/src/lib/labels.js.map +1 -1
- package/src/lib/labels.mjs +86 -0
- package/src/lib/labels.mjs.map +1 -1
- package/src/lib/list-view.d.mts +13 -0
- package/src/lib/list-view.d.ts +13 -0
- package/src/lib/list-view.js +481 -0
- package/src/lib/list-view.js.map +1 -0
- package/src/lib/list-view.mjs +478 -0
- package/src/lib/list-view.mjs.map +1 -0
- package/src/lib/map-core.d.mts +4 -2
- package/src/lib/map-core.d.ts +4 -2
- package/src/lib/map-core.js +19 -7
- package/src/lib/map-core.js.map +1 -1
- package/src/lib/map-core.mjs +18 -7
- package/src/lib/map-core.mjs.map +1 -1
- package/src/lib/map-icons.d.mts +8 -0
- package/src/lib/map-icons.d.ts +8 -0
- package/src/lib/map-icons.js +21 -0
- package/src/lib/map-icons.js.map +1 -0
- package/src/lib/map-icons.mjs +18 -0
- package/src/lib/map-icons.mjs.map +1 -0
- package/src/lib/pins.d.mts +56 -0
- package/src/lib/pins.d.ts +56 -0
- package/src/lib/pins.js +161 -0
- package/src/lib/pins.js.map +1 -0
- package/src/lib/pins.mjs +154 -0
- package/src/lib/pins.mjs.map +1 -0
- package/src/lib/place.d.mts +1 -1
- package/src/lib/place.d.ts +1 -1
- package/src/lib/place.js +46 -6
- package/src/lib/place.js.map +1 -1
- package/src/lib/place.mjs +48 -8
- package/src/lib/place.mjs.map +1 -1
- package/src/lib/providers.d.mts +29 -8
- package/src/lib/providers.d.ts +29 -8
- package/src/lib/providers.js +217 -33
- package/src/lib/providers.js.map +1 -1
- package/src/lib/providers.mjs +217 -33
- package/src/lib/providers.mjs.map +1 -1
- package/src/lib/search-box.d.mts +4 -2
- package/src/lib/search-box.d.ts +4 -2
- package/src/lib/search-box.js +20 -6
- package/src/lib/search-box.js.map +1 -1
- package/src/lib/search-box.mjs +20 -6
- package/src/lib/search-box.mjs.map +1 -1
- package/src/lib/settings.d.mts +5 -2
- package/src/lib/settings.d.ts +5 -2
- package/src/lib/styles.d.mts +9 -1
- package/src/lib/styles.d.ts +9 -1
- package/src/lib/styles.js +146 -2
- package/src/lib/styles.js.map +1 -1
- package/src/lib/styles.mjs +144 -1
- package/src/lib/styles.mjs.map +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @fieldia/map
|
|
2
2
|
|
|
3
3
|
[Fieldia](https://fieldia.dev)'s maps: a **place** and an **area** as fields of a
|
|
4
|
-
form, on the map provider you choose. Built
|
|
5
|
-
loaded only when a map is shown.
|
|
4
|
+
form, and a **list's records on a map**, on the map provider you choose. Built
|
|
5
|
+
on [Leaflet](https://leafletjs.com), loaded only when a map is shown.
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
npm install @fieldia/map
|
|
@@ -53,6 +53,57 @@ the whole area drags to move it, and a point between two adds one.
|
|
|
53
53
|
Options: `shapes` (any of polygon, box, circle), `areas` (named areas to choose
|
|
54
54
|
instead of drawing), `height`.
|
|
55
55
|
|
|
56
|
+
## A list on a map
|
|
57
|
+
|
|
58
|
+
A list page with `map` shows its records on a map too, beside the table or
|
|
59
|
+
instead of it, sharing its search and its records chosen: a pin lights its row
|
|
60
|
+
and a row its pin, a pin opens a card (a picture, a few fields, Choose, Open),
|
|
61
|
+
pins that crowd become a bubble with their count, and "Search this area" puts
|
|
62
|
+
the map's view into the search bar.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { mapListViews, azureMaps } from '@fieldia/map';
|
|
66
|
+
|
|
67
|
+
mountViewer(host, { page, dataSource, listViews: mapListViews({ provider: azureMaps({ key }) }) });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
"map": {
|
|
72
|
+
"latitude": "lat", "longitude": "lng",
|
|
73
|
+
"pin": { "label": "price", "tones": [{ "tone": "info", "when": "offer == 'rent'" }] },
|
|
74
|
+
"card": { "title": "name", "image": "photo", "fields": ["price", "bedrooms"] },
|
|
75
|
+
"view": "both"
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Where each record is: `point` (a json field holding a GeoJSON Point), or
|
|
80
|
+
`latitude` and `longitude`. A pin's `look` is `pin`, `label`, `icon`, `photo`
|
|
81
|
+
or `dot`; `label` names the field it says (an amount or a number written short,
|
|
82
|
+
"E£9.5M"), `icon` or `iconField` its icon (the map's own car, arrow, scooter,
|
|
83
|
+
bike, bus, person, shop, food and flag, Fieldia's, or the app's `icons`),
|
|
84
|
+
`image` its photo, `turn` a number field in degrees (a car seen from above
|
|
85
|
+
turns whole; other icons keep upright, a pointer on their edge turning), and
|
|
86
|
+
`tones` its colour by condition. `widget` names the app's own pin, given as
|
|
87
|
+
`mapListViews({ pins: { name: (record, { document }) => element } })`. `card`
|
|
88
|
+
takes `title`, `image`, `fields` and `open`; `cluster: false` draws every pin
|
|
89
|
+
on its own, though pins near one another then overlap and a pointer cannot
|
|
90
|
+
reach one under another (the keys still reach each); `view` is `list`, `map` or `both`; `limit` is 500 unless said.
|
|
91
|
+
|
|
92
|
+
The map's own tools draw an area to search in: a polygon, a box or a circle,
|
|
93
|
+
by mouse, finger or keys, as the area field draws one. It becomes a search of
|
|
94
|
+
its own ("In the area drawn"), shown on the map until it leaves the search bar.
|
|
95
|
+
|
|
96
|
+
"Search this area" asks for `between` on a latitude and a longitude, or the
|
|
97
|
+
`within` condition on a point: `{ south, west, north, east }` in degrees, west
|
|
98
|
+
beyond east across the antimeridian. An area drawn asks for `within` with
|
|
99
|
+
`{ polygon: [[lng, lat], …] }` or `{ center: [lng, lat], radius }` in metres;
|
|
100
|
+
on a latitude field, `longitude` names the field of its longitude. The memory
|
|
101
|
+
data source answers all of them; a server's adapter turns them into its own
|
|
102
|
+
query.
|
|
103
|
+
|
|
104
|
+
A form with only a place or an area never carries the list map's code, its
|
|
105
|
+
styles or its words.
|
|
106
|
+
|
|
56
107
|
## Without a mouse
|
|
57
108
|
|
|
58
109
|
Tab reaches the map, which shows a cross at its centre: the arrow keys move the
|
|
@@ -63,6 +114,10 @@ arrow keys and leaves with Delete. A scroll passes over a map to the page
|
|
|
63
114
|
unless Ctrl (⌘) is held, and on a phone one finger scrolls the page while two
|
|
64
115
|
move the map.
|
|
65
116
|
|
|
117
|
+
On a list's map, Tab goes from the map to each pin and bubble by its name;
|
|
118
|
+
Enter or Space opens a pin's card (Ctrl or ⌘ with it chooses the record) or
|
|
119
|
+
zooms into a bubble, and Escape closes the card, back on its pin.
|
|
120
|
+
|
|
66
121
|
## Providers
|
|
67
122
|
|
|
68
123
|
| Provider | Road | Dark | Satellite | Address search |
|
|
@@ -71,15 +126,22 @@ move the map.
|
|
|
71
126
|
| `googleMaps({ key, region, logo })` (Map Tiles API) | yes | no | yes | the app's own (`search`) |
|
|
72
127
|
| `mapbox({ token })` | yes | yes | yes | yes, as people type |
|
|
73
128
|
| `tiles({ url, credit })` | the app's own map images | | | the app's own |
|
|
74
|
-
| `openStreetMap()` | yes, **for demos only** | no | no |
|
|
129
|
+
| `openStreetMap()` | yes, **for demos only** | no | no | Photon, as people type, nearest first (or `{ search: 'nominatim' }`, on Enter) |
|
|
75
130
|
|
|
76
131
|
Keys never live in Fieldia: pass one restricted to your sites, or a function
|
|
77
132
|
fetching a short-lived token from your server. Each provider's credits are shown
|
|
78
133
|
in the map's corner, and its mark where its terms ask. Read your provider's
|
|
79
134
|
terms on keeping search results: a place field keeps what it finds (Mapbox's
|
|
80
|
-
permanent geocoding is asked for by default). OpenStreetMap's public map images
|
|
81
|
-
and Nominatim are for trying things out: their policies
|
|
82
|
-
heavy use.
|
|
135
|
+
permanent geocoding is asked for by default). OpenStreetMap's public map images,
|
|
136
|
+
Photon's public server and Nominatim are for trying things out: their policies
|
|
137
|
+
treat an app in use as heavy use. Photon can run on your own server
|
|
138
|
+
(`openStreetMap({ photonUrl })`).
|
|
139
|
+
|
|
140
|
+
Suggestions come as people type where the provider allows it, nearest where the
|
|
141
|
+
map is looking first, each on two lines (the place, then where it is); Enter
|
|
142
|
+
takes the first. "My location" says how sure it is: a rough location (a
|
|
143
|
+
desktop's, placed by its network) shows the circle it may be anywhere in, says
|
|
144
|
+
how wide, and names no address from it.
|
|
83
145
|
|
|
84
146
|
Pass your own `search` (and `reverse`) to `mapWidgets` to use your own address
|
|
85
147
|
search with any provider.
|
|
@@ -87,6 +149,11 @@ search with any provider.
|
|
|
87
149
|
## In the designer
|
|
88
150
|
|
|
89
151
|
`mapKinds({ locale })` gives the designer Place and Area for its toolbox, with
|
|
90
|
-
their settings
|
|
152
|
+
their settings (a place's address search, "My location", the field its address
|
|
153
|
+
goes into, the map's height; an area's shapes); pass the map widgets to the
|
|
154
|
+
designer and its editors too. A list page's panel has its Map: where each
|
|
155
|
+
record is, its pins, its card, and how the list opens. Give the screen editor
|
|
156
|
+
`listViews` (and `sampleCentre`, where its made-up records are) and Try it shows
|
|
157
|
+
the map too.
|
|
91
158
|
|
|
92
159
|
MIT. Leaflet is BSD-2-Clause; its stylesheet is included with its licence.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fieldia/map",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Fieldia's maps: a place and an area as fields, on any map provider. Leaflet, loaded only where a map is shown.",
|
|
3
|
+
"version": "0.15.0",
|
|
4
|
+
"description": "Fieldia's maps: a place and an area as fields, and a list's records on a map, on any map provider. Leaflet, loaded only where a map is shown.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://fieldia.dev",
|
|
7
7
|
"type": "commonjs",
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
"./package.json": "./package.json"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@fieldia/core": "0.
|
|
25
|
-
"@fieldia/
|
|
24
|
+
"@fieldia/core": "0.15.0",
|
|
25
|
+
"@fieldia/viewer": "0.15.0",
|
|
26
|
+
"@fieldia/widgets": "0.15.0",
|
|
26
27
|
"leaflet": "^1.9.4",
|
|
27
28
|
"tslib": "^2.8.1"
|
|
28
29
|
},
|
package/src/index.d.mts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export { mapWidgets } from './lib/widgets.mjs';
|
|
2
|
+
export { mapListViews } from './lib/list-view.mjs';
|
|
3
|
+
export { MAP_ICONS } from './lib/map-icons.mjs';
|
|
4
|
+
export type { MapRecord, PinDrawer } from './lib/pins.mjs';
|
|
2
5
|
export { mapKinds, type MapKind, type MapKindContext } from './lib/kinds.mjs';
|
|
3
6
|
export type { MapSettings } from './lib/settings.mjs';
|
|
4
7
|
export { azureMaps, googleMaps, mapbox, openStreetMap, tiles, type AddressSearch, type AzureMapsOptions, type BaseMap, type Credit, type GoogleMapsOptions, type MapboxOptions, type MapProvider, type MapView, type OpenStreetMapOptions, type PlaceFound, type ReverseSearch, type SearchRequest, type TileSpec, type TilesOptions, } from './lib/providers.mjs';
|
package/src/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export { mapWidgets } from './lib/widgets';
|
|
2
|
+
export { mapListViews } from './lib/list-view';
|
|
3
|
+
export { MAP_ICONS } from './lib/map-icons';
|
|
4
|
+
export type { MapRecord, PinDrawer } from './lib/pins';
|
|
2
5
|
export { mapKinds, type MapKind, type MapKindContext } from './lib/kinds';
|
|
3
6
|
export type { MapSettings } from './lib/settings';
|
|
4
7
|
export { azureMaps, googleMaps, mapbox, openStreetMap, tiles, type AddressSearch, type AzureMapsOptions, type BaseMap, type Credit, type GoogleMapsOptions, type MapboxOptions, type MapProvider, type MapView, type OpenStreetMapOptions, type PlaceFound, type ReverseSearch, type SearchRequest, type TileSpec, type TilesOptions, } from './lib/providers';
|
package/src/index.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MAP_LABELS = exports.metresBetween = exports.writePoint = exports.writeArea = exports.readPoint = exports.readArea = exports.tiles = exports.openStreetMap = exports.mapbox = exports.googleMaps = exports.azureMaps = exports.mapKinds = exports.mapWidgets = void 0;
|
|
3
|
+
exports.MAP_LABELS = exports.metresBetween = exports.writePoint = exports.writeArea = exports.readPoint = exports.readArea = exports.tiles = exports.openStreetMap = exports.mapbox = exports.googleMaps = exports.azureMaps = exports.mapKinds = exports.MAP_ICONS = exports.mapListViews = exports.mapWidgets = void 0;
|
|
4
4
|
var widgets_1 = require("./lib/widgets");
|
|
5
5
|
Object.defineProperty(exports, "mapWidgets", { enumerable: true, get: function () { return widgets_1.mapWidgets; } });
|
|
6
|
+
var list_view_1 = require("./lib/list-view");
|
|
7
|
+
Object.defineProperty(exports, "mapListViews", { enumerable: true, get: function () { return list_view_1.mapListViews; } });
|
|
8
|
+
var map_icons_1 = require("./lib/map-icons");
|
|
9
|
+
Object.defineProperty(exports, "MAP_ICONS", { enumerable: true, get: function () { return map_icons_1.MAP_ICONS; } });
|
|
6
10
|
var kinds_1 = require("./lib/kinds");
|
|
7
11
|
Object.defineProperty(exports, "mapKinds", { enumerable: true, get: function () { return kinds_1.mapKinds; } });
|
|
8
12
|
var providers_1 = require("./lib/providers");
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/map/src/index.ts"],"names":[],"mappings":";;;AAAA,yCAA2C;AAAlC,qGAAA,UAAU,OAAA;AACnB,qCAA0E;AAAjE,iGAAA,QAAQ,OAAA;AAEjB,6CAoByB;AAnBvB,sGAAA,SAAS,OAAA;AACT,uGAAA,UAAU,OAAA;AACV,mGAAA,MAAM,OAAA;AACN,0GAAA,aAAa,OAAA;AACb,kGAAA,KAAK,OAAA;AAgBP,iCAAgK;AAAvJ,+FAAA,QAAQ,OAAA;AAAE,gGAAA,SAAS,OAAA;AAAE,gGAAA,SAAS,OAAA;AAAE,iGAAA,UAAU,OAAA;AAAE,oGAAA,aAAa,OAAA;AAClE,uCAA0D;AAAjD,oGAAA,UAAU,OAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/map/src/index.ts"],"names":[],"mappings":";;;AAAA,yCAA2C;AAAlC,qGAAA,UAAU,OAAA;AACnB,6CAA+C;AAAtC,yGAAA,YAAY,OAAA;AACrB,6CAA4C;AAAnC,sGAAA,SAAS,OAAA;AAElB,qCAA0E;AAAjE,iGAAA,QAAQ,OAAA;AAEjB,6CAoByB;AAnBvB,sGAAA,SAAS,OAAA;AACT,uGAAA,UAAU,OAAA;AACV,mGAAA,MAAM,OAAA;AACN,0GAAA,aAAa,OAAA;AACb,kGAAA,KAAK,OAAA;AAgBP,iCAAgK;AAAvJ,+FAAA,QAAQ,OAAA;AAAE,gGAAA,SAAS,OAAA;AAAE,gGAAA,SAAS,OAAA;AAAE,iGAAA,UAAU,OAAA;AAAE,oGAAA,aAAa,OAAA;AAClE,uCAA0D;AAAjD,oGAAA,UAAU,OAAA"}
|
package/src/index.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { mapWidgets } from './lib/widgets.mjs';
|
|
2
|
+
export { mapListViews } from './lib/list-view.mjs';
|
|
3
|
+
export { MAP_ICONS } from './lib/map-icons.mjs';
|
|
2
4
|
export { mapKinds } from './lib/kinds.mjs';
|
|
3
5
|
export { azureMaps, googleMaps, mapbox, openStreetMap, tiles, } from './lib/providers.mjs';
|
|
4
6
|
export { readArea, readPoint, writeArea, writePoint, metresBetween } from './lib/geo.mjs';
|
package/src/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sourceRoot":"","sources":["../../../../libs/map/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAqC,MAAM,aAAa,CAAC;AAE1E,OAAO,EACL,SAAS,EACT,UAAU,EACV,MAAM,EACN,aAAa,EACb,KAAK,GAeN,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAA4E,MAAM,WAAW,CAAC;AAChK,OAAO,EAAE,UAAU,EAAkB,MAAM,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.mjs","sourceRoot":"","sources":["../../../../libs/map/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,EAAE,QAAQ,EAAqC,MAAM,aAAa,CAAC;AAE1E,OAAO,EACL,SAAS,EACT,UAAU,EACV,MAAM,EACN,aAAa,EACb,KAAK,GAeN,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAA4E,MAAM,WAAW,CAAC;AAChK,OAAO,EAAE,UAAU,EAAkB,MAAM,cAAc,CAAC"}
|
package/src/lib/area.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.areaWidget = areaWidget;
|
|
4
|
+
const drawing_1 = require("./drawing");
|
|
4
5
|
const geo_1 = require("./geo");
|
|
5
6
|
const labels_1 = require("./labels");
|
|
6
7
|
const map_core_1 = require("./map-core");
|
|
@@ -22,15 +23,6 @@ const map_core_1 = require("./map-core");
|
|
|
22
23
|
* `height`.
|
|
23
24
|
*/
|
|
24
25
|
const ALL = ['polygon', 'box', 'circle'];
|
|
25
|
-
const ICONS = {
|
|
26
|
-
polygon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M5 8l7-5 7 6-3 11H7z"/></svg>',
|
|
27
|
-
box: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="4" y="6" width="16" height="12" rx="1"/></svg>',
|
|
28
|
-
circle: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="8"/></svg>',
|
|
29
|
-
};
|
|
30
|
-
/** How near the first point a press closes the ring: a fingertip's width, not the mark's. */
|
|
31
|
-
const CLOSE_PX = 22;
|
|
32
|
-
/** A press that moves less than this is a press, not a drag. */
|
|
33
|
-
const DRAG_PX = 4;
|
|
34
26
|
const same = (a, b) => JSON.stringify(a && (0, geo_1.writeArea)(a)) === JSON.stringify(b && (0, geo_1.writeArea)(b));
|
|
35
27
|
function areaWidget(settings) {
|
|
36
28
|
return (context) => {
|
|
@@ -47,11 +39,6 @@ function areaWidget(settings) {
|
|
|
47
39
|
: [];
|
|
48
40
|
let readonly = false;
|
|
49
41
|
let area = null;
|
|
50
|
-
let mode = null;
|
|
51
|
-
/** A polygon's points so far. */
|
|
52
|
-
let points = [];
|
|
53
|
-
/** Where a box or a circle began, by pointer or by Space. */
|
|
54
|
-
let anchor = null;
|
|
55
42
|
const element = doc.createElement('div');
|
|
56
43
|
element.className = 'fd-map-field fd-area';
|
|
57
44
|
// The field's label names the group, so each tool keeps its own name ("Polygon", not "Area").
|
|
@@ -85,9 +72,9 @@ function areaWidget(settings) {
|
|
|
85
72
|
button.type = 'button';
|
|
86
73
|
button.className = 'fd-button';
|
|
87
74
|
button.setAttribute('aria-pressed', 'false');
|
|
88
|
-
button.innerHTML =
|
|
75
|
+
button.innerHTML = drawing_1.SHAPE_ICONS[shape];
|
|
89
76
|
button.append(labels[shape]);
|
|
90
|
-
button.addEventListener('click', () => arm(mode === shape ? null : shape));
|
|
77
|
+
button.addEventListener('click', () => drawing.arm(drawing.mode === shape ? null : shape));
|
|
91
78
|
toolButtons.set(shape, button);
|
|
92
79
|
tools.append(button);
|
|
93
80
|
});
|
|
@@ -111,7 +98,7 @@ function areaWidget(settings) {
|
|
|
111
98
|
const chosen = choice ? (0, geo_1.readArea)(choice.value) : null;
|
|
112
99
|
if (!chosen)
|
|
113
100
|
return;
|
|
114
|
-
arm(null);
|
|
101
|
+
drawing.arm(null);
|
|
115
102
|
commit(chosen, true);
|
|
116
103
|
});
|
|
117
104
|
tools.append(chooser);
|
|
@@ -122,31 +109,30 @@ function areaWidget(settings) {
|
|
|
122
109
|
clear.textContent = labels.clear;
|
|
123
110
|
clear.hidden = true;
|
|
124
111
|
clear.addEventListener('click', () => {
|
|
125
|
-
arm(null);
|
|
112
|
+
drawing.arm(null);
|
|
126
113
|
commit(null);
|
|
127
114
|
announce(labels.shapeCleared);
|
|
128
115
|
(toolButtons.get(shapes[0]) ?? box.element).focus();
|
|
129
116
|
});
|
|
130
117
|
tools.append(clear);
|
|
131
|
-
//
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
box.follow(hint);
|
|
118
|
+
// Drawing a new shape: the tools' buttons say which is in hand; the shape drawn becomes the value.
|
|
119
|
+
const drawing = (0, drawing_1.shapeDrawing)({
|
|
120
|
+
box,
|
|
121
|
+
labels,
|
|
122
|
+
keys: element,
|
|
123
|
+
announce,
|
|
124
|
+
armed: (mode) => {
|
|
125
|
+
for (const [shape, button] of toolButtons)
|
|
126
|
+
button.setAttribute('aria-pressed', String(shape === mode));
|
|
127
|
+
draw();
|
|
128
|
+
},
|
|
129
|
+
done: (drawn) => {
|
|
130
|
+
commit(drawn);
|
|
131
|
+
announce(labels.shapeDrawn);
|
|
132
|
+
},
|
|
133
|
+
escaped: () => (toolButtons.get(shapes[0]) ?? box.element).focus(),
|
|
134
|
+
finishesOn: (target) => target === toolButtons.get('polygon'),
|
|
135
|
+
});
|
|
150
136
|
element.append(tools, box.element, live);
|
|
151
137
|
// ---- the value -------------------------------------------------------------
|
|
152
138
|
function commit(next, fit = false) {
|
|
@@ -159,12 +145,9 @@ function areaWidget(settings) {
|
|
|
159
145
|
let map = null;
|
|
160
146
|
let L = null;
|
|
161
147
|
let shapeLayer = null;
|
|
162
|
-
let trace = null;
|
|
163
148
|
const grips = [];
|
|
164
149
|
/** The grip under the hand: the others follow it, it follows the hand. */
|
|
165
150
|
let held = null;
|
|
166
|
-
/** A polygon's points while it is drawn, and the line reaching for the pointer. */
|
|
167
|
-
const marks = [];
|
|
168
151
|
function clearLayers() {
|
|
169
152
|
shapeLayer?.remove();
|
|
170
153
|
shapeLayer = null;
|
|
@@ -173,15 +156,15 @@ function areaWidget(settings) {
|
|
|
173
156
|
}
|
|
174
157
|
function layerFor(shape, className) {
|
|
175
158
|
return shape.shape === 'circle'
|
|
176
|
-
? L.circle([shape.center.lat, shape.center.lng], { radius: shape.radius, className, interactive: !readonly && !mode })
|
|
177
|
-
: L.polygon(shape.points.map((p) => [p.lat, p.lng]), { className, interactive: !readonly && !mode });
|
|
159
|
+
? L.circle([shape.center.lat, shape.center.lng], { radius: shape.radius, className, interactive: !readonly && !drawing.mode })
|
|
160
|
+
: L.polygon(shape.points.map((p) => [p.lat, p.lng]), { className, interactive: !readonly && !drawing.mode });
|
|
178
161
|
}
|
|
179
162
|
/** The shape as it now is, and its grips; `fit` brings it into view. */
|
|
180
163
|
function draw(fit = false) {
|
|
181
164
|
if (!map || !L)
|
|
182
165
|
return;
|
|
183
166
|
clearLayers();
|
|
184
|
-
if (!area || mode)
|
|
167
|
+
if (!area || drawing.mode)
|
|
185
168
|
return;
|
|
186
169
|
shapeLayer = layerFor(area, `fd-map-shape${readonly ? '' : ' fd-map-movable'}`).addTo(map);
|
|
187
170
|
if (!readonly) {
|
|
@@ -327,7 +310,7 @@ function areaWidget(settings) {
|
|
|
327
310
|
if (!el)
|
|
328
311
|
return;
|
|
329
312
|
el.addEventListener('pointerdown', (event) => {
|
|
330
|
-
if (!map || !area || readonly || mode || event.button !== 0)
|
|
313
|
+
if (!map || !area || readonly || drawing.mode || event.button !== 0)
|
|
331
314
|
return;
|
|
332
315
|
event.stopPropagation();
|
|
333
316
|
const start = map.mouseEventToLatLng(event);
|
|
@@ -356,210 +339,10 @@ function areaWidget(settings) {
|
|
|
356
339
|
});
|
|
357
340
|
}
|
|
358
341
|
// ---- the tools ---------------------------------------------------------------
|
|
359
|
-
function sayStep() {
|
|
360
|
-
if (!mode)
|
|
361
|
-
return;
|
|
362
|
-
const words = mode === 'polygon' ? labels.drawPolygon : mode === 'box' ? labels.drawBox : labels.drawCircle;
|
|
363
|
-
hintWords.textContent = mode === 'polygon' && points.length ? `${words} ${(0, labels_1.fill)(labels.points, { n: points.length })}.` : words;
|
|
364
|
-
finishButton.hidden = mode !== 'polygon' || points.length < 3;
|
|
365
|
-
}
|
|
366
|
-
function arm(next) {
|
|
367
|
-
if (readonly)
|
|
368
|
-
next = null;
|
|
369
|
-
mode = next;
|
|
370
|
-
points = [];
|
|
371
|
-
anchor = null;
|
|
372
|
-
trace?.remove();
|
|
373
|
-
trace = null;
|
|
374
|
-
remark();
|
|
375
|
-
for (const [shape, button] of toolButtons)
|
|
376
|
-
button.setAttribute('aria-pressed', String(shape === mode));
|
|
377
|
-
hint.hidden = !mode;
|
|
378
|
-
// Every tool holds the ground still: a press is a point, never a pan.
|
|
379
|
-
box.setPannable(!mode);
|
|
380
|
-
box.ready((m) => (mode ? m.doubleClickZoom.disable() : m.doubleClickZoom.enable()));
|
|
381
|
-
box.element.dataset['drawing'] = mode ?? '';
|
|
382
|
-
if (!mode)
|
|
383
|
-
delete box.element.dataset['drawing'];
|
|
384
|
-
sayStep();
|
|
385
|
-
draw();
|
|
386
|
-
if (mode)
|
|
387
|
-
announce(hintWords.textContent ?? '');
|
|
388
|
-
}
|
|
389
|
-
/** The marks of a polygon's points: made again only when the points change, never as the pointer moves. */
|
|
390
|
-
function remark() {
|
|
391
|
-
for (const m of marks.splice(0))
|
|
392
|
-
m.remove();
|
|
393
|
-
if (!map || !L || mode !== 'polygon')
|
|
394
|
-
return;
|
|
395
|
-
points.forEach((p, i) => {
|
|
396
|
-
const mark = L.marker([p.lat, p.lng], { icon: gripIcon(), interactive: i === 0, keyboard: false }).addTo(map);
|
|
397
|
-
// Back on the first point closes the ring.
|
|
398
|
-
if (i === 0)
|
|
399
|
-
mark.on('click', () => points.length >= 3 && finish());
|
|
400
|
-
marks.push(mark);
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
/** The outline being drawn, toward the pointer: changed in place as it moves. */
|
|
404
|
-
function retrace(toward) {
|
|
405
|
-
if (!map || !L)
|
|
406
|
-
return;
|
|
407
|
-
let ring = null;
|
|
408
|
-
let circle = null;
|
|
409
|
-
if (mode === 'polygon') {
|
|
410
|
-
ring = [...points, ...(toward ? [toward] : [])];
|
|
411
|
-
if (ring.length < 2)
|
|
412
|
-
ring = null;
|
|
413
|
-
marks[0]?.getElement()?.toggleAttribute('data-closes', nearFirst(toward));
|
|
414
|
-
}
|
|
415
|
-
else if (anchor && toward) {
|
|
416
|
-
if (mode === 'box')
|
|
417
|
-
ring = (0, geo_1.boxFrom)(anchor, toward);
|
|
418
|
-
else
|
|
419
|
-
circle = { center: anchor, radius: (0, geo_1.metresBetween)(anchor, toward) };
|
|
420
|
-
}
|
|
421
|
-
const latLngs = ring?.map((p) => [p.lat, p.lng]);
|
|
422
|
-
if (trace && latLngs && trace instanceof L.Polygon && !(trace instanceof L.Rectangle)) {
|
|
423
|
-
trace.setLatLngs(latLngs);
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
if (trace && circle && trace instanceof L.Circle) {
|
|
427
|
-
trace.setLatLng([circle.center.lat, circle.center.lng]);
|
|
428
|
-
trace.setRadius(circle.radius);
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
trace?.remove();
|
|
432
|
-
trace = null;
|
|
433
|
-
if (latLngs)
|
|
434
|
-
trace = L.polygon(latLngs, { className: 'fd-map-trace', interactive: false }).addTo(map);
|
|
435
|
-
else if (circle)
|
|
436
|
-
trace = L.circle([circle.center.lat, circle.center.lng], { radius: circle.radius, className: 'fd-map-trace', interactive: false }).addTo(map);
|
|
437
|
-
}
|
|
438
|
-
function nearFirst(at) {
|
|
439
|
-
if (!map || !at || points.length < 3)
|
|
440
|
-
return false;
|
|
441
|
-
return map.latLngToContainerPoint([points[0].lat, points[0].lng]).distanceTo(map.latLngToContainerPoint([at.lat, at.lng])) <= CLOSE_PX;
|
|
442
|
-
}
|
|
443
|
-
function addPoint(at) {
|
|
444
|
-
if (mode === 'polygon') {
|
|
445
|
-
if (nearFirst(at))
|
|
446
|
-
return finish();
|
|
447
|
-
points.push(at);
|
|
448
|
-
sayStep();
|
|
449
|
-
remark();
|
|
450
|
-
retrace();
|
|
451
|
-
announce((0, labels_1.fill)(labels.points, { n: points.length }));
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
if (!anchor) {
|
|
455
|
-
anchor = at;
|
|
456
|
-
retrace(at);
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
const end = at;
|
|
460
|
-
const start = anchor;
|
|
461
|
-
if (mode === 'box')
|
|
462
|
-
complete({ shape: 'box', points: (0, geo_1.boxFrom)(start, end) });
|
|
463
|
-
else if (mode === 'circle')
|
|
464
|
-
complete({ shape: 'circle', center: start, radius: Math.max(1, (0, geo_1.metresBetween)(start, end)) });
|
|
465
|
-
}
|
|
466
|
-
function finish() {
|
|
467
|
-
if (mode !== 'polygon' || points.length < 3)
|
|
468
|
-
return;
|
|
469
|
-
complete({ shape: 'polygon', points: [...points] });
|
|
470
|
-
}
|
|
471
|
-
function complete(drawn) {
|
|
472
|
-
mode = null;
|
|
473
|
-
arm(null);
|
|
474
|
-
commit(drawn);
|
|
475
|
-
announce(labels.shapeDrawn);
|
|
476
|
-
}
|
|
477
342
|
box.ready((m, leaflet) => {
|
|
478
343
|
map = m;
|
|
479
344
|
L = leaflet;
|
|
480
345
|
draw(!!area);
|
|
481
|
-
m.on('click', (event) => {
|
|
482
|
-
if (mode !== 'polygon' || readonly)
|
|
483
|
-
return;
|
|
484
|
-
addPoint({ lat: event.latlng.lat, lng: event.latlng.lng });
|
|
485
|
-
});
|
|
486
|
-
m.on('dblclick', () => {
|
|
487
|
-
if (mode !== 'polygon')
|
|
488
|
-
return;
|
|
489
|
-
// The two clicks of a double-click have each placed a point, on top of each other: one is enough.
|
|
490
|
-
const [a, b] = points.slice(-2);
|
|
491
|
-
if (a && b && m.latLngToContainerPoint([a.lat, a.lng]).distanceTo(m.latLngToContainerPoint([b.lat, b.lng])) < 6) {
|
|
492
|
-
points.pop();
|
|
493
|
-
remark();
|
|
494
|
-
}
|
|
495
|
-
finish();
|
|
496
|
-
});
|
|
497
|
-
m.on('mousemove', (event) => {
|
|
498
|
-
if (mode === 'polygon' && points.length)
|
|
499
|
-
retrace({ lat: event.latlng.lat, lng: event.latlng.lng });
|
|
500
|
-
});
|
|
501
|
-
// A box and a circle are drawn by dragging: pointer events, which a mouse, a finger and a pen all raise.
|
|
502
|
-
let pressed = null;
|
|
503
|
-
box.element.addEventListener('pointerdown', (event) => {
|
|
504
|
-
if ((mode !== 'box' && mode !== 'circle') || readonly || event.button !== 0)
|
|
505
|
-
return;
|
|
506
|
-
if (event.target.closest('.fd-map-hint, .fd-map-zoom, .fd-map-credits, .fd-map-logo'))
|
|
507
|
-
return;
|
|
508
|
-
const at = m.mouseEventToLatLng(event);
|
|
509
|
-
pressed = { x: event.clientX, y: event.clientY, at: { lat: at.lat, lng: at.lng }, id: event.pointerId };
|
|
510
|
-
anchor = pressed.at;
|
|
511
|
-
box.element.setPointerCapture?.(event.pointerId);
|
|
512
|
-
});
|
|
513
|
-
box.element.addEventListener('pointermove', (event) => {
|
|
514
|
-
if (!pressed || event.pointerId !== pressed.id)
|
|
515
|
-
return;
|
|
516
|
-
const at = m.mouseEventToLatLng(event);
|
|
517
|
-
retrace({ lat: at.lat, lng: at.lng });
|
|
518
|
-
});
|
|
519
|
-
const release = (event) => {
|
|
520
|
-
if (!pressed || event.pointerId !== pressed.id)
|
|
521
|
-
return;
|
|
522
|
-
const moved = Math.hypot(event.clientX - pressed.x, event.clientY - pressed.y);
|
|
523
|
-
const at = m.mouseEventToLatLng(event);
|
|
524
|
-
pressed = null;
|
|
525
|
-
// A press with no drag is no shape: the tool stays in hand.
|
|
526
|
-
if (event.type === 'pointercancel' || moved < DRAG_PX) {
|
|
527
|
-
anchor = null;
|
|
528
|
-
retrace();
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
addPoint({ lat: at.lat, lng: at.lng });
|
|
532
|
-
};
|
|
533
|
-
box.element.addEventListener('pointerup', release);
|
|
534
|
-
box.element.addEventListener('pointercancel', release);
|
|
535
|
-
});
|
|
536
|
-
// The keys of drawing, wherever in the field the focus is.
|
|
537
|
-
element.addEventListener('keydown', (event) => {
|
|
538
|
-
if (!mode)
|
|
539
|
-
return;
|
|
540
|
-
const onMap = event.target === box.element;
|
|
541
|
-
if (event.key === 'Escape') {
|
|
542
|
-
event.preventDefault();
|
|
543
|
-
arm(null);
|
|
544
|
-
(toolButtons.get(shapes[0]) ?? box.element).focus();
|
|
545
|
-
}
|
|
546
|
-
else if (event.key === 'Enter' && mode === 'polygon' && points.length >= 3 && (onMap || event.target === finishButton || toolButtons.get('polygon') === event.target)) {
|
|
547
|
-
event.preventDefault();
|
|
548
|
-
finish();
|
|
549
|
-
}
|
|
550
|
-
else if (event.key === 'Backspace' && mode === 'polygon' && points.length && onMap) {
|
|
551
|
-
event.preventDefault();
|
|
552
|
-
points.pop();
|
|
553
|
-
sayStep();
|
|
554
|
-
remark();
|
|
555
|
-
retrace();
|
|
556
|
-
announce((0, labels_1.fill)(labels.points, { n: points.length }));
|
|
557
|
-
}
|
|
558
|
-
else if (event.key === ' ' && onMap && map) {
|
|
559
|
-
event.preventDefault();
|
|
560
|
-
const c = map.getCenter();
|
|
561
|
-
addPoint({ lat: c.lat, lng: c.lng });
|
|
562
|
-
}
|
|
563
346
|
});
|
|
564
347
|
return {
|
|
565
348
|
element,
|
|
@@ -569,8 +352,8 @@ function areaWidget(settings) {
|
|
|
569
352
|
const wasReadonly = readonly;
|
|
570
353
|
readonly = state.readonly;
|
|
571
354
|
tools.hidden = readonly;
|
|
572
|
-
if (readonly && mode)
|
|
573
|
-
arm(null);
|
|
355
|
+
if (readonly && drawing.mode)
|
|
356
|
+
drawing.arm(null);
|
|
574
357
|
const next = (0, geo_1.readArea)(state.value);
|
|
575
358
|
const outside = !same(next, area);
|
|
576
359
|
if (outside)
|