leaflet-india-boundary 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/LICENSE +25 -0
- package/LICENSE-DATA +101 -0
- package/README.md +391 -0
- package/data/india-boundary.geojson +1 -0
- package/dist/cjs/attribution.js +24 -0
- package/dist/cjs/attribution.js.map +1 -0
- package/dist/cjs/data.js +78 -0
- package/dist/cjs/data.js.map +1 -0
- package/dist/cjs/index.js +39 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/leaflet.js +152 -0
- package/dist/cjs/leaflet.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/react.js +77 -0
- package/dist/cjs/react.js.map +1 -0
- package/dist/cjs/style.js +129 -0
- package/dist/cjs/style.js.map +1 -0
- package/dist/cjs/suppress.js +462 -0
- package/dist/cjs/suppress.js.map +1 -0
- package/dist/cjs/suppressionData.js +205 -0
- package/dist/cjs/suppressionData.js.map +1 -0
- package/dist/esm/attribution.d.ts +21 -0
- package/dist/esm/attribution.d.ts.map +1 -0
- package/dist/esm/attribution.js +21 -0
- package/dist/esm/attribution.js.map +1 -0
- package/dist/esm/data.d.ts +78 -0
- package/dist/esm/data.d.ts.map +1 -0
- package/dist/esm/data.js +75 -0
- package/dist/esm/data.js.map +1 -0
- package/dist/esm/index.d.ts +19 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +19 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/leaflet.d.ts +103 -0
- package/dist/esm/leaflet.d.ts.map +1 -0
- package/dist/esm/leaflet.js +147 -0
- package/dist/esm/leaflet.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/react.d.ts +31 -0
- package/dist/esm/react.d.ts.map +1 -0
- package/dist/esm/react.js +69 -0
- package/dist/esm/react.js.map +1 -0
- package/dist/esm/style.d.ts +106 -0
- package/dist/esm/style.d.ts.map +1 -0
- package/dist/esm/style.js +121 -0
- package/dist/esm/style.js.map +1 -0
- package/dist/esm/suppress.d.ts +64 -0
- package/dist/esm/suppress.d.ts.map +1 -0
- package/dist/esm/suppress.js +456 -0
- package/dist/esm/suppress.js.map +1 -0
- package/dist/esm/suppressionData.d.ts +34 -0
- package/dist/esm/suppressionData.d.ts.map +1 -0
- package/dist/esm/suppressionData.js +202 -0
- package/dist/esm/suppressionData.js.map +1 -0
- package/dist/style.css +66 -0
- package/package.json +113 -0
- package/src/attribution.ts +23 -0
- package/src/data.ts +153 -0
- package/src/index.ts +45 -0
- package/src/leaflet.ts +252 -0
- package/src/react.ts +92 -0
- package/src/style.css +66 -0
- package/src/style.ts +168 -0
- package/src/suppress.ts +599 -0
- package/src/suppressionData.ts +238 -0
- package/tools/build-india-boundary.mjs +836 -0
- package/tools/postbuild.mjs +61 -0
package/src/leaflet.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import {
|
|
2
|
+
geoJSON,
|
|
3
|
+
type GeoJSON as LeafletGeoJSON,
|
|
4
|
+
type Map as LeafletMap,
|
|
5
|
+
type Path,
|
|
6
|
+
type PathOptions,
|
|
7
|
+
} from "leaflet";
|
|
8
|
+
import { INDIA_BOUNDARY_ATTRIBUTION } from "./attribution.js";
|
|
9
|
+
import { INDIA_BOUNDARY, type IndiaBoundaryProperties } from "./data.js";
|
|
10
|
+
import {
|
|
11
|
+
isOsmAdminBoundaryVisible,
|
|
12
|
+
OSM_ADMIN_BOUNDARY_MIN_ZOOM,
|
|
13
|
+
OSM_ADMIN_BOUNDARY_OPACITY,
|
|
14
|
+
osmAdminBoundaryColor,
|
|
15
|
+
osmAdminBoundaryWidth,
|
|
16
|
+
OSM_ADMIN_BOUNDARY_WIDE_ZOOM,
|
|
17
|
+
type OsmAdminBoundaryStyleOptions,
|
|
18
|
+
} from "./style.js";
|
|
19
|
+
|
|
20
|
+
/** Base class Leaflet writes onto the SVG paths, for CSS overrides. */
|
|
21
|
+
export const INDIA_BOUNDARY_CLASS = "leaflet-india-boundary";
|
|
22
|
+
|
|
23
|
+
/** Added alongside the base class from `wideZoom` up, mirroring carto's colour swap. */
|
|
24
|
+
export const INDIA_BOUNDARY_WIDE_CLASS = "leaflet-india-boundary--wide";
|
|
25
|
+
|
|
26
|
+
/** Pane the layer creates and filters when `tileFilter` is set. */
|
|
27
|
+
export const INDIA_BOUNDARY_PANE = "indiaBoundary";
|
|
28
|
+
|
|
29
|
+
export interface IndiaBoundaryLayerOptions extends OsmAdminBoundaryStyleOptions {
|
|
30
|
+
/**
|
|
31
|
+
* A CSS filter to put the boundary stroke through — pass exactly the same value
|
|
32
|
+
* your app applies to `.leaflet-tile-pane`.
|
|
33
|
+
*
|
|
34
|
+
* **This is the option most likely to be needed and least likely to be
|
|
35
|
+
* guessed, so it is worth the paragraph.** If your basemap is stock OSM raster
|
|
36
|
+
* tiles, leave this unset and everything below is irrelevant.
|
|
37
|
+
*
|
|
38
|
+
* If instead you filter your tile pane — inverting OSM's light tiles to build a
|
|
39
|
+
* dark basemap is the common case — then the tiles on screen are no longer the
|
|
40
|
+
* colours OSM rendered. carto's border colours are still correct *inside the
|
|
41
|
+
* tile*, but your overlay is drawn into a pane that is not filtered, so a
|
|
42
|
+
* colour lifted from carto's stylesheet lands nowhere near the purple actually
|
|
43
|
+
* visible. Passing the same filter here puts the overlay through the same
|
|
44
|
+
* transform, and it comes out matching.
|
|
45
|
+
*
|
|
46
|
+
* Setting this also raises the default `opacity` from carto's 0.5 to 1 — see
|
|
47
|
+
* {@link IndiaBoundaryLayerOptions.opacity}.
|
|
48
|
+
*
|
|
49
|
+
* Implemented by creating a dedicated map pane and filtering that, so it works
|
|
50
|
+
* under both the SVG and Canvas renderers. If you also pass `pane`, the filter
|
|
51
|
+
* goes on *your* pane and therefore on everything else in it, so pass a pane you
|
|
52
|
+
* own.
|
|
53
|
+
*/
|
|
54
|
+
tileFilter?: string;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Stroke opacity. Defaults to carto's own 0.5, or to 1 when `tileFilter` is set.
|
|
58
|
+
*
|
|
59
|
+
* The default is conditional because carto's 0.5 is not a free-standing value —
|
|
60
|
+
* it is composited against carto's light land fill (`#f2efe9`) *inside the tile*,
|
|
61
|
+
* before any filter of yours runs. Honouring 0.5 on the unfiltered side of an
|
|
62
|
+
* inverting filter, over a near-black map, lands about three times too dark. The
|
|
63
|
+
* exact match was tried in the app this came from and resolves to `#5b5157`,
|
|
64
|
+
* which is genuinely indistinguishable from OSM's own borders and therefore
|
|
65
|
+
* genuinely invisible: a dark basemap crushes every border, OSM's included.
|
|
66
|
+
*
|
|
67
|
+
* So: unfiltered basemap, 0.5 is right. Filtered basemap, 1 keeps carto's hue
|
|
68
|
+
* while staying legible, which is the entire point of drawing the line.
|
|
69
|
+
*/
|
|
70
|
+
opacity?: number;
|
|
71
|
+
|
|
72
|
+
/** Extra class(es) on each path, alongside {@link INDIA_BOUNDARY_CLASS}. */
|
|
73
|
+
className?: string;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Map pane to draw into. Defaults to Leaflet's `overlayPane`, or to a pane this
|
|
77
|
+
* layer creates ({@link INDIA_BOUNDARY_PANE}) when `tileFilter` is set.
|
|
78
|
+
*/
|
|
79
|
+
pane?: string;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Attribution shown in Leaflet's attribution control. Defaults to
|
|
83
|
+
* {@link INDIA_BOUNDARY_ATTRIBUTION}, because the OSM-derived portion of this
|
|
84
|
+
* data is ODbL and asks for it. Pass `false` only if you credit it elsewhere.
|
|
85
|
+
*/
|
|
86
|
+
attribution?: string | false;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether the boundary responds to the pointer. Off by default: it is a border,
|
|
90
|
+
* not a feature, and a national boundary swallowing clicks meant for the markers
|
|
91
|
+
* underneath it is a bug in every app I can imagine.
|
|
92
|
+
*/
|
|
93
|
+
interactive?: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface Resolved {
|
|
97
|
+
style: OsmAdminBoundaryStyleOptions;
|
|
98
|
+
opacity: number;
|
|
99
|
+
wideZoom: number;
|
|
100
|
+
className?: string;
|
|
101
|
+
tileFilter?: string;
|
|
102
|
+
pane?: string;
|
|
103
|
+
interactive: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolve(options: IndiaBoundaryLayerOptions): Resolved {
|
|
107
|
+
return {
|
|
108
|
+
style: options,
|
|
109
|
+
// The one line in this package that encodes the filter interaction.
|
|
110
|
+
opacity: options.opacity ?? (options.tileFilter ? 1 : OSM_ADMIN_BOUNDARY_OPACITY),
|
|
111
|
+
wideZoom: options.wideZoom ?? OSM_ADMIN_BOUNDARY_WIDE_ZOOM,
|
|
112
|
+
className: options.className,
|
|
113
|
+
tileFilter: options.tileFilter,
|
|
114
|
+
pane: options.pane ?? (options.tileFilter ? INDIA_BOUNDARY_PANE : undefined),
|
|
115
|
+
interactive: options.interactive ?? false,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function pathStyle(zoom: number, r: Resolved): PathOptions {
|
|
120
|
+
const wide = zoom >= r.wideZoom;
|
|
121
|
+
const classes = [INDIA_BOUNDARY_CLASS];
|
|
122
|
+
if (wide) classes.push(INDIA_BOUNDARY_WIDE_CLASS);
|
|
123
|
+
if (r.className) classes.push(r.className);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
className: classes.join(" "),
|
|
127
|
+
color: osmAdminBoundaryColor(zoom, r.style),
|
|
128
|
+
weight: osmAdminBoundaryWidth(zoom),
|
|
129
|
+
opacity: r.opacity,
|
|
130
|
+
// Below carto's z4 the basemap draws no national borders, so this draws
|
|
131
|
+
// nothing either. `stroke: false` rather than removing the layer from the
|
|
132
|
+
// map: same result on screen, but `map.hasLayer()` keeps telling the truth
|
|
133
|
+
// and layer-control checkboxes do not flicker.
|
|
134
|
+
stroke: isOsmAdminBoundaryVisible(zoom, r.style),
|
|
135
|
+
fill: false,
|
|
136
|
+
interactive: r.interactive,
|
|
137
|
+
// carto joins its border segments with a bevel. Note that nothing in CSS may
|
|
138
|
+
// set `stroke-linejoin` — a CSS property would beat the SVG attribute
|
|
139
|
+
// Leaflet writes here.
|
|
140
|
+
lineJoin: "bevel",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* India's boundary through Jammu & Kashmir, Ladakh and Aksai Chin — as depicted
|
|
146
|
+
* by the Survey of India — drawn over your basemap in the basemap's own border
|
|
147
|
+
* style.
|
|
148
|
+
*
|
|
149
|
+
* ```js
|
|
150
|
+
* import { indiaBoundaryLayer } from "leaflet-india-boundary/leaflet";
|
|
151
|
+
*
|
|
152
|
+
* indiaBoundaryLayer().addTo(map);
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
155
|
+
* The returned layer restyles itself on zoom, so carto's per-zoom widths and its
|
|
156
|
+
* colour swap at z10 keep matching the tiles underneath, and it draws nothing
|
|
157
|
+
* below z4 where the basemap has no national borders to match.
|
|
158
|
+
*
|
|
159
|
+
* ## The ceiling, stated plainly
|
|
160
|
+
*
|
|
161
|
+
* This *adds* India's claimed border. It cannot *remove* the Line of Control /
|
|
162
|
+
* Line of Actual Control that raster tiles have already drawn, because by the time
|
|
163
|
+
* a tile reaches the browser it is a picture. Where the two run apart, both are
|
|
164
|
+
* visible. Suppressing the basemap's own line needs either vector tiles, where the
|
|
165
|
+
* boundary layer can be filtered out, or a provider political view — HERE's
|
|
166
|
+
* `pview=IN`, MapTiler's worldview setting.
|
|
167
|
+
*/
|
|
168
|
+
export function indiaBoundaryLayer(
|
|
169
|
+
options: IndiaBoundaryLayerOptions = {},
|
|
170
|
+
): LeafletGeoJSON<IndiaBoundaryProperties> {
|
|
171
|
+
const r = resolve(options);
|
|
172
|
+
const attribution = options.attribution ?? INDIA_BOUNDARY_ATTRIBUTION;
|
|
173
|
+
|
|
174
|
+
// Declared before `geoJSON()` on purpose. Leaflet builds the child paths
|
|
175
|
+
// synchronously inside the constructor, which calls `style` right there — so a
|
|
176
|
+
// `style` closure reading a `let` declared afterwards would hit it in its
|
|
177
|
+
// temporal dead zone and throw on construction.
|
|
178
|
+
let map: LeafletMap | null = null;
|
|
179
|
+
const zoomNow = () => map?.getZoom() ?? r.style.minZoom ?? OSM_ADMIN_BOUNDARY_MIN_ZOOM;
|
|
180
|
+
|
|
181
|
+
const layer = geoJSON<IndiaBoundaryProperties>(INDIA_BOUNDARY, {
|
|
182
|
+
style: () => pathStyle(zoomNow(), r),
|
|
183
|
+
pane: r.pane,
|
|
184
|
+
interactive: r.interactive,
|
|
185
|
+
...(attribution === false ? {} : { attribution }),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const restyle = () => {
|
|
189
|
+
const zoom = zoomNow();
|
|
190
|
+
layer.setStyle(() => pathStyle(zoom, r));
|
|
191
|
+
|
|
192
|
+
// `setStyle` does not reapply `className`. Leaflet writes the class once, when
|
|
193
|
+
// it creates the path element, and its `_updateStyle` only touches the paint
|
|
194
|
+
// attributes — so the wide-zoom modifier has to be toggled by hand or it stays
|
|
195
|
+
// frozen at whatever it was when the layer was built. Harmless for the default
|
|
196
|
+
// rendering, where the colour arrives as the `stroke` attribute; not harmless
|
|
197
|
+
// for anyone driving the colours from style.css, where the class *is* the
|
|
198
|
+
// mechanism.
|
|
199
|
+
const wide = zoom >= r.wideZoom;
|
|
200
|
+
for (const el of indiaBoundaryElements(layer)) {
|
|
201
|
+
el.classList.toggle(INDIA_BOUNDARY_WIDE_CLASS, wide);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Instance-level overrides of two documented `Layer` methods. Leaflet's own
|
|
206
|
+
// class system (`L.GeoJSON.extend`) would work too, but it types poorly and
|
|
207
|
+
// subclassing it from an ES class fights its `initialize` convention for no
|
|
208
|
+
// gain — there is no second implementation to share.
|
|
209
|
+
const baseOnAdd = layer.onAdd.bind(layer);
|
|
210
|
+
const baseOnRemove = layer.onRemove.bind(layer);
|
|
211
|
+
|
|
212
|
+
layer.onAdd = (m: LeafletMap) => {
|
|
213
|
+
map = m;
|
|
214
|
+
// Must happen before the base `onAdd`, which is when each path resolves the
|
|
215
|
+
// renderer that will live in this pane.
|
|
216
|
+
if (r.tileFilter && r.pane) {
|
|
217
|
+
const pane = m.getPane(r.pane) ?? m.createPane(r.pane);
|
|
218
|
+
if (!pane.style.zIndex) pane.style.zIndex = "400"; // beside overlayPane, under markers
|
|
219
|
+
pane.style.filter = r.tileFilter;
|
|
220
|
+
if (!r.interactive) pane.style.pointerEvents = "none";
|
|
221
|
+
}
|
|
222
|
+
const result = baseOnAdd(m);
|
|
223
|
+
m.on("zoomend", restyle);
|
|
224
|
+
restyle();
|
|
225
|
+
return result;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
layer.onRemove = (m: LeafletMap) => {
|
|
229
|
+
m.off("zoomend", restyle);
|
|
230
|
+
map = null;
|
|
231
|
+
return baseOnRemove(m);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return layer;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Escape hatch for anyone doing something this factory does not cover — reading
|
|
239
|
+
* the live SVG elements, say, to hand them to an animation library.
|
|
240
|
+
*
|
|
241
|
+
* Returns undefined for canvas-rendered paths, which have no element of their own.
|
|
242
|
+
*/
|
|
243
|
+
export function indiaBoundaryElements(
|
|
244
|
+
layer: LeafletGeoJSON<IndiaBoundaryProperties>,
|
|
245
|
+
): Element[] {
|
|
246
|
+
const out: Element[] = [];
|
|
247
|
+
layer.eachLayer((child) => {
|
|
248
|
+
const el = (child as Path).getElement?.();
|
|
249
|
+
if (el) out.push(el);
|
|
250
|
+
});
|
|
251
|
+
return out;
|
|
252
|
+
}
|
package/src/react.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useMap } from "react-leaflet";
|
|
3
|
+
import { indiaBoundaryLayer, type IndiaBoundaryLayerOptions } from "./leaflet.js";
|
|
4
|
+
|
|
5
|
+
export type IndiaBoundaryProps = IndiaBoundaryLayerOptions;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* react-leaflet wrapper. Drop it inside a `<MapContainer>`, anywhere below the
|
|
9
|
+
* `<TileLayer>`:
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* import { IndiaBoundary } from "leaflet-india-boundary/react";
|
|
13
|
+
*
|
|
14
|
+
* <MapContainer center={[22, 79]} zoom={5}>
|
|
15
|
+
* <TileLayer url="https://tile.openstreetmap.org/{z}/{x}/{y}.png" />
|
|
16
|
+
* <IndiaBoundary />
|
|
17
|
+
* </MapContainer>
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* If your app filters its tile pane to build a dark basemap, pass the same filter
|
|
21
|
+
* so the overlay is transformed with it — see `tileFilter` on
|
|
22
|
+
* {@link IndiaBoundaryLayerOptions}, which is the one option worth reading about
|
|
23
|
+
* before you need it:
|
|
24
|
+
*
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <IndiaBoundary tileFilter="invert(1) hue-rotate(192deg) saturate(0.45)" />
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Renders no DOM of its own — the layer is Leaflet's, so React only owns its
|
|
30
|
+
* lifetime. Note this file is deliberately `.ts`, not `.tsx`: the component
|
|
31
|
+
* returns `null`, so the package needs no JSX transform to build.
|
|
32
|
+
*/
|
|
33
|
+
export function IndiaBoundary(props: IndiaBoundaryProps = {}): null {
|
|
34
|
+
const map = useMap();
|
|
35
|
+
|
|
36
|
+
// Every option is a primitive, so they can be listed individually instead of
|
|
37
|
+
// reaching for a deep-equality hook or re-creating the layer whenever the
|
|
38
|
+
// caller passes a fresh object literal (which, in JSX, is every render).
|
|
39
|
+
const {
|
|
40
|
+
attribution,
|
|
41
|
+
className,
|
|
42
|
+
color,
|
|
43
|
+
colorWide,
|
|
44
|
+
interactive,
|
|
45
|
+
minZoom,
|
|
46
|
+
opacity,
|
|
47
|
+
pane,
|
|
48
|
+
tileFilter,
|
|
49
|
+
wideZoom,
|
|
50
|
+
} = props;
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const layer = indiaBoundaryLayer({
|
|
54
|
+
attribution,
|
|
55
|
+
className,
|
|
56
|
+
color,
|
|
57
|
+
colorWide,
|
|
58
|
+
interactive,
|
|
59
|
+
minZoom,
|
|
60
|
+
opacity,
|
|
61
|
+
pane,
|
|
62
|
+
tileFilter,
|
|
63
|
+
wideZoom,
|
|
64
|
+
});
|
|
65
|
+
layer.addTo(map);
|
|
66
|
+
return () => {
|
|
67
|
+
layer.remove();
|
|
68
|
+
};
|
|
69
|
+
}, [
|
|
70
|
+
map,
|
|
71
|
+
attribution,
|
|
72
|
+
className,
|
|
73
|
+
color,
|
|
74
|
+
colorWide,
|
|
75
|
+
interactive,
|
|
76
|
+
minZoom,
|
|
77
|
+
opacity,
|
|
78
|
+
pane,
|
|
79
|
+
tileFilter,
|
|
80
|
+
wideZoom,
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export {
|
|
87
|
+
INDIA_BOUNDARY_CLASS,
|
|
88
|
+
INDIA_BOUNDARY_PANE,
|
|
89
|
+
INDIA_BOUNDARY_WIDE_CLASS,
|
|
90
|
+
indiaBoundaryLayer,
|
|
91
|
+
type IndiaBoundaryLayerOptions,
|
|
92
|
+
} from "./leaflet.js";
|
package/src/style.css
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* leaflet-india-boundary — OPTIONAL stylesheet.
|
|
3
|
+
*
|
|
4
|
+
* You do not need this file. The layer sets colour, width, opacity and join as
|
|
5
|
+
* SVG attributes, so `indiaBoundaryLayer().addTo(map)` already draws a border
|
|
6
|
+
* that matches the basemap. Import this only if you would rather drive the
|
|
7
|
+
* styling from CSS than from JavaScript options.
|
|
8
|
+
*
|
|
9
|
+
* import "leaflet-india-boundary/style.css";
|
|
10
|
+
*
|
|
11
|
+
* Be aware of the precedence if you do: a CSS `stroke` beats the SVG `stroke`
|
|
12
|
+
* attribute Leaflet writes, so any colour rule here silently wins over the
|
|
13
|
+
* `color` / `colorWide` options. That is the point of the file, but it is worth
|
|
14
|
+
* knowing before you spend an afternoon wondering why `color` does nothing.
|
|
15
|
+
*
|
|
16
|
+
* One rule you must not add: `stroke-linejoin`. The layer sets `lineJoin: bevel`
|
|
17
|
+
* as an SVG attribute to match carto, and a CSS property would override it.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
.leaflet-india-boundary {
|
|
21
|
+
/* Belt and braces. The layer also passes `interactive: false`, which keeps
|
|
22
|
+
Leaflet from marking the paths as pointer targets, so clicks reach the
|
|
23
|
+
markers underneath. */
|
|
24
|
+
pointer-events: none;
|
|
25
|
+
|
|
26
|
+
/* CSS route to the tile-filter fix, for apps that would rather set a variable
|
|
27
|
+
than pass `tileFilter` in JS. Set it on your map container:
|
|
28
|
+
|
|
29
|
+
.leaflet-container {
|
|
30
|
+
--india-boundary-filter: invert(1) hue-rotate(192deg) saturate(0.45);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Use one route or the other, not both — `tileFilter` filters a dedicated pane,
|
|
34
|
+
and combining the two filters the stroke twice.
|
|
35
|
+
|
|
36
|
+
Why this exists at all: if you filter `.leaflet-tile-pane` to turn OSM's light
|
|
37
|
+
tiles into a dark basemap, the tiles on screen are no longer the colours OSM
|
|
38
|
+
rendered, while this overlay sits in an unfiltered pane. carto's purple applied
|
|
39
|
+
raw here lands nowhere near the purple actually visible. Putting the overlay
|
|
40
|
+
through the same filter is what makes them match. See the README. */
|
|
41
|
+
filter: var(--india-boundary-filter, none);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* carto's own values, for reference and for overriding. Uncomment to take control
|
|
46
|
+
* of the colours from CSS.
|
|
47
|
+
*
|
|
48
|
+
* These are openstreetmap-carto's `@admin-boundaries` and `@admin-boundaries-wide`
|
|
49
|
+
* verbatim from `style/admin.mss`, which is the whole trick: the border reads as
|
|
50
|
+
* part of the map because it is the exact colour the map draws every other national
|
|
51
|
+
* border in.
|
|
52
|
+
*
|
|
53
|
+
* Do not add a white line underneath. carto has one, but it is a `comp-op: darken`
|
|
54
|
+
* device for hiding the seam where two countries' borders overlap — not a visible
|
|
55
|
+
* casing. On a single overlay there is no seam to hide and it just looks wrong.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/*
|
|
59
|
+
.leaflet-india-boundary {
|
|
60
|
+
stroke: #8d618b;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.leaflet-india-boundary--wide {
|
|
64
|
+
stroke: #a37da1;
|
|
65
|
+
}
|
|
66
|
+
*/
|
package/src/style.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openstreetmap-carto's own `admin_level=2` border style, transcribed from its
|
|
3
|
+
* `style/admin.mss` (the `::wideline` attachment) rather than picked by eye.
|
|
4
|
+
*
|
|
5
|
+
* Matching these exactly is most of what stops the overlay reading as something
|
|
6
|
+
* drawn *on top of* the basemap instead of part of it. A border that is a
|
|
7
|
+
* slightly different purple, or a pixel too heavy, looks like an annotation. The
|
|
8
|
+
* same border in the basemap's own colour and weight looks like the map.
|
|
9
|
+
*
|
|
10
|
+
* Nothing in this module imports Leaflet, or anything else. It is here so that a
|
|
11
|
+
* MapLibre, OpenLayers or deck.gl user gets the same values without taking a
|
|
12
|
+
* mapping dependency they do not use.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** carto's `@admin-boundaries`. Applies below {@link OSM_ADMIN_BOUNDARY_WIDE_ZOOM}. */
|
|
16
|
+
export const OSM_ADMIN_BOUNDARY_COLOR = "#8d618b";
|
|
17
|
+
|
|
18
|
+
/** carto's `@admin-boundaries-wide`, which it swaps in from zoom 10. */
|
|
19
|
+
export const OSM_ADMIN_BOUNDARY_COLOR_WIDE = "#a37da1";
|
|
20
|
+
|
|
21
|
+
/** The zoom at which carto swaps `@admin-boundaries` for the lighter wide colour. */
|
|
22
|
+
export const OSM_ADMIN_BOUNDARY_WIDE_ZOOM = 10;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* carto's country-border rule starts at `[zoom >= 4]`, so below that the basemap
|
|
26
|
+
* draws no national boundaries at all — and neither should this overlay. On a
|
|
27
|
+
* borderless world map it is a lone squiggle floating over central Asia, attached
|
|
28
|
+
* to nothing. See `isOsmAdminBoundaryVisible`.
|
|
29
|
+
*/
|
|
30
|
+
export const OSM_ADMIN_BOUNDARY_MIN_ZOOM = 4;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* carto's width table ends here. Above it the z14 width carries on, which is what
|
|
34
|
+
* carto does too.
|
|
35
|
+
*/
|
|
36
|
+
export const OSM_ADMIN_BOUNDARY_MAX_TABLE_ZOOM = 14;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* carto's own stroke opacity for national borders.
|
|
40
|
+
*
|
|
41
|
+
* Read the note on `tileFilter` in the README before overriding this. Short
|
|
42
|
+
* version: carto composites this 0.5 against its light land fill (`#f2efe9`)
|
|
43
|
+
* *inside the tile*. If your app puts a CSS filter over its tile pane, an overlay
|
|
44
|
+
* drawn at 0.5 on the unfiltered side of that filter lands roughly three times
|
|
45
|
+
* too dark and disappears.
|
|
46
|
+
*/
|
|
47
|
+
export const OSM_ADMIN_BOUNDARY_OPACITY = 0.5;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Stroke width by zoom, from carto's `::wideline` attachment.
|
|
51
|
+
*
|
|
52
|
+
* carto pairs this with a white "background" line, which is *not* a visible
|
|
53
|
+
* casing — it is a `comp-op: darken` trick for hiding the seams where two
|
|
54
|
+
* countries' borders overlap. Do not draw one. On a single overlay there is
|
|
55
|
+
* nothing to hide, and a white line under a purple one just looks like a mistake.
|
|
56
|
+
*/
|
|
57
|
+
export const OSM_ADMIN_BOUNDARY_WIDTH: Readonly<Record<number, number>> =
|
|
58
|
+
Object.freeze({
|
|
59
|
+
4: 1.2,
|
|
60
|
+
5: 1.5,
|
|
61
|
+
6: 1.8,
|
|
62
|
+
7: 2.2,
|
|
63
|
+
8: 3,
|
|
64
|
+
9: 3.5,
|
|
65
|
+
10: 4.5,
|
|
66
|
+
11: 5,
|
|
67
|
+
12: 6,
|
|
68
|
+
13: 7,
|
|
69
|
+
14: 8,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/** Every carto-derived value in one object, for destructuring or spreading. */
|
|
73
|
+
export const OSM_ADMIN_STYLE = Object.freeze({
|
|
74
|
+
color: OSM_ADMIN_BOUNDARY_COLOR,
|
|
75
|
+
colorWide: OSM_ADMIN_BOUNDARY_COLOR_WIDE,
|
|
76
|
+
wideZoom: OSM_ADMIN_BOUNDARY_WIDE_ZOOM,
|
|
77
|
+
minZoom: OSM_ADMIN_BOUNDARY_MIN_ZOOM,
|
|
78
|
+
maxTableZoom: OSM_ADMIN_BOUNDARY_MAX_TABLE_ZOOM,
|
|
79
|
+
opacity: OSM_ADMIN_BOUNDARY_OPACITY,
|
|
80
|
+
width: OSM_ADMIN_BOUNDARY_WIDTH,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/** Overrides accepted by the resolvers below and by every layer factory. */
|
|
84
|
+
export interface OsmAdminBoundaryStyleOptions {
|
|
85
|
+
/** Stroke colour below `wideZoom`. Defaults to carto's `@admin-boundaries`. */
|
|
86
|
+
color?: string;
|
|
87
|
+
/** Stroke colour from `wideZoom` up. Defaults to carto's `@admin-boundaries-wide`. */
|
|
88
|
+
colorWide?: string;
|
|
89
|
+
/** Zoom at which `colorWide` takes over. Defaults to 10, as carto does. */
|
|
90
|
+
wideZoom?: number;
|
|
91
|
+
/** Below this zoom the boundary is not drawn. Defaults to 4, as carto does. */
|
|
92
|
+
minZoom?: number;
|
|
93
|
+
/** Stroke opacity. Defaults to carto's 0.5 — but see `tileFilter`. */
|
|
94
|
+
opacity?: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** carto's width for a zoom, clamped to the ends of its table. */
|
|
98
|
+
export function osmAdminBoundaryWidth(zoom: number): number {
|
|
99
|
+
const z = Math.min(
|
|
100
|
+
OSM_ADMIN_BOUNDARY_MAX_TABLE_ZOOM,
|
|
101
|
+
Math.max(OSM_ADMIN_BOUNDARY_MIN_ZOOM, Math.round(zoom)),
|
|
102
|
+
);
|
|
103
|
+
return OSM_ADMIN_BOUNDARY_WIDTH[z] ?? OSM_ADMIN_BOUNDARY_WIDTH[OSM_ADMIN_BOUNDARY_MIN_ZOOM];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** carto's border colour for a zoom. */
|
|
107
|
+
export function osmAdminBoundaryColor(
|
|
108
|
+
zoom: number,
|
|
109
|
+
options: OsmAdminBoundaryStyleOptions = {},
|
|
110
|
+
): string {
|
|
111
|
+
const wideZoom = options.wideZoom ?? OSM_ADMIN_BOUNDARY_WIDE_ZOOM;
|
|
112
|
+
return zoom >= wideZoom
|
|
113
|
+
? (options.colorWide ?? OSM_ADMIN_BOUNDARY_COLOR_WIDE)
|
|
114
|
+
: (options.color ?? OSM_ADMIN_BOUNDARY_COLOR);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Whether the basemap draws national borders at this zoom, and so whether we should. */
|
|
118
|
+
export function isOsmAdminBoundaryVisible(
|
|
119
|
+
zoom: number,
|
|
120
|
+
options: OsmAdminBoundaryStyleOptions = {},
|
|
121
|
+
): boolean {
|
|
122
|
+
return zoom >= (options.minZoom ?? OSM_ADMIN_BOUNDARY_MIN_ZOOM);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A resolved stroke, ready to hand to whatever renderer you are using. */
|
|
126
|
+
export interface ResolvedBoundaryStyle {
|
|
127
|
+
color: string;
|
|
128
|
+
width: number;
|
|
129
|
+
opacity: number;
|
|
130
|
+
visible: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The whole style for one zoom level, in renderer-neutral terms.
|
|
135
|
+
*
|
|
136
|
+
* ```js
|
|
137
|
+
* const { color, width, visible } = osmAdminBoundaryStyle(map.getZoom());
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export function osmAdminBoundaryStyle(
|
|
141
|
+
zoom: number,
|
|
142
|
+
options: OsmAdminBoundaryStyleOptions = {},
|
|
143
|
+
): ResolvedBoundaryStyle {
|
|
144
|
+
return {
|
|
145
|
+
color: osmAdminBoundaryColor(zoom, options),
|
|
146
|
+
width: osmAdminBoundaryWidth(zoom),
|
|
147
|
+
opacity: options.opacity ?? OSM_ADMIN_BOUNDARY_OPACITY,
|
|
148
|
+
visible: isOsmAdminBoundaryVisible(zoom, options),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* carto's width table as a MapLibre / Mapbox style expression, for
|
|
154
|
+
* `paint: { "line-width": … }`.
|
|
155
|
+
*
|
|
156
|
+
* `interpolate` rather than `step` on purpose: carto's table is per-integer-zoom
|
|
157
|
+
* because raster tiles only exist at integer zooms, but a vector map renders the
|
|
158
|
+
* fractional zooms in between, and stepping through them makes the line visibly
|
|
159
|
+
* jump. Interpolating hits carto's exact widths at every integer zoom and behaves
|
|
160
|
+
* sanely between them.
|
|
161
|
+
*/
|
|
162
|
+
export function osmAdminBoundaryWidthExpression(): unknown[] {
|
|
163
|
+
const stops = Object.keys(OSM_ADMIN_BOUNDARY_WIDTH)
|
|
164
|
+
.map(Number)
|
|
165
|
+
.sort((a, b) => a - b)
|
|
166
|
+
.flatMap((z) => [z, OSM_ADMIN_BOUNDARY_WIDTH[z]]);
|
|
167
|
+
return ["interpolate", ["linear"], ["zoom"], ...stops];
|
|
168
|
+
}
|