@superrouteca/smap-arcgis 0.0.0-stage → 1.1.1
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/CHANGELOG.md +27 -0
- package/LICENSE +21 -0
- package/README.md +131 -2
- package/package.json +66 -4
- package/src/basemap.js +180 -0
- package/src/graphics.js +169 -0
- package/src/index.js +7 -0
- package/src/services.js +149 -0
- package/src/session.js +198 -0
- package/src/style.js +251 -0
- package/types/index.d.ts +125 -0
- package/types/services.d.ts +46 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.1.1 — 2026-10-27
|
|
4
|
+
|
|
5
|
+
- The vector basemap draws: water, land, roads, labels and icons appear in every colour scheme and label language
|
|
6
|
+
(with 1.1.0, `createSMapBasemap()` and `createVectorTileLayer()` loaded without an error and drew nothing). The style
|
|
7
|
+
is converted into what ArcGIS's vector tile layer supports before it is handed over; sMap's own style is unchanged.
|
|
8
|
+
- What the conversion simplifies, compared with the sMap web map: a label shows the name in the label language and,
|
|
9
|
+
for a name in another script, the local name on a second line, in one font (the per-script fonts are not used);
|
|
10
|
+
labels that sMap places at one of several positions keep the first position; layer properties ArcGIS does not
|
|
11
|
+
have are left out, and 3D buildings are drawn flat. `layer.smap.simplified` lists every simplification of the
|
|
12
|
+
style shown.
|
|
13
|
+
- `toArcGISStyle()` converts any style the same way (for a `VectorTileLayer` of your own); `convertStyle: false`
|
|
14
|
+
hands sMap's style to ArcGIS unchanged.
|
|
15
|
+
- Chinese, Japanese and Korean labels show in the vector basemap: sMap 1.1.1 serves them in its fonts. Chinese
|
|
16
|
+
characters take the Simplified Chinese form in every language.
|
|
17
|
+
|
|
18
|
+
## 1.1.0 — 2026-10-27
|
|
19
|
+
|
|
20
|
+
- First version.
|
|
21
|
+
- sMap as an ArcGIS basemap: vector tiles in sMap's style (`createSMapBasemap`, `createVectorTileLayer`; the five
|
|
22
|
+
colour schemes, custom styles and labels in any language, restyled in place) or sMap's raster tiles
|
|
23
|
+
(`createRasterTileLayer`). The map key or token on every request, the nearest map node and the next one when a node
|
|
24
|
+
fails (`installInterceptor`); the sMap badge and the OpenStreetMap credit (`addAttribution`).
|
|
25
|
+
- sMap's points, pins, lines and areas as ArcGIS graphics with the SDK's options and popups (`createGraphicsLayer`).
|
|
26
|
+
- ArcGIS REST services on an sMap map (`addArcGisService`): tile and vector tile services, and feature layers read as
|
|
27
|
+
GeoJSON and coloured by their renderer.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Goodneo Solutions
|
|
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
CHANGED
|
@@ -1,3 +1,132 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @superrouteca/smap-arcgis
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
sMap and the [ArcGIS Maps SDK for JavaScript](https://developers.arcgis.com/javascript/) both ways:
|
|
4
|
+
|
|
5
|
+
- **sMap as an ArcGIS basemap**: a `VectorTileLayer` with sMap's style and vector tiles (the five colour schemes and
|
|
6
|
+
your saved custom styles, labels in any language), or a `WebTileLayer` of sMap's raster tiles. The map key or token
|
|
7
|
+
goes on every request through ArcGIS request interceptors; requests go to the nearest map node and move to the next
|
|
8
|
+
one when a node fails; the sMap badge and the OpenStreetMap credit are shown.
|
|
9
|
+
- **sMap's points, pins, lines and areas as ArcGIS graphics**, with the options of the SDK's `addPoints` / `addLines` /
|
|
10
|
+
`addPolygons` and popups as ArcGIS popups.
|
|
11
|
+
- **ArcGIS REST services on an sMap map**: cached map and image services and vector tile services through the map's
|
|
12
|
+
`addArcGisLayer`, and feature layers queried as GeoJSON, coloured by their own renderer.
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install @superrouteca/smap-arcgis @arcgis/core
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`@arcgis/core` 5 is a peer dependency (not needed for `@superrouteca/smap-arcgis/services`). The adapter depends on
|
|
19
|
+
`@superrouteca/smap` (1.1.0) for the nodes, the key and the token.
|
|
20
|
+
|
|
21
|
+
## Access
|
|
22
|
+
|
|
23
|
+
Every call takes the options of the sMap JS client: `apiKey` (a map key, `smk_…`, whose allowlist names your site),
|
|
24
|
+
or `token` / `getToken` (short-lived tokens from your server), plus `baseUrl` (default `https://tile.srsr.ca`),
|
|
25
|
+
`nodes` and `nearest`. Share one session between layers with `session: createSession({ … })`.
|
|
26
|
+
|
|
27
|
+
## sMap as the basemap
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
import Map from '@arcgis/core/Map.js';
|
|
31
|
+
import MapView from '@arcgis/core/views/MapView.js';
|
|
32
|
+
import { createSMapBasemap, addAttribution } from '@superrouteca/smap-arcgis';
|
|
33
|
+
|
|
34
|
+
const basemap = await createSMapBasemap({ apiKey: 'smk_…', style: 'light', language: 'fr' });
|
|
35
|
+
const view = new MapView({ container: 'map', map: new Map({ basemap }), center: [2.35, 48.86], zoom: 12 });
|
|
36
|
+
addAttribution(view); // the sMap badge and the OpenStreetMap credit, bottom right
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
An existing ArcGIS app switches its basemap and keeps every layer: `map.basemap = await createSMapBasemap({ … })`.
|
|
40
|
+
|
|
41
|
+
- `style`: `light`, `white`, `grayscale`, `dark`, `black`, or a saved custom style `custom:sms_…`.
|
|
42
|
+
- `raster: true` uses sMap's raster tiles (a `WebTileLayer`, `retina: true` for 512 px images): the exact sMap look
|
|
43
|
+
wherever vector tiles are not wanted, and in a `SceneView` as draped imagery.
|
|
44
|
+
- `basemap: { pois: { visible: false }, roads: { color: '#ffffff' } }` shows, hides and recolours the basemap by
|
|
45
|
+
class (vector), as the sMap map's `setBasemap`.
|
|
46
|
+
- `basemap.smap.setStyle(style)` / `setLanguage(language)` / `setBasemap(overrides)` (vector) restyle the layer in
|
|
47
|
+
place.
|
|
48
|
+
|
|
49
|
+
The layers on their own: `createVectorTileLayer(options)` and `createRasterTileLayer(options)`.
|
|
50
|
+
|
|
51
|
+
### The vector style in ArcGIS
|
|
52
|
+
|
|
53
|
+
ArcGIS's `VectorTileLayer` supports part of the Style Specification, so the vector basemap converts sMap's style
|
|
54
|
+
before handing it over (`toArcGISStyle(style)` does the same for a `VectorTileLayer` of your own; `convertStyle: false`
|
|
55
|
+
hands the style over unchanged). Compared with the sMap web map:
|
|
56
|
+
|
|
57
|
+
- a label shows the name in the label language and, for a name in another script, the local name on a second line,
|
|
58
|
+
in one font;
|
|
59
|
+
- a label that sMap places at one of several positions keeps the first one;
|
|
60
|
+
- layer properties ArcGIS does not have are left out, and 3D buildings are drawn flat;
|
|
61
|
+
- Chinese, Japanese and Korean labels are drawn with sMap's own fonts (sMap 1.1.1 or later); Chinese characters take
|
|
62
|
+
the Simplified Chinese form in every language.
|
|
63
|
+
|
|
64
|
+
`basemap.smap.simplified` lists each simplification of the style shown (`"<layer>: <what>"`).
|
|
65
|
+
|
|
66
|
+
### Keys, tokens and nodes
|
|
67
|
+
|
|
68
|
+
`createSMapBasemap` (and the layer functions) install one request interceptor per session
|
|
69
|
+
(`installInterceptor(session)`, returning `{ remove() }`): every request to an sMap node is sent to the node read from
|
|
70
|
+
now, with `Authorization: Bearer <token>` and `X-SMap-Key` headers; a network error or a 5xx answer moves the session
|
|
71
|
+
to the next node, so the tiles that follow come from it. A map key also travels in the style's own tile URLs, so key
|
|
72
|
+
access works even where a request does not pass through the interceptors. Tokens are renewed in the background
|
|
73
|
+
before they expire.
|
|
74
|
+
|
|
75
|
+
ArcGIS draws the Style Specification styles it supports; sMap's schemes keep to what it reads. 3D buildings are not
|
|
76
|
+
drawn by ArcGIS from vector tiles: in a `SceneView` the sMap basemap is a flat, draped layer.
|
|
77
|
+
|
|
78
|
+
## sMap's layers as graphics
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
import { createGraphicsLayer } from '@superrouteca/smap-arcgis';
|
|
82
|
+
import { popupTemplate } from '@superrouteca/smap';
|
|
83
|
+
|
|
84
|
+
view.map.addMany([
|
|
85
|
+
createGraphicsLayer('zones', 'areas', zones, { fill: '#2563eb', opacity: 0.2, outline: '#2563eb' }),
|
|
86
|
+
createGraphicsLayer('route', 'lines', route, { color: { property: 'color' }, width: 5 }),
|
|
87
|
+
createGraphicsLayer('stops', 'points', stops, {
|
|
88
|
+
marker: { border: { property: 'color' } }, label: 'name',
|
|
89
|
+
popup: popupTemplate('<b>{name}</b><br>{address}'), popupTitle: 'Stop',
|
|
90
|
+
}),
|
|
91
|
+
]);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Points: circles, an icon, or pins (`pin` and `circle` shapes, fill, border); lines: colour per feature, width,
|
|
95
|
+
opacity, dashes; areas: fill, outline, opacity; `minzoom` / `maxzoom` become the layer's scale range; a `label` is a
|
|
96
|
+
text graphic beside each point. GeoJSON simplestyle properties colour a layer that gives no colour of its own. The
|
|
97
|
+
`popup` option is each graphic's popup content (a string, a DOM node or a promise of either). Numbers inside pins,
|
|
98
|
+
count badges, casings, arrows, clustering, extrusion and Auto Zoom are not drawn here; they are the sMap map's own.
|
|
99
|
+
|
|
100
|
+
## ArcGIS services on an sMap map
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
import * as sMap from '@superrouteca/smap';
|
|
104
|
+
import { addArcGisService } from '@superrouteca/smap-arcgis/services';
|
|
105
|
+
|
|
106
|
+
const map = new sMap.Map({ container: 'map', apiKey: 'smk_…' });
|
|
107
|
+
await map.ready;
|
|
108
|
+
await addArcGisService(map, 'hillshade', 'https://services.arcgisonline.com/arcgis/rest/services/Elevation/World_Hillshade/MapServer', { slot: 'bottom' });
|
|
109
|
+
await addArcGisService(map, 'parcels', 'https://example.com/arcgis/rest/services/Parcels/FeatureServer/0', {
|
|
110
|
+
token: arcgisToken, where: "zoning = 'R1'", layerOptions: { popup: sMap.popupTemplate('{address}') },
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- Cached `MapServer` / `ImageServer` and `VectorTileServer` services go through the map's
|
|
115
|
+
`addArcGisLayer(id, { url, attribution, slot, ...layerOptions })`, which reads the service (its tile size, its
|
|
116
|
+
vector tile style, and its copyright text as the default attribution).
|
|
117
|
+
- One layer of a feature or map service (`…/FeatureServer/0`, `…/MapServer/2`) is queried as GeoJSON in WGS 84, page
|
|
118
|
+
by page (`maxFeatures`, default 50,000), coloured by the layer's simple or unique-value renderer, and added with
|
|
119
|
+
`map.addData` (points, lines and areas as `<id>`, `<id>:lines` and `<id>:areas`).
|
|
120
|
+
- `token` is an ArcGIS token for secured services. `slot` is passed only when given; a layer without one is drawn on
|
|
121
|
+
top, in the order added.
|
|
122
|
+
- Helpers: `queryFeatures(url, options)`, `serviceInfo(url)`, `parseServiceUrl(url)`, `rendererStyle`,
|
|
123
|
+
`symbolToSimplestyle`.
|
|
124
|
+
|
|
125
|
+
## Example
|
|
126
|
+
|
|
127
|
+
[`examples/index.html`](examples/index.html): serve the repository root (`npx serve .`) and open
|
|
128
|
+
`/arcgis/examples/?key=smk_…` (`&raster=1` for raster tiles).
|
|
129
|
+
|
|
130
|
+
## Licence
|
|
131
|
+
|
|
132
|
+
MIT. The map data is © OpenStreetMap contributors; the credit stays visible on every map.
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@superrouteca/smap-arcgis",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "sMap for the ArcGIS Maps SDK for JavaScript: sMap basemaps (vector and raster), sMap layers as graphics, and ArcGIS REST services on sMap maps.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Goodneo Solutions",
|
|
7
|
+
"homepage": "https://github.com/Goodneo-Solutions/smap-sdk/tree/main/arcgis#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Goodneo-Solutions/smap-sdk.git",
|
|
11
|
+
"directory": "arcgis"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"map",
|
|
15
|
+
"maps",
|
|
16
|
+
"arcgis",
|
|
17
|
+
"esri",
|
|
18
|
+
"vector-tiles",
|
|
19
|
+
"smap",
|
|
20
|
+
"superroute"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"main": "./src/index.js",
|
|
25
|
+
"module": "./src/index.js",
|
|
26
|
+
"types": "./types/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./types/index.d.ts",
|
|
30
|
+
"default": "./src/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./services": {
|
|
33
|
+
"types": "./types/services.d.ts",
|
|
34
|
+
"default": "./src/services.js"
|
|
35
|
+
},
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"src",
|
|
40
|
+
"types",
|
|
41
|
+
"README.md",
|
|
42
|
+
"CHANGELOG.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"test": "node --import ./test/setup.mjs --test test/*.test.mjs",
|
|
50
|
+
"typecheck": "tsc -p test/tsconfig.json"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@superrouteca/smap": "1.1.1"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@arcgis/core": "^5.0.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"@arcgis/core": {
|
|
60
|
+
"optional": true
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@arcgis/core": "5.1.27",
|
|
65
|
+
"@maplibre/maplibre-gl-style-spec": "26.4.4",
|
|
66
|
+
"typescript": "6.0.3"
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/basemap.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// sMap in the ArcGIS Maps SDK for JavaScript: a VectorTileLayer with sMap's style and vector tiles (the five schemes
|
|
2
|
+
// and custom styles, labels in any language), a WebTileLayer of sMap's raster tiles where vector tiles are not wanted,
|
|
3
|
+
// the map key / token on every request through ArcGIS request interceptors, the nearest node with failover, and the
|
|
4
|
+
// sMap badge with the OpenStreetMap credit.
|
|
5
|
+
import esriConfig from '@arcgis/core/config.js';
|
|
6
|
+
import Basemap from '@arcgis/core/Basemap.js';
|
|
7
|
+
import VectorTileLayer from '@arcgis/core/layers/VectorTileLayer.js';
|
|
8
|
+
import WebTileLayer from '@arcgis/core/layers/WebTileLayer.js';
|
|
9
|
+
import { applyBasemapOverrides } from '@superrouteca/smap';
|
|
10
|
+
import { createSession, ATTRIBUTION } from './session.js';
|
|
11
|
+
import { toArcGISStyle } from './style.js';
|
|
12
|
+
|
|
13
|
+
const installed = new WeakMap();
|
|
14
|
+
|
|
15
|
+
function escapeRegExp(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
16
|
+
|
|
17
|
+
function urlsFor(session) {
|
|
18
|
+
return new RegExp('^(' + session.origins().map(escapeRegExp).join('|') + ')(/|$)');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Adds an ArcGIS request interceptor for sMap URLs: each request goes to the node read from now (so a node that
|
|
23
|
+
* failed is left), with the token and the map key as headers; a failed request moves the session to the next node.
|
|
24
|
+
* Installed once per session; returns `{ interceptor, remove() }`.
|
|
25
|
+
*/
|
|
26
|
+
export function installInterceptor(sessionOrOptions) {
|
|
27
|
+
const session = createSession(sessionOrOptions);
|
|
28
|
+
if (installed.has(session)) { return installed.get(session); }
|
|
29
|
+
const interceptor = {
|
|
30
|
+
urls: urlsFor(session),
|
|
31
|
+
before(params) {
|
|
32
|
+
if (!params || !params.url || !session.nodeOf(params.url)) { return undefined; }
|
|
33
|
+
params.url = session.rebase(params.url);
|
|
34
|
+
const ro = params.requestOptions || (params.requestOptions = {});
|
|
35
|
+
ro.headers = { ...(ro.headers || {}), ...session.headers() };
|
|
36
|
+
return undefined;
|
|
37
|
+
},
|
|
38
|
+
error(error) {
|
|
39
|
+
const details = (error && error.details) || {};
|
|
40
|
+
const url = details.url || (error && error.url);
|
|
41
|
+
const status = details.httpStatus || details.status || 0;
|
|
42
|
+
// 4xx answers are final (a refused key stays refused on every node); network errors and 5xx move on.
|
|
43
|
+
if (url && session.nodeOf(url) && (status === 0 || status >= 500)) { session.failed(url); }
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
const update = () => { interceptor.urls = urlsFor(session); };
|
|
47
|
+
const offNode = session.on('nodechange', update);
|
|
48
|
+
session.ready().then(update, () => {});
|
|
49
|
+
esriConfig.request.interceptors = esriConfig.request.interceptors || [];
|
|
50
|
+
esriConfig.request.interceptors.push(interceptor);
|
|
51
|
+
const handle = {
|
|
52
|
+
interceptor,
|
|
53
|
+
remove() {
|
|
54
|
+
offNode();
|
|
55
|
+
const list = esriConfig.request.interceptors;
|
|
56
|
+
const i = list.indexOf(interceptor);
|
|
57
|
+
if (i >= 0) { list.splice(i, 1); }
|
|
58
|
+
installed.delete(session);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
installed.set(session, handle);
|
|
62
|
+
return handle;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A VectorTileLayer drawing sMap's vector tiles in a scheme or custom style, labels in `language`. The style is read
|
|
67
|
+
* through the session (nearest node, failover, key, token); tiles, fonts and sprites through the interceptor.
|
|
68
|
+
*
|
|
69
|
+
* `basemap` shows, hides and recolours classes (as the sMap map's setBasemap).
|
|
70
|
+
*
|
|
71
|
+
* @returns {Promise<VectorTileLayer>} with `smap: { session, style, language, setStyle, setLanguage, setBasemap, basemap }`
|
|
72
|
+
*/
|
|
73
|
+
export async function createVectorTileLayer(options = {}) {
|
|
74
|
+
const session = createSession(options.session || options);
|
|
75
|
+
installInterceptor(session);
|
|
76
|
+
await session.ready();
|
|
77
|
+
let style = options.style || 'light';
|
|
78
|
+
let language = options.language;
|
|
79
|
+
let overrides = options.basemap || null;
|
|
80
|
+
let simplified = [];
|
|
81
|
+
// Basemap options (contract §1: show, hide and recolour by class), applied by the SDK's own function; then the
|
|
82
|
+
// style is converted into what ArcGIS's VectorTileLayer draws (style.js; `convertStyle: false` hands it over as is).
|
|
83
|
+
const styled = async () => {
|
|
84
|
+
const doc = await session.style(style, language);
|
|
85
|
+
const own = overrides && Object.keys(overrides).length ? applyBasemapOverrides(doc, overrides) : doc;
|
|
86
|
+
if (options.convertStyle === false) { simplified = []; return own; }
|
|
87
|
+
const changes = [];
|
|
88
|
+
const out = toArcGISStyle(own, { language, changes });
|
|
89
|
+
simplified = changes;
|
|
90
|
+
return out;
|
|
91
|
+
};
|
|
92
|
+
const layer = new VectorTileLayer({
|
|
93
|
+
id: options.id || 'smap-vector',
|
|
94
|
+
title: options.title || 'sMap',
|
|
95
|
+
style: await styled(),
|
|
96
|
+
...(options.layerProps || {}),
|
|
97
|
+
});
|
|
98
|
+
const reload = async () => { await layer.loadStyle(await styled()); return layer; };
|
|
99
|
+
layer.smap = {
|
|
100
|
+
session,
|
|
101
|
+
get style() { return style; },
|
|
102
|
+
get language() { return language; },
|
|
103
|
+
basemap() { return overrides ? { ...overrides } : {}; },
|
|
104
|
+
/** What the conversion for ArcGIS simplified in the style now shown, one line per layer and property. */
|
|
105
|
+
get simplified() { return simplified.slice(); },
|
|
106
|
+
setStyle(next) { style = next; return reload(); },
|
|
107
|
+
setLanguage(next) { language = next; return reload(); },
|
|
108
|
+
setBasemap(next) { overrides = next && Object.keys(next).length ? { ...next } : null; return reload(); },
|
|
109
|
+
};
|
|
110
|
+
return layer;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A WebTileLayer of sMap's raster tiles (contract §5): the same schemes, custom styles and languages as images drawn on
|
|
115
|
+
* sMap's nodes. `retina` reads 512 px tiles (the @2x images of the same tiles, shown in the 256 px grid). The layer's
|
|
116
|
+
* copyright is the OpenStreetMap credit.
|
|
117
|
+
*/
|
|
118
|
+
export async function createRasterTileLayer(options = {}) {
|
|
119
|
+
const session = createSession(options.session || options);
|
|
120
|
+
installInterceptor(session);
|
|
121
|
+
await session.ready();
|
|
122
|
+
const style = options.style || 'light';
|
|
123
|
+
const urlTemplate = session.rasterTemplate(style, {
|
|
124
|
+
language: options.language,
|
|
125
|
+
retina: options.retina,
|
|
126
|
+
placeholders: { z: '{level}', x: '{col}', y: '{row}' },
|
|
127
|
+
});
|
|
128
|
+
const layer = new WebTileLayer({
|
|
129
|
+
id: options.id || 'smap-raster',
|
|
130
|
+
title: options.title || 'sMap',
|
|
131
|
+
urlTemplate,
|
|
132
|
+
copyright: ATTRIBUTION,
|
|
133
|
+
...(options.layerProps || {}),
|
|
134
|
+
});
|
|
135
|
+
layer.smap = { session, style, language: options.language };
|
|
136
|
+
return layer;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* An sMap basemap for a Map or WebMap (`map.basemap = await createSMapBasemap({ … })`): vector tiles by default,
|
|
141
|
+
* raster tiles with `raster: true`. An existing ArcGIS app switches its basemap to sMap and keeps its layers.
|
|
142
|
+
*/
|
|
143
|
+
export async function createSMapBasemap(options = {}) {
|
|
144
|
+
const style = options.style || 'light';
|
|
145
|
+
const layer = options.raster ? await createRasterTileLayer(options) : await createVectorTileLayer(options);
|
|
146
|
+
const basemap = new Basemap({
|
|
147
|
+
id: options.basemapId || 'smap-' + style.replace(':', '-'),
|
|
148
|
+
title: options.title || 'sMap ' + style.replace(/^custom:/, ''),
|
|
149
|
+
baseLayers: [layer],
|
|
150
|
+
thumbnailUrl: layer.smap.session.rasterTemplate(style, { language: options.language }).replace('{z}', '3').replace('{x}', '2').replace('{y}', '2'),
|
|
151
|
+
});
|
|
152
|
+
basemap.smap = layer.smap;
|
|
153
|
+
return basemap;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const BADGE_ICON = '<svg viewBox="0 0 56 56" width="14" height="14" aria-hidden="true" focusable="false">' +
|
|
157
|
+
'<rect x="2" y="2" width="52" height="52" rx="13" fill="#2563eb"/>' +
|
|
158
|
+
'<path d="M38 15H22a8 8 0 0 0 0 16h12a8 8 0 0 1 0 16H18" fill="none" stroke="#fff" stroke-width="7" ' +
|
|
159
|
+
'stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The sMap badge and the OpenStreetMap credit in the view's bottom-right corner (beside ArcGIS's own attribution,
|
|
163
|
+
* which stays). Returns the element.
|
|
164
|
+
*/
|
|
165
|
+
export function addAttribution(view, options = {}) {
|
|
166
|
+
const doc = options.document || (view.container && view.container.ownerDocument) || globalThis.document;
|
|
167
|
+
const el = doc.createElement('div');
|
|
168
|
+
el.className = 'smap-arcgis-attribution esri-widget';
|
|
169
|
+
el.setAttribute('style', 'display:flex;gap:6px;align-items:center;padding:2px 6px;font:11px/16px Helvetica,Arial,sans-serif;background:rgba(255,255,255,.8)');
|
|
170
|
+
const badge = doc.createElement('span');
|
|
171
|
+
badge.className = 'smap-badge';
|
|
172
|
+
badge.setAttribute('style', 'display:inline-flex;gap:3px;align-items:center;font-weight:600');
|
|
173
|
+
badge.innerHTML = BADGE_ICON + '<span>sMap</span>';
|
|
174
|
+
const credit = doc.createElement('span');
|
|
175
|
+
credit.textContent = ATTRIBUTION;
|
|
176
|
+
el.appendChild(badge);
|
|
177
|
+
el.appendChild(credit);
|
|
178
|
+
view.ui.add(el, 'bottom-right');
|
|
179
|
+
return el;
|
|
180
|
+
}
|
package/src/graphics.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// sMap's points, lines and areas as an ArcGIS GraphicsLayer, with the options of the sMap SDK's addPoints / addLines /
|
|
2
|
+
// addPolygons and the layer's popup as the graphics' PopupTemplate. GeoJSON simplestyle properties colour a layer that
|
|
3
|
+
// gives no colour of its own.
|
|
4
|
+
import Graphic from '@arcgis/core/Graphic.js';
|
|
5
|
+
import GraphicsLayer from '@arcgis/core/layers/GraphicsLayer.js';
|
|
6
|
+
|
|
7
|
+
const GEOMETRY_TYPES = ['Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon', 'GeometryCollection'];
|
|
8
|
+
const WGS84 = { wkid: 4326 };
|
|
9
|
+
|
|
10
|
+
/** sMap layer data (a FeatureCollection, a Feature, a geometry, or an array of points / features) as features. */
|
|
11
|
+
export function toFeatures(data) {
|
|
12
|
+
if (!data) { return []; }
|
|
13
|
+
if (data.type === 'FeatureCollection') { return data.features || []; }
|
|
14
|
+
if (data.type === 'Feature') { return [data]; }
|
|
15
|
+
if (GEOMETRY_TYPES.includes(data.type)) { return [{ type: 'Feature', properties: {}, geometry: data }]; }
|
|
16
|
+
if (Array.isArray(data)) {
|
|
17
|
+
return data.map((p) => {
|
|
18
|
+
if (p && p.type === 'Feature') { return p; }
|
|
19
|
+
let lng = p.lng ?? p.lon ?? p.longitude;
|
|
20
|
+
let lat = p.lat ?? p.latitude;
|
|
21
|
+
if (p.coordinates) { [lng, lat] = p.coordinates; }
|
|
22
|
+
return { type: 'Feature', id: p.id, properties: p, geometry: { type: 'Point', coordinates: [lng, lat] } };
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function signedArea(ring) {
|
|
29
|
+
let a = 0;
|
|
30
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) { a += (ring[i][0] - ring[j][0]) * (ring[i][1] + ring[j][1]); }
|
|
31
|
+
return a / 2; // > 0: clockwise
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ArcGIS polygons: outer rings clockwise, holes counter-clockwise (GeoJSON is the other way round).
|
|
35
|
+
function rings(polygon) {
|
|
36
|
+
return polygon.map((ring, i) => {
|
|
37
|
+
const cw = signedArea(ring) > 0;
|
|
38
|
+
return (i === 0) === cw ? ring : ring.slice().reverse();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** GeoJSON geometry → ArcGIS geometry (autocast object, WGS 84); GeometryCollection → several. */
|
|
43
|
+
export function toArcGisGeometries(g) {
|
|
44
|
+
if (!g) { return []; }
|
|
45
|
+
switch (g.type) {
|
|
46
|
+
case 'Point': return [{ type: 'point', longitude: g.coordinates[0], latitude: g.coordinates[1], spatialReference: WGS84 }];
|
|
47
|
+
case 'MultiPoint': return [{ type: 'multipoint', points: g.coordinates, spatialReference: WGS84 }];
|
|
48
|
+
case 'LineString': return [{ type: 'polyline', paths: [g.coordinates], spatialReference: WGS84 }];
|
|
49
|
+
case 'MultiLineString': return [{ type: 'polyline', paths: g.coordinates, spatialReference: WGS84 }];
|
|
50
|
+
case 'Polygon': return [{ type: 'polygon', rings: rings(g.coordinates), spatialReference: WGS84 }];
|
|
51
|
+
case 'MultiPolygon': return [{ type: 'polygon', rings: g.coordinates.flatMap(rings), spatialReference: WGS84 }];
|
|
52
|
+
case 'GeometryCollection': return (g.geometries || []).flatMap(toArcGisGeometries);
|
|
53
|
+
default: return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function read(value, props, fallback, simplestyleKey) {
|
|
58
|
+
let v;
|
|
59
|
+
if (value === undefined || value === null) { v = simplestyleKey ? props[simplestyleKey] : undefined; }
|
|
60
|
+
else if (typeof value === 'object' && !Array.isArray(value) && value.property) { v = props[value.property] ?? value.default; }
|
|
61
|
+
else if (Array.isArray(value) && value.length === 2 && value[0] === 'get') { v = props[value[1]]; }
|
|
62
|
+
else { v = value; }
|
|
63
|
+
return v === undefined || v === null ? fallback : v;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function pinSvg(fill, border, width, round) {
|
|
67
|
+
const svg = round
|
|
68
|
+
? '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="' + (15 - width / 2) + '" fill="' + fill + '" stroke="' + border + '" stroke-width="' + width + '"/></svg>'
|
|
69
|
+
: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="42" viewBox="0 0 32 42"><path d="M16 40.5C16 40.5 2.5 25 2.5 15.5a13.5 13.5 0 1 1 27 0C29.5 25 16 40.5 16 40.5Z" fill="' + fill + '" stroke="' + border + '" stroke-width="' + width + '" stroke-linejoin="round"/></svg>';
|
|
70
|
+
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const SIZES = { small: 8, medium: 12, large: 18 };
|
|
74
|
+
|
|
75
|
+
/** The ArcGIS symbol of one feature for sMap point / line / area options. */
|
|
76
|
+
export function symbolFor(kind, props, options = {}) {
|
|
77
|
+
if (kind === 'points') {
|
|
78
|
+
const mk = options.marker;
|
|
79
|
+
if (mk) {
|
|
80
|
+
const round = mk.shape === 'circle' || mk.shape === 'vehicle';
|
|
81
|
+
const size = mk.size || 32;
|
|
82
|
+
const border = read(mk.border, props, read(options.color, props, '#2563eb', 'marker-color'));
|
|
83
|
+
return {
|
|
84
|
+
type: 'picture-marker', url: pinSvg(read(mk.fill, props, '#ffffff'), border, mk.borderWidth ?? 3, round),
|
|
85
|
+
width: size * 0.75, height: (round ? size : size * 42 / 32) * 0.75, yoffset: round ? 0 : size * 42 / 32 * 0.375,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (options.icon) {
|
|
89
|
+
const s = (options.iconSize || 1) * 24;
|
|
90
|
+
return { type: 'picture-marker', url: options.icon, width: s, height: s };
|
|
91
|
+
}
|
|
92
|
+
const radius = options.radius ?? (SIZES[props['marker-size']] ? SIZES[props['marker-size']] / 2 : 6);
|
|
93
|
+
return {
|
|
94
|
+
type: 'simple-marker', style: 'circle', size: radius * 1.5,
|
|
95
|
+
color: read(options.color, props, '#2563eb', 'marker-color'),
|
|
96
|
+
outline: { color: read(options.strokeColor, props, '#ffffff'), width: (options.strokeWidth ?? 2) * 0.75 },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (kind === 'lines') {
|
|
100
|
+
const color = read(options.color, props, '#2563eb', 'stroke');
|
|
101
|
+
const width = options.width ?? props['stroke-width'] ?? 4;
|
|
102
|
+
const dash = Array.isArray(options.dash) && options.dash.length ? 'dash' : 'solid';
|
|
103
|
+
return { type: 'simple-line', color: withAlpha(color, options.opacity ?? props['stroke-opacity']), width: width * 0.75, style: dash, cap: 'round', join: 'round' };
|
|
104
|
+
}
|
|
105
|
+
const fill = read(options.fill ?? options.color, props, '#2563eb', 'fill');
|
|
106
|
+
return {
|
|
107
|
+
type: 'simple-fill',
|
|
108
|
+
color: withAlpha(fill, options.opacity ?? props['fill-opacity'] ?? 0.3),
|
|
109
|
+
outline: { color: read(options.outline, props, fill, 'stroke'), width: (options.outlineWidth ?? 1) * 0.75 },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function withAlpha(color, alpha) {
|
|
114
|
+
if (alpha === undefined || alpha === null) { return color; }
|
|
115
|
+
if (typeof color === 'string' && /^#[0-9a-f]{6}$/i.test(color)) {
|
|
116
|
+
const n = (i) => parseInt(color.slice(i, i + 2), 16);
|
|
117
|
+
return [n(1), n(3), n(5), alpha];
|
|
118
|
+
}
|
|
119
|
+
return color;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A GraphicsLayer of sMap layer data. `kind` is `points`, `lines` or `areas` (the SDK's addPoints, addLines,
|
|
124
|
+
* addPolygons options apply); the `popup` option becomes each graphic's PopupTemplate (content from the feature's
|
|
125
|
+
* attributes; a promise is awaited by the popup), and `label` a text graphic beside each point.
|
|
126
|
+
*/
|
|
127
|
+
export function createGraphicsLayer(id, kind, data, options = {}) {
|
|
128
|
+
if (!['points', 'lines', 'areas'].includes(kind)) { throw new Error('sMap: kind is points, lines or areas, not "' + kind + '"'); }
|
|
129
|
+
const graphics = [];
|
|
130
|
+
const want = { points: /Point$/, lines: /LineString$/, areas: /Polygon$/ }[kind];
|
|
131
|
+
for (const f of toFeatures(data)) {
|
|
132
|
+
const props = { ...(f.properties || {}) };
|
|
133
|
+
const parts = f.geometry && f.geometry.type === 'GeometryCollection' ? f.geometry.geometries || [] : [f.geometry];
|
|
134
|
+
const geometries = parts.filter((g) => g && want.test(g.type)).flatMap(toArcGisGeometries);
|
|
135
|
+
for (const geometry of geometries) {
|
|
136
|
+
const graphic = { geometry, attributes: props, symbol: symbolFor(kind, props, options) };
|
|
137
|
+
if (options.popup) {
|
|
138
|
+
graphic.popupTemplate = {
|
|
139
|
+
title: options.popupTitle || '',
|
|
140
|
+
content: (event) => {
|
|
141
|
+
const attrs = (event && event.graphic && event.graphic.attributes) || props;
|
|
142
|
+
return options.popup(attrs, f);
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
graphics.push(new Graphic(graphic));
|
|
147
|
+
if (kind === 'points' && options.label && props[options.label] != null) {
|
|
148
|
+
const right = options.labelPosition === 'right';
|
|
149
|
+
graphics.push(new Graphic({
|
|
150
|
+
geometry, attributes: props,
|
|
151
|
+
symbol: {
|
|
152
|
+
type: 'text', text: String(props[options.label]), color: '#333333', haloColor: '#ffffff', haloSize: 1.5,
|
|
153
|
+
font: { size: 9, family: 'sans-serif' }, horizontalAlignment: right ? 'left' : 'center',
|
|
154
|
+
xoffset: right ? 12 : 0, yoffset: right ? 0 : -14,
|
|
155
|
+
},
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const layer = new GraphicsLayer({ id, title: options.title || id, graphics, ...(options.layerProps || {}) });
|
|
161
|
+
if (options.minzoom !== undefined) { layer.minScale = zoomToScale(options.minzoom); }
|
|
162
|
+
if (options.maxzoom !== undefined) { layer.maxScale = zoomToScale(options.maxzoom); }
|
|
163
|
+
return layer;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The Web Mercator map scale at a zoom (96 dpi, 256 px tiles), for minScale / maxScale. */
|
|
167
|
+
export function zoomToScale(zoom) {
|
|
168
|
+
return 591657527.591555 / Math.pow(2, zoom);
|
|
169
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @superrouteca/smap-arcgis — sMap and the ArcGIS Maps SDK for JavaScript both ways.
|
|
2
|
+
export { Session, createSession, FLAVORS, ATTRIBUTION } from './session.js';
|
|
3
|
+
export { installInterceptor, createVectorTileLayer, createRasterTileLayer, createSMapBasemap, addAttribution } from './basemap.js';
|
|
4
|
+
export { toArcGISStyle, styleLanguage, nameExpression } from './style.js';
|
|
5
|
+
export { createGraphicsLayer, symbolFor, toArcGisGeometries, toFeatures, zoomToScale } from './graphics.js';
|
|
6
|
+
export { addArcGisService, queryFeatures, serviceInfo, parseServiceUrl, withToken, symbolToSimplestyle, rendererStyle } from './services.js';
|
|
7
|
+
export const version = '1.1.1';
|