@metsa/isom-maplibre 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 +21 -0
- package/README.md +142 -0
- package/dist/icons.json +7 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +45 -0
- package/dist/style.geojson.json +1807 -0
- package/dist/style.json +3353 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Malthe Poulsen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# isom-maplibre
|
|
2
|
+
|
|
3
|
+
A [MapLibre GL](https://maplibre.org) style that renders orienteering maps per
|
|
4
|
+
[ISOM 2017-2](https://orienteering.sport/iof/mapping/), the IOF International
|
|
5
|
+
Specification for Orienteering Maps, at 1:10,000.
|
|
6
|
+
|
|
7
|
+
Every line width, dash, pattern and symbol is computed from the ISOM dimensions
|
|
8
|
+
(mm at 1:15,000, enlarged ×1.5 and converted at 96 dpi), uses the ISOM colours,
|
|
9
|
+
and is stacked in the ISOM colour order. The dimensions are cross-checked against
|
|
10
|
+
the [OpenOrienteering Mapper](https://www.openorienteering.org/apps/mapper/)
|
|
11
|
+
ISOM 2017-2 symbol set.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install @metsa/isom-maplibre maplibre-gl
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Releases are published to npm with provenance.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### In-memory GeoJSON
|
|
24
|
+
|
|
25
|
+
`isomGeojsonStyle()` returns the GeoJSON style (one GeoJSON source per table,
|
|
26
|
+
no sprite) with your data attached, and `registerIsomIcons()` supplies the
|
|
27
|
+
pattern and symbol images at runtime:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import maplibregl from "maplibre-gl";
|
|
31
|
+
import { isomGeojsonStyle, registerIsomIcons } from "@metsa/isom-maplibre";
|
|
32
|
+
|
|
33
|
+
const map = new maplibregl.Map({
|
|
34
|
+
container: "map",
|
|
35
|
+
style: isomGeojsonStyle({ contours, water, paths }), // FeatureCollections, lng/lat
|
|
36
|
+
});
|
|
37
|
+
registerIsomIcons(map);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Tables you leave out start empty; fill them later with
|
|
41
|
+
`map.getSource(table).setData(featureCollection)`. `DETAIL_TABLES` lists them.
|
|
42
|
+
The style without data is also exported as
|
|
43
|
+
`@metsa/isom-maplibre/style.geojson.json`.
|
|
44
|
+
|
|
45
|
+
### Vector tiles
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import style from "@metsa/isom-maplibre/style.json";
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The style reads one vector source per table from `/tiles/<table>` (TileJSON)
|
|
52
|
+
and its images from the sprite `/sprite/isom`. Point those URLs at your server
|
|
53
|
+
and build the sprite from `ICONS` (SVGs keyed by image id), or pass `-sprites`
|
|
54
|
+
to the generator (see [Development](#development)). Two optional sources serve
|
|
55
|
+
large tile sets:
|
|
56
|
+
|
|
57
|
+
- `overview`: one source whose layers carry `vegetation_areas`, `water`,
|
|
58
|
+
`paths` and `manmade`, drawn below zoom 13 in place of the per-table sources.
|
|
59
|
+
- `coverage`: polygons outlining where data exists. From zoom 10 they are white
|
|
60
|
+
paper under the map, so a basemap merged underneath never shows through; below
|
|
61
|
+
zoom 10 they are a translucent brown patch marking where maps are.
|
|
62
|
+
|
|
63
|
+
Every layer except the background carries `metadata` for selecting layers
|
|
64
|
+
without parsing ids: `isom:pass` (`detail`, `overview` or `coverage`), and on
|
|
65
|
+
symbol layers `isom:code` and `isom:group` (the ISOM colour group). The style's
|
|
66
|
+
own `metadata["isom:tables"]` lists the tables in definition order.
|
|
67
|
+
|
|
68
|
+
## Data
|
|
69
|
+
|
|
70
|
+
Each feature carries a string `isom_code` property. Codes without a symbol stay
|
|
71
|
+
invisible.
|
|
72
|
+
|
|
73
|
+
| Table | Geometry | `isom_code` |
|
|
74
|
+
|--------------------|-----------------|----------------------------------------------------------------------|
|
|
75
|
+
| `contours` | lines | 101.000, 101.001 (slope line), 102.000, 103.000, 104.000, 105.000 |
|
|
76
|
+
| `cliffs` | lines, polygons | 201.000, 202.000, 206.000 |
|
|
77
|
+
| `knolls_points` | points | 109.000, 111.000 |
|
|
78
|
+
| `vegetation_areas` | polygons, lines | 401.000 to 410.000, 412.000, 413.000, 415.000 |
|
|
79
|
+
| `water` | lines, polygons | 301.000, 302.000, 304.000, 305.000, 306.000, 308.000 |
|
|
80
|
+
| `paths` | lines | 502.000 to 507.000 |
|
|
81
|
+
| `manmade` | lines, polygons | 501.000, 509.000, 510.000, 511.000, 515.000, 516.000, 520.000, 521.000, 521.001 (large building: outline and 65% infill), 529.000 |
|
|
82
|
+
|
|
83
|
+
A 101.001 slope line is a two-point line from the contour downhill; the symbol
|
|
84
|
+
takes its bearing from it. Coordinates are lng/lat, as for any MapLibre source.
|
|
85
|
+
|
|
86
|
+
## Scale
|
|
87
|
+
|
|
88
|
+
Up to zoom 15 every dimension is constant; above it the whole map magnifies ×2
|
|
89
|
+
per zoom level, which keeps ISOM proportions intact. Zoom 15 is true 1:10,000
|
|
90
|
+
at about 56° latitude. At latitude φ the true-scale zoom is
|
|
91
|
+
`log2(156543.03 · cos φ / 2.6458)`.
|
|
92
|
+
|
|
93
|
+
## Limitations
|
|
94
|
+
|
|
95
|
+
MapLibre cannot draw some ISOM ornaments, so these symbols are simplified:
|
|
96
|
+
|
|
97
|
+
- Plain line without its ornament: 104 tags, 105 dots, 201 tags, 510 pylon
|
|
98
|
+
bars, 515 dots, 516 tags, 529 tick pairs.
|
|
99
|
+
- Flat fill without its pattern: 402 and 404 holes, 412 dots, 413 dot rows.
|
|
100
|
+
- Dashes are not balanced per feature as ISOM asks.
|
|
101
|
+
- The white core of 509 and 511 masks what lies beneath it.
|
|
102
|
+
- Fill patterns (308, 407, 409) keep their density above zoom 15.
|
|
103
|
+
- The map must be displayed north-up for the north-oriented patterns to be
|
|
104
|
+
correct.
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
[`isom.yaml`](isom.yaml) defines the whole style: scale, palette (referenced
|
|
109
|
+
through YAML anchors), images, and the symbol stack. It is validated by
|
|
110
|
+
[`isom.schema.json`](isom.schema.json), which editors with the YAML language
|
|
111
|
+
server pick up automatically; the generator enforces the schema too, plus the
|
|
112
|
+
cross-references it cannot express (palette colours, table and image names).
|
|
113
|
+
`src/style.json`, `src/style.geojson.json` and `src/icons.json` are generated
|
|
114
|
+
from it:
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
go generate ./... # regenerate src/
|
|
118
|
+
go test ./... # schema, ISOM dimension and colour-order checks
|
|
119
|
+
npm run build && npm test
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The generator is also usable directly, for example to write a sprite directory
|
|
123
|
+
for a tile server. It reads the definition embedded in the module unless
|
|
124
|
+
`-spec` names another file:
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
go run github.com/MetsaApp/isom-maplibre/cmd/genstyle@latest \
|
|
128
|
+
-style style.json -geojson-style style.geojson.json -icons icons.json -sprites sprites/
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Go programs get the parsed definition from `isomstyle.Default()` in
|
|
132
|
+
`github.com/MetsaApp/isom-maplibre/pkg/isomstyle` (`Load` and `Parse` take
|
|
133
|
+
another one), and render it with `Style`, `GeojsonStyle` and `Icons`.
|
|
134
|
+
|
|
135
|
+
Commits follow [Conventional Commits](https://www.conventionalcommits.org);
|
|
136
|
+
release-please opens the release PR, and merging it tags the release and
|
|
137
|
+
publishes the package.
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT. ISOM is a specification of the International Orienteering Federation;
|
|
142
|
+
this project is not affiliated with the IOF.
|
package/dist/icons.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"isom:111": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"5\" height=\"3\" viewBox=\"0 0 5 3\"><path d=\"M0.742 0.621A1.758 1.758 0 0 0 4.258 0.621\" fill=\"none\" stroke=\"#D15C00\" stroke-width=\"1.02\"/></svg>",
|
|
3
|
+
"isom:308": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"4\" height=\"17\" viewBox=\"0 0 4 17\"><path d=\"M0 0.85H4M0 2.55H4M0 4.25H4M0 5.95H4M0 7.65H4M0 9.35H4M0 11.05H4M0 12.75H4M0 14.45H4M0 16.15H4\" fill=\"none\" stroke=\"#00FFFF\" stroke-width=\"0.567\"/></svg>",
|
|
4
|
+
"isom:407": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"62\" height=\"4\" viewBox=\"0 0 62 4\"><path d=\"M2.385 0V4M7.154 0V4M11.923 0V4M16.692 0V4M21.462 0V4M26.231 0V4M31 0V4M35.769 0V4M40.538 0V4M45.308 0V4M50.077 0V4M54.846 0V4M59.615 0V4\" fill=\"none\" stroke=\"#3DFF17\" stroke-width=\"0.68\"/></svg>",
|
|
5
|
+
"isom:409": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"31\" height=\"4\" viewBox=\"0 0 31 4\"><path d=\"M1.192 0V4M3.577 0V4M5.962 0V4M8.346 0V4M10.731 0V4M13.115 0V4M15.5 0V4M17.885 0V4M20.269 0V4M22.654 0V4M25.038 0V4M27.423 0V4M29.808 0V4\" fill=\"none\" stroke=\"#3DFF17\" stroke-width=\"0.794\"/></svg>",
|
|
6
|
+
"isom:slope": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"3\" height=\"1\" viewBox=\"0 0 3 1\"><path d=\"M0.167 0.5H2.833\" stroke=\"#D15C00\" stroke-width=\"0.794\"/></svg>"
|
|
7
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FeatureCollection } from "geojson";
|
|
2
|
+
import type { Map, StyleSpecification } from "maplibre-gl";
|
|
3
|
+
import geojsonStyle from "./style.geojson.json";
|
|
4
|
+
/** Pattern and point-symbol SVGs keyed by style image id ("isom:111"), sized
|
|
5
|
+
* in CSS px at 1:10,000. registerIsomIcons() rasterizes them for you. */
|
|
6
|
+
export declare const ICONS: Record<string, string>;
|
|
7
|
+
export type IsomTable = keyof typeof geojsonStyle.sources;
|
|
8
|
+
/** Source layers the style expects, in definition order; each feature carries
|
|
9
|
+
* a string `isom_code` property ("401.000", "509.000", ...) selecting the ISOM
|
|
10
|
+
* symbol. */
|
|
11
|
+
export declare const DETAIL_TABLES: readonly IsomTable[];
|
|
12
|
+
/** The generated GeoJSON style (one source per table, the full-detail symbol
|
|
13
|
+
* pass at every zoom, no sprite: registerIsomIcons supplies the images) with
|
|
14
|
+
* the given FeatureCollections attached. Tables left out start empty. */
|
|
15
|
+
export declare function isomGeojsonStyle(data?: Partial<Record<IsomTable, FeatureCollection>>): StyleSpecification;
|
|
16
|
+
/** Rasterizes the ISOM pattern/symbol images on demand at 2x (fill patterns
|
|
17
|
+
* and point symbols reference them as "isom:<id>"). Call once right after
|
|
18
|
+
* constructing the Map. */
|
|
19
|
+
export declare function registerIsomIcons(map: Map): void;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import geojsonStyle from "./style.geojson.json" with { type: "json" };
|
|
2
|
+
import icons from "./icons.json" with { type: "json" };
|
|
3
|
+
/** Pattern and point-symbol SVGs keyed by style image id ("isom:111"), sized
|
|
4
|
+
* in CSS px at 1:10,000. registerIsomIcons() rasterizes them for you. */
|
|
5
|
+
export const ICONS = icons;
|
|
6
|
+
/** Source layers the style expects, in definition order; each feature carries
|
|
7
|
+
* a string `isom_code` property ("401.000", "509.000", ...) selecting the ISOM
|
|
8
|
+
* symbol. */
|
|
9
|
+
export const DETAIL_TABLES = geojsonStyle.metadata["isom:tables"];
|
|
10
|
+
/** The generated GeoJSON style (one source per table, the full-detail symbol
|
|
11
|
+
* pass at every zoom, no sprite: registerIsomIcons supplies the images) with
|
|
12
|
+
* the given FeatureCollections attached. Tables left out start empty. */
|
|
13
|
+
export function isomGeojsonStyle(data) {
|
|
14
|
+
const style = structuredClone(geojsonStyle);
|
|
15
|
+
for (const t of DETAIL_TABLES) {
|
|
16
|
+
const fc = data?.[t];
|
|
17
|
+
if (fc)
|
|
18
|
+
style.sources[t].data = fc;
|
|
19
|
+
}
|
|
20
|
+
return style;
|
|
21
|
+
}
|
|
22
|
+
/** Rasterizes the ISOM pattern/symbol images on demand at 2x (fill patterns
|
|
23
|
+
* and point symbols reference them as "isom:<id>"). Call once right after
|
|
24
|
+
* constructing the Map. */
|
|
25
|
+
export function registerIsomIcons(map) {
|
|
26
|
+
const pending = new Set();
|
|
27
|
+
map.on("styleimagemissing", async ({ id }) => {
|
|
28
|
+
const svg = ICONS[id];
|
|
29
|
+
// styleimagemissing refires every frame until the image exists.
|
|
30
|
+
if (!svg || pending.has(id) || map.hasImage(id))
|
|
31
|
+
return;
|
|
32
|
+
pending.add(id);
|
|
33
|
+
const img = new Image();
|
|
34
|
+
img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
|
|
35
|
+
await img.decode();
|
|
36
|
+
const c = document.createElement("canvas");
|
|
37
|
+
c.width = img.width * 2;
|
|
38
|
+
c.height = img.height * 2;
|
|
39
|
+
const ctx = c.getContext("2d");
|
|
40
|
+
ctx.drawImage(img, 0, 0, c.width, c.height);
|
|
41
|
+
if (!map.hasImage(id)) {
|
|
42
|
+
map.addImage(id, ctx.getImageData(0, 0, c.width, c.height), { pixelRatio: 2 });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|