@nways/maps 0.0.0-stage → 1.0.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/style.js ADDED
@@ -0,0 +1,228 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 N-WAYS LIMITED
3
+ // The N-Ways Maps SDK's styles: the address of N-Ways' hosted style, and a
4
+ // built-in compact light or dark street map drawn from N-Ways' vector tiles
5
+ // (the base PMTiles archive) and fonts, with the partner's API key on every
6
+ // request. Pure data, no MapLibre needed, so it can be tested and served on
7
+ // its own.
8
+ //
9
+ // The N-Ways app draws a much richer style (src/lib/map/style.ts: 3D bridges,
10
+ // traffic, place icons). This one keeps to what any partner map needs and
11
+ // takes the same colours.
12
+
13
+ /** Where the tiles and fonts are served. */
14
+ export const NWAYS_ORIGIN = 'https://nways.ng'
15
+
16
+ /** The tile archives a partner map may use. */
17
+ export const TILESETS = { base: 'nigeria.pmtiles' }
18
+
19
+ /** Data credits every N-Ways map must show (OpenStreetMap's licence, the ODbL, requires it). */
20
+ export const ATTRIBUTION =
21
+ '<a href="https://nways.ng/developers/terms#attribution">N-Ways</a> · © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors · <a href="https://protomaps.com">Protomaps</a>'
22
+
23
+ const FONT = { regular: ['Inter-Regular'], medium: ['Inter-Medium'], semibold: ['Inter-SemiBold'], italic: ['Inter-Italic'] }
24
+
25
+ /** Colours, from the app's palettes (src/lib/map/style.ts, roadLook.ts). */
26
+ export const THEMES = {
27
+ light: {
28
+ earth: '#bcd59c',
29
+ park: '#86b663',
30
+ forest: '#8fbb6c',
31
+ water: '#74b3dc',
32
+ waterLabel: '#3f7795',
33
+ building: '#cec4b4',
34
+ buildingOutline: '#b8ad9c',
35
+ minor: '#fbfaf7',
36
+ minorCasing: '#94a985',
37
+ major: '#fbfcf6',
38
+ majorCasing: '#7f9d70',
39
+ highway: '#e9f1df',
40
+ highwayCasing: '#4f7a4a',
41
+ path: '#a3b98a',
42
+ boundary: '#7d8a76',
43
+ label: '#25302b',
44
+ labelMinor: '#5d6761',
45
+ halo: '#e7f0da',
46
+ },
47
+ dark: {
48
+ earth: '#283140',
49
+ park: '#1d5249',
50
+ forest: '#1f4a45',
51
+ water: '#1a3550',
52
+ waterLabel: '#7fa8c9',
53
+ building: '#333c4b',
54
+ buildingOutline: '#3b4555',
55
+ minor: '#737b8d',
56
+ minorCasing: '#1b2029',
57
+ major: '#7a958a',
58
+ majorCasing: '#1f2b27',
59
+ highway: '#3d7d59',
60
+ highwayCasing: '#1d4430',
61
+ path: '#4f5768',
62
+ boundary: '#8a93a3',
63
+ label: '#e3e8ef',
64
+ labelMinor: '#a4aebb',
65
+ halo: '#283140',
66
+ },
67
+ }
68
+
69
+ /** N-Ways' hosted styles, by theme (served at /maps/v1/styles/<id>.json). */
70
+ export const STYLE_IDS = { light: 'streets-light', dark: 'streets-dark' }
71
+
72
+ /**
73
+ * The address of N-Ways' hosted style for a theme and language, with the key:
74
+ * `{ key, theme: 'light' | 'dark', lang: 'en' | 'fr', origin }`. The hosted style
75
+ * is kept up to date by N-Ways (new layers, fixes) without a new SDK release.
76
+ */
77
+ export function styleUrl(options = {}) {
78
+ const { key = '', theme = 'light', origin = NWAYS_ORIGIN, lang = 'en' } = options
79
+ const id = STYLE_IDS[theme]
80
+ if (!id) throw new Error(`N-Ways: unknown theme "${theme}" (light or dark)`)
81
+ const url = `${origin}/maps/v1/styles/${id}.json` + (lang === 'fr' ? '?lang=fr' : '')
82
+ return withKey(url, key, origin)
83
+ }
84
+
85
+ /** Adds the API key to an N-Ways address (and only to N-Ways addresses), once. */
86
+ export function withKey(url, key, origin = NWAYS_ORIGIN) {
87
+ if (!key || !url.startsWith(origin + '/')) return url
88
+ const [path, hash] = url.split('#')
89
+ if (/[?&]key=/.test(path)) return url // already has one
90
+ return path + (path.includes('?') ? '&' : '?') + 'key=' + encodeURIComponent(key) + (hash !== undefined ? '#' + hash : '')
91
+ }
92
+
93
+ const zoomWidth = (stops) => ['interpolate', ['exponential', 1.6], ['zoom'], ...stops.flat()]
94
+ const kindIs = (...kinds) => ['in', ['get', 'kind'], ['literal', kinds]]
95
+
96
+ /**
97
+ * The style for a map: `{ key, theme: 'light' | 'dark', origin, lang: 'en' | 'fr' }`.
98
+ * Labels use the local name (`name`); French maps prefer `name:fr` where there is one.
99
+ */
100
+ export function buildStyle(options = {}) {
101
+ const { key = '', theme = 'light', origin = NWAYS_ORIGIN, lang = 'en' } = options
102
+ const c = THEMES[theme]
103
+ if (!c) throw new Error(`N-Ways: unknown theme "${theme}" (light or dark)`)
104
+ const name = lang === 'fr' ? ['coalesce', ['get', 'name:fr'], ['get', 'name']] : ['get', 'name']
105
+ const archive = `${origin}/tiles/${TILESETS.base}`
106
+ const road = (id, kinds, colour, casing, widths, minzoom = 0) => [
107
+ {
108
+ id: `${id}-casing`,
109
+ type: 'line',
110
+ source: 'nways',
111
+ 'source-layer': 'roads',
112
+ minzoom,
113
+ filter: kindIs(...kinds),
114
+ layout: { 'line-cap': 'round', 'line-join': 'round' },
115
+ paint: { 'line-color': casing, 'line-width': zoomWidth(widths.map(([z, w]) => [z, w + 1.5])) },
116
+ },
117
+ {
118
+ id,
119
+ type: 'line',
120
+ source: 'nways',
121
+ 'source-layer': 'roads',
122
+ minzoom,
123
+ filter: kindIs(...kinds),
124
+ layout: { 'line-cap': 'round', 'line-join': 'round' },
125
+ paint: { 'line-color': colour, 'line-width': zoomWidth(widths) },
126
+ },
127
+ ]
128
+ return {
129
+ version: 8,
130
+ name: `N-Ways ${theme}`,
131
+ glyphs: withKey(`${origin}/fonts/{fontstack}/{range}.pbf`, key, origin),
132
+ sources: {
133
+ nways: {
134
+ type: 'vector',
135
+ // PMTiles: one archive read with range requests (the pmtiles protocol, registered by createMap).
136
+ url: `pmtiles://${withKey(archive, key, origin)}`,
137
+ attribution: ATTRIBUTION,
138
+ },
139
+ },
140
+ layers: [
141
+ { id: 'background', type: 'background', paint: { 'background-color': c.earth } },
142
+ {
143
+ id: 'landuse-green',
144
+ type: 'fill',
145
+ source: 'nways',
146
+ 'source-layer': 'landuse',
147
+ filter: kindIs('park', 'forest', 'wood', 'nature_reserve', 'grass', 'garden', 'golf_course', 'cemetery'),
148
+ paint: { 'fill-color': ['match', ['get', 'kind'], ['forest', 'wood', 'nature_reserve'], c.forest, c.park], 'fill-opacity': 0.7 },
149
+ },
150
+ { id: 'water', type: 'fill', source: 'nways', 'source-layer': 'water', paint: { 'fill-color': c.water } },
151
+ {
152
+ id: 'buildings',
153
+ type: 'fill',
154
+ source: 'nways',
155
+ 'source-layer': 'buildings',
156
+ minzoom: 14,
157
+ paint: { 'fill-color': c.building, 'fill-outline-color': c.buildingOutline },
158
+ },
159
+ {
160
+ id: 'paths',
161
+ type: 'line',
162
+ source: 'nways',
163
+ 'source-layer': 'roads',
164
+ minzoom: 14,
165
+ filter: kindIs('path'),
166
+ paint: { 'line-color': c.path, 'line-width': 1, 'line-dasharray': [2, 1.5] },
167
+ },
168
+ ...road('roads-minor', ['minor_road', 'other'], c.minor, c.minorCasing, [[12, 0.5], [15, 3], [18, 12]], 11),
169
+ ...road('roads-major', ['major_road', 'medium_road'], c.major, c.majorCasing, [[8, 0.6], [12, 1.6], [15, 5], [18, 16]], 7),
170
+ ...road('roads-highway', ['highway'], c.highway, c.highwayCasing, [[5, 0.6], [10, 1.8], [14, 5], [18, 20]]),
171
+ {
172
+ id: 'boundaries',
173
+ type: 'line',
174
+ source: 'nways',
175
+ 'source-layer': 'boundaries',
176
+ filter: kindIs('country', 'region'),
177
+ layout: { 'line-join': 'round' },
178
+ paint: { 'line-color': c.boundary, 'line-width': ['match', ['get', 'kind'], 'country', 1.4, 0.8], 'line-dasharray': [3, 2] },
179
+ },
180
+ {
181
+ id: 'water-labels',
182
+ type: 'symbol',
183
+ source: 'nways',
184
+ 'source-layer': 'water',
185
+ minzoom: 10,
186
+ filter: ['has', 'name'],
187
+ layout: { 'text-field': name, 'text-font': FONT.italic, 'text-size': 12 },
188
+ paint: { 'text-color': c.waterLabel, 'text-halo-color': c.halo, 'text-halo-width': 1 },
189
+ },
190
+ {
191
+ id: 'road-labels',
192
+ type: 'symbol',
193
+ source: 'nways',
194
+ 'source-layer': 'roads',
195
+ minzoom: 13,
196
+ filter: ['has', 'name'],
197
+ layout: {
198
+ 'symbol-placement': 'line',
199
+ 'text-field': name,
200
+ 'text-font': FONT.medium,
201
+ 'text-size': ['interpolate', ['linear'], ['zoom'], 13, 10, 18, 14],
202
+ },
203
+ paint: { 'text-color': c.labelMinor, 'text-halo-color': c.halo, 'text-halo-width': 1.5 },
204
+ },
205
+ {
206
+ id: 'place-labels',
207
+ type: 'symbol',
208
+ source: 'nways',
209
+ 'source-layer': 'places',
210
+ // Each place from the zoom the tiles suggest, so towns don't crowd out cities.
211
+ filter: ['all', kindIs('locality', 'neighbourhood', 'region', 'country'), ['<=', ['to-number', ['get', 'min_zoom'], 0], ['+', ['zoom'], 1]]],
212
+ layout: {
213
+ 'text-field': name,
214
+ 'text-font': ['match', ['get', 'kind'], ['country', 'region'], ['literal', FONT.semibold], ['literal', FONT.medium]],
215
+ 'text-size': ['match', ['get', 'kind'], 'country', 15, 'region', 13, 'neighbourhood', 11, 13],
216
+ 'text-transform': ['match', ['get', 'kind'], ['country', 'region'], 'uppercase', 'none'],
217
+ 'text-letter-spacing': ['match', ['get', 'kind'], ['country', 'region'], 0.08, 0],
218
+ 'text-max-width': 8,
219
+ },
220
+ paint: {
221
+ 'text-color': ['match', ['get', 'kind'], 'neighbourhood', c.labelMinor, c.label],
222
+ 'text-halo-color': c.halo,
223
+ 'text-halo-width': 1.5,
224
+ },
225
+ },
226
+ ],
227
+ }
228
+ }