@versatiles/style 3.5.2 → 3.6.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 +110 -14
- package/dist/index.d.ts +9 -4
- package/dist/index.js +13 -3
- package/dist/index.test.js +50 -24
- package/dist/lib/decorator.js +4 -0
- package/dist/lib/random_color.d.ts +9 -0
- package/dist/lib/random_color.js +150 -0
- package/dist/lib/random_color.test.d.ts +1 -0
- package/dist/lib/random_color.test.js +68 -0
- package/dist/lib/recolor.d.ts +3 -2
- package/dist/lib/recolor.js +2 -1
- package/dist/lib/recolor.test.d.ts +13 -1
- package/dist/lib/recolor.test.js +23 -10
- package/dist/lib/shortbread/layers.js +4 -4
- package/dist/lib/style_builder.d.ts +8 -15
- package/dist/lib/style_builder.js +50 -39
- package/dist/lib/style_builder.test.js +9 -15
- package/dist/lib/style_guesser.d.ts +2 -0
- package/dist/lib/style_guesser.js +123 -0
- package/dist/lib/style_guesser.test.d.ts +1 -0
- package/dist/lib/style_guesser.test.js +57 -0
- package/dist/lib/types.d.ts +23 -7
- package/dist/style/colorful.d.ts +4 -4
- package/dist/style/colorful.js +3 -3
- package/dist/style/graybeard.js +2 -2
- package/dist/style/neutrino.d.ts +4 -4
- package/dist/style/neutrino.js +3 -3
- package/package.json +16 -15
package/README.MD
CHANGED
|
@@ -7,20 +7,20 @@ Programatically generates StyleJSON for MapLibre.
|
|
|
7
7
|
|
|
8
8
|

|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
# Styles
|
|
11
11
|
|
|
12
12
|
* Colorful - colorful, full featured map
|
|
13
13
|
* Neutrino - light basemap
|
|
14
14
|
* Graybeard - gray basemap
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# Download styles for versatiles.org
|
|
17
17
|
|
|
18
18
|
You can download the latest StyleJSONs from the [latest release](https://github.com/versatiles-org/versatiles-style/releases/latest/).
|
|
19
19
|
We provide each style with and without labels, and also in multiple languages.
|
|
20
20
|
|
|
21
21
|
Be aware that styles already include `tiles.versatiles.org` as source for tiles, fonts (glyphs) and icons (sprites). So you might want to update the URLs in the JSON.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
# Create styles in the frontend (web browser)
|
|
24
24
|
|
|
25
25
|
Download latest release:
|
|
26
26
|
|
|
@@ -35,19 +35,17 @@ Use it in:
|
|
|
35
35
|
<script src="maplibre-gl.js"></script>
|
|
36
36
|
<script src="versatiles-style.js"></script>
|
|
37
37
|
<script>
|
|
38
|
-
const
|
|
38
|
+
const styleBuilder = new VersaTilesStyle.Colorful();
|
|
39
|
+
styleBuilder.tilesUrl = ['tiles/{z}/{x}/{y}'];
|
|
40
|
+
|
|
39
41
|
const map = new maplibregl.Map({
|
|
40
42
|
container: 'map',
|
|
41
|
-
style:
|
|
42
|
-
glyphsUrl: baseUrl + 'fonts/{fontstack}/{range}.pbf',
|
|
43
|
-
spriteUrl: baseUrl + 'sprites/sprites',
|
|
44
|
-
tilesUrl: [baseUrl + 'tiles/{z}/{x}/{y}'],
|
|
45
|
-
})
|
|
43
|
+
style: styleBuilder.build()
|
|
46
44
|
});
|
|
47
45
|
<script>
|
|
48
46
|
```
|
|
49
47
|
|
|
50
|
-
|
|
48
|
+
# Create styles in the backend (Node.js)
|
|
51
49
|
|
|
52
50
|
Install `versatiles-style` via NPM:
|
|
53
51
|
|
|
@@ -58,11 +56,109 @@ npm install versatiles-style
|
|
|
58
56
|
Use it in Node.js:
|
|
59
57
|
|
|
60
58
|
```javascript
|
|
61
|
-
import {
|
|
62
|
-
let style =
|
|
63
|
-
|
|
59
|
+
import { Colorful } from 'versatiles-style';
|
|
60
|
+
let style = new Colorful();
|
|
61
|
+
style.language = 'de';
|
|
62
|
+
writeFileSync('style.json', JSON.stringify(style.build()));
|
|
64
63
|
```
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
# API
|
|
67
66
|
|
|
68
67
|
<!--- This chapter is generated automatically --->
|
|
68
|
+
|
|
69
|
+
## Interfaces
|
|
70
|
+
|
|
71
|
+
### Interface: `TileJSONSpecificationRaster`<a id="interface_tilejsonspecificationraster"></a>
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
interface {
|
|
75
|
+
attribution?: string;
|
|
76
|
+
bounds?: [number, number, number, number];
|
|
77
|
+
center?: [number, number];
|
|
78
|
+
description?: string;
|
|
79
|
+
fillzoom?: number;
|
|
80
|
+
format: "avif" | "jpg" | "png" | "webp";
|
|
81
|
+
grids?: string[];
|
|
82
|
+
legend?: string;
|
|
83
|
+
maxzoom?: number;
|
|
84
|
+
minzoom?: number;
|
|
85
|
+
name?: string;
|
|
86
|
+
scheme?: "xyz" | "tms";
|
|
87
|
+
template?: string;
|
|
88
|
+
tilejson?: "3.0.0";
|
|
89
|
+
tiles: string[];
|
|
90
|
+
type: "raster";
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Interface: `TileJSONSpecificationVector`<a id="interface_tilejsonspecificationvector"></a>
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
interface {
|
|
98
|
+
attribution?: string;
|
|
99
|
+
bounds?: [number, number, number, number];
|
|
100
|
+
center?: [number, number];
|
|
101
|
+
description?: string;
|
|
102
|
+
fillzoom?: number;
|
|
103
|
+
format: "pbf";
|
|
104
|
+
grids?: string[];
|
|
105
|
+
legend?: string;
|
|
106
|
+
maxzoom?: number;
|
|
107
|
+
minzoom?: number;
|
|
108
|
+
name?: string;
|
|
109
|
+
scheme?: "xyz" | "tms";
|
|
110
|
+
template?: string;
|
|
111
|
+
tilejson?: "3.0.0";
|
|
112
|
+
tiles: string[];
|
|
113
|
+
type: "vector";
|
|
114
|
+
vector_layers: VectorLayer[];
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Type Aliases
|
|
119
|
+
|
|
120
|
+
### Type: `TileJSONSpecification`<a id="type_tilejsonspecification"></a>
|
|
121
|
+
|
|
122
|
+
**Type:** <code>[TileJSONSpecificationRaster](#interface_tilejsonspecificationraster) | [TileJSONSpecificationVector](#interface_tilejsonspecificationvector)</code>
|
|
123
|
+
|
|
124
|
+
## Functions
|
|
125
|
+
|
|
126
|
+
### Method: `colorful(options)`
|
|
127
|
+
|
|
128
|
+
<sup><a href="https://github.com/versatiles-org/versatiles-style/blob/6f93c18/src/index.ts#L4">\[src]</a></sup>
|
|
129
|
+
|
|
130
|
+
**Parameters:**
|
|
131
|
+
|
|
132
|
+
* <code>options: StylemakerOptions<default></code> (optional)
|
|
133
|
+
|
|
134
|
+
**Returns:** <code>MaplibreStyle</code>
|
|
135
|
+
|
|
136
|
+
### Method: `graybeard(options)`
|
|
137
|
+
|
|
138
|
+
<sup><a href="https://github.com/versatiles-org/versatiles-style/blob/6f93c18/src/index.ts#L9">\[src]</a></sup>
|
|
139
|
+
|
|
140
|
+
**Parameters:**
|
|
141
|
+
|
|
142
|
+
* <code>options: StylemakerOptions<default></code> (optional)
|
|
143
|
+
|
|
144
|
+
**Returns:** <code>MaplibreStyle</code>
|
|
145
|
+
|
|
146
|
+
### Method: `guessStyle(spec)`
|
|
147
|
+
|
|
148
|
+
<sup><a href="https://github.com/versatiles-org/versatiles-style/blob/6f93c18/src/lib/style_guesser.ts#L12">\[src]</a></sup>
|
|
149
|
+
|
|
150
|
+
**Parameters:**
|
|
151
|
+
|
|
152
|
+
* <code>spec: [TileJSONSpecification](#type_tilejsonspecification)</code>
|
|
153
|
+
|
|
154
|
+
**Returns:** <code>MaplibreStyle</code>
|
|
155
|
+
|
|
156
|
+
### Method: `neutrino(options)`
|
|
157
|
+
|
|
158
|
+
<sup><a href="https://github.com/versatiles-org/versatiles-style/blob/6f93c18/src/index.ts#L14">\[src]</a></sup>
|
|
159
|
+
|
|
160
|
+
**Parameters:**
|
|
161
|
+
|
|
162
|
+
* <code>options: StylemakerOptions<default></code> (optional)
|
|
163
|
+
|
|
164
|
+
**Returns:** <code>MaplibreStyle</code>
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
1
|
+
import type { MaplibreStyle, StylemakerOptions } from './lib/types.js';
|
|
2
|
+
import Colorful from './style/colorful.js';
|
|
3
|
+
export declare function colorful(options?: StylemakerOptions<Colorful>): MaplibreStyle;
|
|
4
|
+
import Graybeard from './style/graybeard.js';
|
|
5
|
+
export declare function graybeard(options?: StylemakerOptions<Graybeard>): MaplibreStyle;
|
|
6
|
+
import Neutrino from './style/neutrino.js';
|
|
7
|
+
export declare function neutrino(options?: StylemakerOptions<Neutrino>): MaplibreStyle;
|
|
8
|
+
export type { TileJSONSpecification, TileJSONSpecificationRaster, TileJSONSpecificationVector } from './lib/types.js';
|
|
9
|
+
export { default as guessStyle } from './lib/style_guesser.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
|
|
1
|
+
import Colorful from './style/colorful.js';
|
|
2
|
+
export function colorful(options) {
|
|
3
|
+
return new Colorful().build(options);
|
|
4
|
+
}
|
|
5
|
+
import Graybeard from './style/graybeard.js';
|
|
6
|
+
export function graybeard(options) {
|
|
7
|
+
return new Graybeard().build(options);
|
|
8
|
+
}
|
|
9
|
+
import Neutrino from './style/neutrino.js';
|
|
10
|
+
export function neutrino(options) {
|
|
11
|
+
return new Neutrino().build(options);
|
|
12
|
+
}
|
|
13
|
+
export { default as guessStyle } from './lib/style_guesser.js';
|
package/dist/index.test.js
CHANGED
|
@@ -1,26 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
import * as
|
|
3
|
-
import StyleBuilder from './lib/style_builder.js';
|
|
1
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
2
|
+
import * as builders from './index.js';
|
|
4
3
|
describe('Style Builders', () => {
|
|
5
|
-
const
|
|
6
|
-
'Colorful',
|
|
7
|
-
'Graybeard',
|
|
8
|
-
'Neutrino',
|
|
4
|
+
const styles = [
|
|
5
|
+
{ name: 'Colorful', builder: builders.colorful },
|
|
6
|
+
{ name: 'Graybeard', builder: builders.graybeard },
|
|
7
|
+
{ name: 'Neutrino', builder: builders.neutrino },
|
|
9
8
|
];
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
});
|
|
15
|
-
Object.entries(builderClasses).forEach(([styleName, builderClass]) => {
|
|
16
|
-
it(`should create and test an instance of ${styleName}`, () => {
|
|
17
|
-
const builder = new builderClass();
|
|
18
|
-
expect(builder).toBeInstanceOf(StyleBuilder);
|
|
19
|
-
expect(typeof builder.name).toBe('string');
|
|
20
|
-
builder.baseUrl = 'https://example.org';
|
|
21
|
-
const style = builder.build();
|
|
9
|
+
styles.forEach(({ name, builder }) => {
|
|
10
|
+
it(`should create and test an instance of ${name}`, () => {
|
|
11
|
+
expect(typeof builder).toBe('function');
|
|
12
|
+
const style = builder({ baseUrl: 'https://example.org' });
|
|
22
13
|
expect(JSON.stringify(style).length).toBeGreaterThan(50000);
|
|
23
|
-
expect(style.name).toBe('versatiles-' +
|
|
14
|
+
expect(style.name).toBe('versatiles-' + name.toLowerCase());
|
|
24
15
|
expect(style.glyphs).toBe('https://example.org/assets/fonts/{fontstack}/{range}.pbf');
|
|
25
16
|
expect(style.sprite).toBe('https://example.org/assets/sprites/sprites');
|
|
26
17
|
expect(Object.keys(style.sources).join(',')).toBe('versatiles-shortbread');
|
|
@@ -29,9 +20,44 @@ describe('Style Builders', () => {
|
|
|
29
20
|
});
|
|
30
21
|
});
|
|
31
22
|
describe('Colorful', () => {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
23
|
+
const style = builders.colorful({
|
|
24
|
+
baseUrl: 'https://dev.null',
|
|
25
|
+
colors: { commercial: '#f00' },
|
|
26
|
+
});
|
|
36
27
|
expect(style.glyphs).toBe('https://dev.null/assets/fonts/{fontstack}/{range}.pbf');
|
|
28
|
+
const paint = style.layers.find(l => l.id === 'land-commercial')?.paint;
|
|
29
|
+
expect(paint).toBeDefined();
|
|
30
|
+
if (paint == null)
|
|
31
|
+
throw Error();
|
|
32
|
+
expect(paint).toHaveProperty('fill-color');
|
|
33
|
+
if (!('fill-color' in paint))
|
|
34
|
+
throw Error();
|
|
35
|
+
expect(paint['fill-color']).toBe('#ff0000');
|
|
36
|
+
});
|
|
37
|
+
describe('guessStyle', () => {
|
|
38
|
+
it('should build raster styles', () => {
|
|
39
|
+
const style = builders.guessStyle({
|
|
40
|
+
type: 'raster',
|
|
41
|
+
tiles: [],
|
|
42
|
+
format: 'avif',
|
|
43
|
+
});
|
|
44
|
+
expect(style).toStrictEqual({
|
|
45
|
+
layers: [{ id: 'raster', source: 'rasterSource', type: 'raster' }],
|
|
46
|
+
sources: { rasterSource: { format: 'avif', tilejson: '3.0.0', tiles: [], type: 'raster' } },
|
|
47
|
+
version: 8,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
it('should build vector styles', () => {
|
|
51
|
+
const style = builders.guessStyle({
|
|
52
|
+
type: 'vector',
|
|
53
|
+
tiles: [],
|
|
54
|
+
format: 'pbf',
|
|
55
|
+
vector_layers: [],
|
|
56
|
+
});
|
|
57
|
+
expect(style).toStrictEqual({
|
|
58
|
+
layers: [{ id: 'background', paint: { 'background-color': '#fff' }, type: 'background' }],
|
|
59
|
+
sources: { vectorSource: { format: 'pbf', tilejson: '3.0.0', tiles: [], type: 'vector', 'vector_layers': [] } },
|
|
60
|
+
'version': 8,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
37
63
|
});
|
package/dist/lib/decorator.js
CHANGED
|
@@ -9,6 +9,8 @@ export function decorate(layers, rules) {
|
|
|
9
9
|
const layerStyles = new Map();
|
|
10
10
|
// Iterate through the generated layer style rules
|
|
11
11
|
Object.entries(rules).forEach(([idDef, layerStyle]) => {
|
|
12
|
+
if (layerStyle == null)
|
|
13
|
+
return;
|
|
12
14
|
// Expand any braces in IDs and filter them through a RegExp if necessary
|
|
13
15
|
const ids = expandBraces(idDef).flatMap(id => {
|
|
14
16
|
if (!id.includes('*'))
|
|
@@ -41,6 +43,8 @@ export function decorate(layers, rules) {
|
|
|
41
43
|
// Function to process each style attribute for the layer
|
|
42
44
|
function processStyling(layer, styleRule) {
|
|
43
45
|
for (const [ruleKeyCamelCase, ruleValue] of Object.entries(styleRule)) {
|
|
46
|
+
if (ruleValue == null)
|
|
47
|
+
continue;
|
|
44
48
|
// CamelCase to not-camel-case
|
|
45
49
|
const ruleKey = ruleKeyCamelCase.replace(/[A-Z]/g, c => '-' + c.toLowerCase());
|
|
46
50
|
const propertyDefs = maplibreProperties.get(layer.type + '/' + ruleKey);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface RandomColorOptions {
|
|
2
|
+
seed?: string;
|
|
3
|
+
hue?: number | string;
|
|
4
|
+
opacity?: number;
|
|
5
|
+
luminosity?: number | string;
|
|
6
|
+
saturation?: number | string;
|
|
7
|
+
}
|
|
8
|
+
export type RandomColorFunction = (options?: RandomColorOptions) => string;
|
|
9
|
+
export default function randomColorGenerator(startSeed?: number | string): RandomColorFunction;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
export default function randomColorGenerator(startSeed) {
|
|
2
|
+
let seed = inputToSeed(startSeed);
|
|
3
|
+
const colorDictionary = initColorDictionary();
|
|
4
|
+
return randomColor;
|
|
5
|
+
function randomColor(options) {
|
|
6
|
+
options ??= {};
|
|
7
|
+
if (options.seed != null) {
|
|
8
|
+
seed = inputToSeed(options.seed);
|
|
9
|
+
}
|
|
10
|
+
options.opacity ??= 1;
|
|
11
|
+
const H = pickHue(options);
|
|
12
|
+
const S = pickSaturation(H, options);
|
|
13
|
+
const V = pickBrightness(H, S, options);
|
|
14
|
+
const hsl = HSVtoHSL([H, S, V]).map(v => v.toFixed(0));
|
|
15
|
+
if (options.opacity === 1) {
|
|
16
|
+
return `hsl(${hsl[0]},${hsl[1]}%,${hsl[2]}%)`;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
return `hsla(${hsl[0]},${hsl[1]}%,${hsl[2]}%,${options.opacity})`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function pickHue(options) {
|
|
23
|
+
let hue = randomWithin(getHueRange(options.hue));
|
|
24
|
+
if (hue < 0)
|
|
25
|
+
hue = 360 + hue;
|
|
26
|
+
return hue;
|
|
27
|
+
}
|
|
28
|
+
function pickSaturation(hue, options) {
|
|
29
|
+
if (options.hue === 'monochrome')
|
|
30
|
+
return 0;
|
|
31
|
+
if (options.luminosity === 'random')
|
|
32
|
+
return randomWithin([0, 100]);
|
|
33
|
+
const { saturationRange } = getColorInfo(hue);
|
|
34
|
+
let [sMin, sMax] = saturationRange;
|
|
35
|
+
if (options.saturation === 'strong')
|
|
36
|
+
return sMax;
|
|
37
|
+
switch (options.luminosity) {
|
|
38
|
+
case 'bright':
|
|
39
|
+
sMin = 55;
|
|
40
|
+
break;
|
|
41
|
+
case 'dark':
|
|
42
|
+
sMin = sMax - 10;
|
|
43
|
+
break;
|
|
44
|
+
case 'light':
|
|
45
|
+
sMax = 55;
|
|
46
|
+
break;
|
|
47
|
+
default:
|
|
48
|
+
}
|
|
49
|
+
return randomWithin([sMin, sMax]);
|
|
50
|
+
}
|
|
51
|
+
function pickBrightness(h, s, options) {
|
|
52
|
+
let bMin = getMinimumBrightness(h, s), bMax = 100;
|
|
53
|
+
switch (options.luminosity) {
|
|
54
|
+
case 'dark':
|
|
55
|
+
bMax = Math.min(100, bMin + 20);
|
|
56
|
+
break;
|
|
57
|
+
case 'light':
|
|
58
|
+
bMin = (bMax + bMin) / 2;
|
|
59
|
+
break;
|
|
60
|
+
case 'random':
|
|
61
|
+
bMin = 0;
|
|
62
|
+
bMax = 100;
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
}
|
|
66
|
+
return randomWithin([bMin, bMax]);
|
|
67
|
+
}
|
|
68
|
+
function getMinimumBrightness(h, s) {
|
|
69
|
+
const { lowerBounds } = getColorInfo(h);
|
|
70
|
+
for (let i = 0; i < lowerBounds.length - 1; i++) {
|
|
71
|
+
const [s1, v1] = lowerBounds[i];
|
|
72
|
+
const [s2, v2] = lowerBounds[i + 1];
|
|
73
|
+
if (s >= s1 && s <= s2) {
|
|
74
|
+
const m = (v2 - v1) / (s2 - s1), b = v1 - m * s1;
|
|
75
|
+
return m * s + b;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
function getHueRange(hue) {
|
|
81
|
+
if (typeof hue === 'number') {
|
|
82
|
+
if (hue < 360 && hue > 0)
|
|
83
|
+
return [hue, hue];
|
|
84
|
+
}
|
|
85
|
+
if (typeof hue === 'string') {
|
|
86
|
+
const color = colorDictionary[hue];
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
88
|
+
if (color?.hueRange)
|
|
89
|
+
return color.hueRange;
|
|
90
|
+
}
|
|
91
|
+
return [0, 360];
|
|
92
|
+
}
|
|
93
|
+
function getColorInfo(hue) {
|
|
94
|
+
// Maps red colors to make picking hue easier
|
|
95
|
+
if (hue >= 334 && hue <= 360)
|
|
96
|
+
hue -= 360;
|
|
97
|
+
for (const colorName in colorDictionary) {
|
|
98
|
+
const color = colorDictionary[colorName];
|
|
99
|
+
if (color.hueRange && hue >= color.hueRange[0] && hue <= color.hueRange[1]) {
|
|
100
|
+
return colorDictionary[colorName];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
throw Error('Color not found');
|
|
104
|
+
}
|
|
105
|
+
function randomWithin(range) {
|
|
106
|
+
//Seeded random algorithm from http://indiegamr.com/generate-repeatable-random-numbers-in-js/
|
|
107
|
+
const max = range[1] || 1;
|
|
108
|
+
const min = range[0] || 0;
|
|
109
|
+
seed = (seed * 9301 + 49297) % 233280;
|
|
110
|
+
const rnd = seed / 233280.0;
|
|
111
|
+
return Math.floor(min + rnd * (max - min));
|
|
112
|
+
}
|
|
113
|
+
function initColorDictionary() {
|
|
114
|
+
const dic = {};
|
|
115
|
+
const defineColor = (name, hueRange, lowerBounds) => {
|
|
116
|
+
const [greyest] = lowerBounds;
|
|
117
|
+
const colorful = lowerBounds[lowerBounds.length - 1];
|
|
118
|
+
dic[name] = {
|
|
119
|
+
hueRange,
|
|
120
|
+
lowerBounds,
|
|
121
|
+
saturationRange: [greyest[0], colorful[0]],
|
|
122
|
+
brightnessRange: [colorful[1], greyest[1]],
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
defineColor('monochrome', null, [[0, 0], [100, 0]]);
|
|
126
|
+
defineColor('red', [-26, 18], [[20, 100], [30, 92], [40, 89], [50, 85], [60, 78], [70, 70], [80, 60], [90, 55], [100, 50]]);
|
|
127
|
+
defineColor('orange', [18, 46], [[20, 100], [30, 93], [40, 88], [50, 86], [60, 85], [70, 70], [100, 70]]);
|
|
128
|
+
defineColor('yellow', [46, 62], [[25, 100], [40, 94], [50, 89], [60, 86], [70, 84], [80, 82], [90, 80], [100, 75]]);
|
|
129
|
+
defineColor('green', [62, 178], [[30, 100], [40, 90], [50, 85], [60, 81], [70, 74], [80, 64], [90, 50], [100, 40]]);
|
|
130
|
+
defineColor('blue', [178, 257], [[20, 100], [30, 86], [40, 80], [50, 74], [60, 60], [70, 52], [80, 44], [90, 39], [100, 35]]);
|
|
131
|
+
defineColor('purple', [257, 282], [[20, 100], [30, 87], [40, 79], [50, 70], [60, 65], [70, 59], [80, 52], [90, 45], [100, 42]]);
|
|
132
|
+
defineColor('pink', [282, 334], [[20, 100], [30, 90], [40, 86], [60, 84], [80, 80], [90, 75], [100, 73]]);
|
|
133
|
+
return dic;
|
|
134
|
+
}
|
|
135
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
136
|
+
function HSVtoHSL(hsv) {
|
|
137
|
+
const s = hsv[1] / 100, v = hsv[2] / 100, k = (2 - s) * v;
|
|
138
|
+
return [hsv[0], 100 * s * v / (k < 1 ? k : 2 - k), 100 * k / 2];
|
|
139
|
+
}
|
|
140
|
+
function inputToSeed(input) {
|
|
141
|
+
if (input == null)
|
|
142
|
+
return 0;
|
|
143
|
+
if (typeof input === 'number')
|
|
144
|
+
return input;
|
|
145
|
+
let i = 0;
|
|
146
|
+
for (let p = 0; p < input.length; p++)
|
|
147
|
+
i = (i * 0x101 + input.charCodeAt(p)) % 0x100000000;
|
|
148
|
+
return i;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import randomColorGenerator from './random_color.js';
|
|
2
|
+
describe('RandomColor', () => {
|
|
3
|
+
let randomColor;
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
randomColor = randomColorGenerator();
|
|
6
|
+
});
|
|
7
|
+
test('constructor initializes without errors', () => {
|
|
8
|
+
expect(randomColor).toBeDefined();
|
|
9
|
+
});
|
|
10
|
+
describe('randomColor method', () => {
|
|
11
|
+
test('returns a valid color string', () => {
|
|
12
|
+
const color = randomColor();
|
|
13
|
+
expect(isValidHSLA(color)).toBeTruthy();
|
|
14
|
+
});
|
|
15
|
+
test('returns correct color string for some test cases', () => {
|
|
16
|
+
expect(randomColor({ hue: 'red' })).toBe('hsl(343,65%,51%)');
|
|
17
|
+
expect(randomColor({ hue: 120 })).toBe('hsl(120,77%,32%)');
|
|
18
|
+
expect(randomColor({ luminosity: 'dark' })).toBe('hsl(135,96%,31%)');
|
|
19
|
+
expect(randomColor({ saturation: 'strong' })).toBe('hsl(193,100%,24%)');
|
|
20
|
+
expect(randomColor({ opacity: 0.5 })).toBe('hsla(242,55%,42%,0.5)');
|
|
21
|
+
expect(randomColor({ seed: 'testSeed' })).toBe('hsl(185,90%,23%)');
|
|
22
|
+
});
|
|
23
|
+
test('consistent color generation with a seed', () => {
|
|
24
|
+
const options = { seed: 'consistentSeed' };
|
|
25
|
+
const color1 = randomColor(options);
|
|
26
|
+
const color2 = randomColor(options);
|
|
27
|
+
expect(color1).toBe(color2);
|
|
28
|
+
});
|
|
29
|
+
test('different color generation without a seed', () => {
|
|
30
|
+
const color1 = randomColor();
|
|
31
|
+
const color2 = randomColor();
|
|
32
|
+
expect(color1).not.toBe(color2);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
function isValidHSLA(textColor) {
|
|
37
|
+
let match, h, s, l, a;
|
|
38
|
+
if (textColor.startsWith('hsla')) {
|
|
39
|
+
match = /^hsla\((\d+),(\d+)%,(\d+)%,([.0-9]+)\)$/.exec(textColor);
|
|
40
|
+
if (match == null)
|
|
41
|
+
return false;
|
|
42
|
+
[, h, s, l, a] = match;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
match = /^hsl\((\d+),(\d+)%,(\d+)%\)$/.exec(textColor);
|
|
46
|
+
if (match == null)
|
|
47
|
+
return false;
|
|
48
|
+
[, h, s, l] = match;
|
|
49
|
+
a = '1';
|
|
50
|
+
}
|
|
51
|
+
if (!check(h, 0, 360))
|
|
52
|
+
return false;
|
|
53
|
+
if (!check(s, 0, 100))
|
|
54
|
+
return false;
|
|
55
|
+
if (!check(l, 0, 100))
|
|
56
|
+
return false;
|
|
57
|
+
if (!check(a, 0, 1))
|
|
58
|
+
return false;
|
|
59
|
+
return true;
|
|
60
|
+
function check(textNumber, min, max) {
|
|
61
|
+
const value = parseFloat(textNumber);
|
|
62
|
+
if (value < min)
|
|
63
|
+
return false;
|
|
64
|
+
if (value > max)
|
|
65
|
+
return false;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
package/dist/lib/recolor.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type { RecolorOptions,
|
|
1
|
+
import type { RecolorOptions, StylemakerColors } from './types.js';
|
|
2
|
+
import type StyleBuilder from './style_builder.ts';
|
|
2
3
|
export declare function getDefaultRecolorFlags(): RecolorOptions;
|
|
3
|
-
export declare function recolor(colors:
|
|
4
|
+
export declare function recolor<Subclass extends StyleBuilder<Subclass>>(colors: StylemakerColors<Subclass>, opt?: RecolorOptions): void;
|
package/dist/lib/recolor.js
CHANGED
|
@@ -29,7 +29,8 @@ export function recolor(colors, opt) {
|
|
|
29
29
|
if ((opt.tint !== undefined) && (opt.tintColor !== undefined) && (opt.tint !== 0))
|
|
30
30
|
tint(opt.tint, Color(opt.tintColor));
|
|
31
31
|
function forEachColor(callback) {
|
|
32
|
-
|
|
32
|
+
for (const k in colors)
|
|
33
|
+
colors[k] = callback(colors[k]);
|
|
33
34
|
}
|
|
34
35
|
function invert() {
|
|
35
36
|
forEachColor(c => c.negate());
|
|
@@ -1 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import type { StyleRules, StyleRulesOptions } from './types.js';
|
|
2
|
+
import StyleBuilder from './style_builder.js';
|
|
3
|
+
export default class TestStyle extends StyleBuilder<TestStyle> {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
defaultFonts: {};
|
|
6
|
+
defaultColors: {
|
|
7
|
+
c0: string;
|
|
8
|
+
c1: string;
|
|
9
|
+
c2: string;
|
|
10
|
+
c3: string;
|
|
11
|
+
};
|
|
12
|
+
protected getStyleRules(_options: StyleRulesOptions<TestStyle>): StyleRules;
|
|
13
|
+
}
|
package/dist/lib/recolor.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getDefaultRecolorFlags, recolor } from './recolor.js';
|
|
2
|
-
import
|
|
2
|
+
import StyleBuilder from './style_builder.js';
|
|
3
3
|
describe('colorTransformer', () => {
|
|
4
4
|
describe('getDefaultRecolorFlags', () => {
|
|
5
5
|
it('should return the default color transformer flags', () => {
|
|
@@ -163,17 +163,30 @@ describe('colorTransformer', () => {
|
|
|
163
163
|
});
|
|
164
164
|
});
|
|
165
165
|
});
|
|
166
|
-
function string2colors(colorList) {
|
|
167
|
-
return Object.fromEntries(colorList.split(',')
|
|
168
|
-
.map((value, index) => ['color' + index, Color('#' + value)]));
|
|
169
|
-
}
|
|
170
166
|
function colors2string(colors) {
|
|
171
|
-
const colorArray = [
|
|
172
|
-
|
|
173
|
-
|
|
167
|
+
const colorArray = [
|
|
168
|
+
colors.c0,
|
|
169
|
+
colors.c1,
|
|
170
|
+
colors.c2,
|
|
171
|
+
colors.c3,
|
|
172
|
+
];
|
|
173
|
+
return colorArray.map(c => c.hexa().slice(1)).join(',');
|
|
174
|
+
}
|
|
175
|
+
export default class TestStyle extends StyleBuilder {
|
|
176
|
+
name = 'teststyle';
|
|
177
|
+
defaultFonts = {};
|
|
178
|
+
defaultColors = {
|
|
179
|
+
c0: '#FFAA5500',
|
|
180
|
+
c1: '#00FFAA55',
|
|
181
|
+
c2: '#5500FFAA',
|
|
182
|
+
c3: '#AA5500FF',
|
|
183
|
+
};
|
|
184
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
185
|
+
getStyleRules(_options) {
|
|
186
|
+
throw new Error('Method not implemented.');
|
|
174
187
|
}
|
|
175
|
-
return colorArray.join(',');
|
|
176
188
|
}
|
|
177
189
|
function getDefaultColors() {
|
|
178
|
-
|
|
190
|
+
const style = new TestStyle();
|
|
191
|
+
return style.getColors(style.defaultColors);
|
|
179
192
|
}
|
|
@@ -316,7 +316,7 @@ export default function getLayers(option) {
|
|
|
316
316
|
{
|
|
317
317
|
id: 'label-motorway-exit',
|
|
318
318
|
type: 'symbol',
|
|
319
|
-
'source-layer': 'street_labels_points',
|
|
319
|
+
'source-layer': 'street_labels_points', // docs say `streets_labels_points`, but layer is actually called `street_labels_points`
|
|
320
320
|
filter: ['==', 'kind', 'motorway_junction'],
|
|
321
321
|
layout: { 'text-field': '{ref}' },
|
|
322
322
|
// FIXME shield
|
|
@@ -394,7 +394,7 @@ export default function getLayers(option) {
|
|
|
394
394
|
},
|
|
395
395
|
// marking
|
|
396
396
|
{
|
|
397
|
-
id: 'marking-oneway',
|
|
397
|
+
id: 'marking-oneway', // streets → oneway
|
|
398
398
|
type: 'symbol',
|
|
399
399
|
'source-layer': 'streets',
|
|
400
400
|
filter: ['all',
|
|
@@ -411,7 +411,7 @@ export default function getLayers(option) {
|
|
|
411
411
|
},
|
|
412
412
|
},
|
|
413
413
|
{
|
|
414
|
-
id: 'marking-oneway-reverse',
|
|
414
|
+
id: 'marking-oneway-reverse', // oneway_reverse
|
|
415
415
|
type: 'symbol',
|
|
416
416
|
'source-layer': 'streets',
|
|
417
417
|
filter: ['all',
|
|
@@ -428,7 +428,7 @@ export default function getLayers(option) {
|
|
|
428
428
|
},
|
|
429
429
|
},
|
|
430
430
|
{
|
|
431
|
-
id: 'marking-bicycle',
|
|
431
|
+
id: 'marking-bicycle', // bicycle=designated or kind=cycleway
|
|
432
432
|
type: 'symbol',
|
|
433
433
|
'source-layer': 'streets',
|
|
434
434
|
filter: ['all',
|
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
import Color from 'color';
|
|
2
|
-
import type {
|
|
3
|
-
export default abstract class StyleBuilder {
|
|
2
|
+
import type { MaplibreStyle, StyleRules, StyleRulesOptions, StylemakerColorStrings, StylemakerColors, StylemakerFontStrings, StylemakerOptions } from './types.js';
|
|
3
|
+
export default abstract class StyleBuilder<Subclass extends StyleBuilder<Subclass>> {
|
|
4
4
|
#private;
|
|
5
|
-
baseUrl: string;
|
|
6
|
-
glyphsUrl: string;
|
|
7
|
-
spriteUrl: string;
|
|
8
|
-
tilesUrls: string[];
|
|
9
|
-
hideLabels: boolean;
|
|
10
|
-
languageSuffix: LanguageSuffix;
|
|
11
|
-
recolor: RecolorOptions;
|
|
12
5
|
abstract readonly name: string;
|
|
13
|
-
abstract
|
|
14
|
-
abstract
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
protected
|
|
18
|
-
protected abstract getStyleRules(
|
|
6
|
+
abstract readonly defaultColors: StylemakerColorStrings<Subclass>;
|
|
7
|
+
abstract readonly defaultFonts: StylemakerFontStrings<Subclass>;
|
|
8
|
+
build(options?: StylemakerOptions<Subclass>): MaplibreStyle;
|
|
9
|
+
getColors(colors: StylemakerColorStrings<Subclass>): StylemakerColors<Subclass>;
|
|
10
|
+
protected transformDefaultColors(callback: (color: Color) => Color): void;
|
|
11
|
+
protected abstract getStyleRules(options: StyleRulesOptions<Subclass>): StyleRules;
|
|
19
12
|
}
|
|
@@ -3,42 +3,46 @@ import getShortbreadTemplate from './shortbread/template.js';
|
|
|
3
3
|
import getShortbreadLayers from './shortbread/layers.js';
|
|
4
4
|
import { decorate } from './decorator.js';
|
|
5
5
|
import { getDefaultRecolorFlags, recolor } from './recolor.js';
|
|
6
|
+
import { deepClone } from './utils.js';
|
|
6
7
|
// Stylemaker class definition
|
|
7
8
|
export default class StyleBuilder {
|
|
8
|
-
baseUrl;
|
|
9
|
-
glyphsUrl = '/assets/fonts/{fontstack}/{range}.pbf';
|
|
10
|
-
spriteUrl = '/assets/sprites/sprites';
|
|
11
|
-
tilesUrls = ['/tiles/osm/{z}/{x}/{y}'];
|
|
12
|
-
hideLabels = false;
|
|
13
|
-
languageSuffix = '';
|
|
14
|
-
recolor = getDefaultRecolorFlags();
|
|
15
9
|
#sourceName = 'versatiles-shortbread';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
build(options) {
|
|
11
|
+
options ??= {};
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
13
|
+
const baseUrl = options.baseUrl ?? globalThis?.document?.location?.href ?? 'https://tiles.versatiles.org';
|
|
14
|
+
const glyphsUrl = options.glyphsUrl ?? '/assets/fonts/{fontstack}/{range}.pbf';
|
|
15
|
+
const spriteUrl = options.spriteUrl ?? '/assets/sprites/sprites';
|
|
16
|
+
const tilesUrls = options.tilesUrls ?? ['/tiles/osm/{z}/{x}/{y}'];
|
|
17
|
+
const hideLabels = options.hideLabels ?? false;
|
|
18
|
+
const languageSuffix = options.languageSuffix ?? '';
|
|
19
|
+
const recolorOptions = options.recolor ?? getDefaultRecolorFlags();
|
|
20
|
+
const colors = this.getColors(this.defaultColors);
|
|
21
|
+
if (options.colors) {
|
|
22
|
+
for (const key in options.colors)
|
|
23
|
+
colors[key] = Color(options.colors[key]);
|
|
22
24
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
// transform colors
|
|
26
|
+
recolor(colors, recolorOptions);
|
|
27
|
+
const fonts = deepClone(this.defaultColors);
|
|
28
|
+
if (options.fonts) {
|
|
29
|
+
for (const key in options.fonts) {
|
|
30
|
+
const fontName = options.fonts[key];
|
|
31
|
+
if (fontName != null)
|
|
32
|
+
fonts[key] = fontName;
|
|
33
|
+
}
|
|
25
34
|
}
|
|
26
|
-
}
|
|
27
|
-
build() {
|
|
28
35
|
// get empty shortbread style
|
|
29
36
|
const style = getShortbreadTemplate();
|
|
30
|
-
const
|
|
31
|
-
.map(([name, colorString]) => [name, Color(colorString)]));
|
|
32
|
-
// transform colors
|
|
33
|
-
recolor(colors, this.recolor);
|
|
34
|
-
// get layer style rules from child class
|
|
35
|
-
const layerStyleRules = this.getStyleRules({
|
|
37
|
+
const styleRuleOptions = {
|
|
36
38
|
colors,
|
|
37
|
-
fonts: this.
|
|
38
|
-
languageSuffix
|
|
39
|
-
}
|
|
39
|
+
fonts: deepClone(this.defaultFonts),
|
|
40
|
+
languageSuffix,
|
|
41
|
+
};
|
|
42
|
+
// get layer style rules from child class
|
|
43
|
+
const layerStyleRules = this.getStyleRules(styleRuleOptions);
|
|
40
44
|
// get shortbread layers
|
|
41
|
-
const layerDefinitions = getShortbreadLayers({ languageSuffix
|
|
45
|
+
const layerDefinitions = getShortbreadLayers({ languageSuffix });
|
|
42
46
|
let layers = layerDefinitions.map(layer => {
|
|
43
47
|
switch (layer.type) {
|
|
44
48
|
case 'background':
|
|
@@ -56,28 +60,35 @@ export default class StyleBuilder {
|
|
|
56
60
|
// apply layer rules
|
|
57
61
|
layers = decorate(layers, layerStyleRules);
|
|
58
62
|
// hide labels, if wanted
|
|
59
|
-
if (
|
|
63
|
+
if (hideLabels)
|
|
60
64
|
layers = layers.filter(l => l.type !== 'symbol');
|
|
61
65
|
style.layers = layers;
|
|
62
|
-
style.name = 'versatiles-' + this.name;
|
|
63
|
-
style.glyphs = resolveUrl(
|
|
64
|
-
style.sprite = resolveUrl(
|
|
66
|
+
style.name = 'versatiles-' + this.name.toLowerCase();
|
|
67
|
+
style.glyphs = resolveUrl(baseUrl, glyphsUrl);
|
|
68
|
+
style.sprite = resolveUrl(baseUrl, spriteUrl);
|
|
65
69
|
const source = style.sources[this.#sourceName];
|
|
66
70
|
if ('tiles' in source)
|
|
67
|
-
source.tiles =
|
|
71
|
+
source.tiles = tilesUrls.map(url => resolveUrl(baseUrl, url));
|
|
68
72
|
return style;
|
|
69
|
-
function resolveUrl(
|
|
70
|
-
if (!Boolean(
|
|
73
|
+
function resolveUrl(base, url) {
|
|
74
|
+
if (!Boolean(base))
|
|
71
75
|
return url;
|
|
72
|
-
url = new URL(url,
|
|
76
|
+
url = new URL(url, base).href;
|
|
73
77
|
url = url.replace(/%7B/gi, '{');
|
|
74
78
|
url = url.replace(/%7D/gi, '}');
|
|
75
79
|
return url;
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
getColors(colors) {
|
|
83
|
+
const entriesString = Object.entries(colors);
|
|
84
|
+
const entriesColor = entriesString.map(([key, value]) => [key, Color(value)]);
|
|
85
|
+
const result = Object.fromEntries(entriesColor);
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
transformDefaultColors(callback) {
|
|
89
|
+
const colors = this.getColors(this.defaultColors);
|
|
90
|
+
for (const key in colors) {
|
|
91
|
+
this.defaultColors[key] = callback(colors[key]).hexa();
|
|
92
|
+
}
|
|
82
93
|
}
|
|
83
94
|
}
|
|
@@ -4,10 +4,10 @@ import StyleBuilder from './style_builder.js';
|
|
|
4
4
|
// Mock class for abstract class StyleBuilder
|
|
5
5
|
class MockStyleBuilder extends StyleBuilder {
|
|
6
6
|
name = 'mock';
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
defaultFonts = { regular: 'Arial' };
|
|
8
|
+
defaultColors = { primary: '#FF8800' };
|
|
9
9
|
negateColors() {
|
|
10
|
-
this.
|
|
10
|
+
this.transformDefaultColors(color => color.negate());
|
|
11
11
|
}
|
|
12
12
|
getStyleRules(opt) {
|
|
13
13
|
for (const color of Object.values(opt.colors))
|
|
@@ -20,7 +20,7 @@ class MockStyleBuilder extends StyleBuilder {
|
|
|
20
20
|
'water-area': {
|
|
21
21
|
textColor: opt.colors.primary,
|
|
22
22
|
textSize: 12,
|
|
23
|
-
textFont:
|
|
23
|
+
textFont: opt.fonts.regular,
|
|
24
24
|
},
|
|
25
25
|
};
|
|
26
26
|
}
|
|
@@ -41,11 +41,11 @@ describe('StyleBuilder', () => {
|
|
|
41
41
|
expect(style).toHaveProperty('glyphs');
|
|
42
42
|
expect(style).toHaveProperty('sprite');
|
|
43
43
|
});
|
|
44
|
-
it('should
|
|
45
|
-
const initialColor = builder.
|
|
44
|
+
it('should transform colors correctly', () => {
|
|
45
|
+
const initialColor = builder.defaultColors.primary;
|
|
46
46
|
builder.negateColors();
|
|
47
|
-
expect(builder.
|
|
48
|
-
expect(builder.
|
|
47
|
+
expect(builder.defaultColors.primary).not.toBe(initialColor);
|
|
48
|
+
expect(builder.defaultColors.primary).toBe(Color(initialColor).negate().hexa());
|
|
49
49
|
});
|
|
50
50
|
describe('build method', () => {
|
|
51
51
|
it('should create a style object', () => {
|
|
@@ -56,14 +56,8 @@ describe('StyleBuilder', () => {
|
|
|
56
56
|
expect(style).toHaveProperty('glyphs');
|
|
57
57
|
expect(style).toHaveProperty('sprite');
|
|
58
58
|
});
|
|
59
|
-
it('should convert color strings to Color instances', () => {
|
|
60
|
-
builder.build();
|
|
61
|
-
const colors = Object.values(builder.colors);
|
|
62
|
-
expect(colors.every((color) => typeof color === 'string')).toBe(true);
|
|
63
|
-
});
|
|
64
59
|
it('should resolve urls correctly', () => {
|
|
65
|
-
builder.baseUrl
|
|
66
|
-
const style = builder.build();
|
|
60
|
+
const style = builder.build({ baseUrl: 'https://my.base.url/' });
|
|
67
61
|
expect(style.glyphs).toBe('https://my.base.url/assets/fonts/{fontstack}/{range}.pbf');
|
|
68
62
|
expect(style.sprite).toBe('https://my.base.url/assets/sprites/sprites');
|
|
69
63
|
const source = style.sources['versatiles-shortbread'];
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/prefer-includes */
|
|
2
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
3
|
+
import { isTileJSONSpecification } from './types.js';
|
|
4
|
+
import randomColorGenerator from './random_color.js';
|
|
5
|
+
import Colorful from '../style/colorful.js';
|
|
6
|
+
export default function guess(spec) {
|
|
7
|
+
if (!isTileJSONSpecification(spec))
|
|
8
|
+
throw Error();
|
|
9
|
+
spec.tilejson ??= '3.0.0';
|
|
10
|
+
switch (spec.type) {
|
|
11
|
+
case 'vector':
|
|
12
|
+
if (isShortbread(spec)) {
|
|
13
|
+
return getShortbreadStyle(spec);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
return getInspectorStyle(spec);
|
|
17
|
+
}
|
|
18
|
+
case 'raster':
|
|
19
|
+
return getImageStyle(spec);
|
|
20
|
+
default:
|
|
21
|
+
throw Error('spec.type must be: "vector" or "raster"');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function isShortbread(spec) {
|
|
25
|
+
if (typeof spec !== 'object')
|
|
26
|
+
return false;
|
|
27
|
+
if (!('vector_layers' in spec))
|
|
28
|
+
return false;
|
|
29
|
+
if (!Array.isArray(spec.vector_layers))
|
|
30
|
+
return false;
|
|
31
|
+
const layerIds = new Set(spec.vector_layers.map(l => String(l.id)));
|
|
32
|
+
const shortbreadIds = ['place_labels', 'boundaries', 'boundary_labels', 'addresses', 'water_lines', 'water_lines_labels', 'dam_lines', 'dam_polygons', 'pier_lines', 'pier_polygons', 'bridges', 'street_polygons', 'streets_polygons_labels', 'ferries', 'streets', 'street_labels', 'street_labels_points', 'aerialways', 'public_transport', 'buildings', 'water_polygons', 'ocean', 'water_polygons_labels', 'land', 'sites', 'pois'];
|
|
33
|
+
return shortbreadIds.every(id => layerIds.has(id));
|
|
34
|
+
}
|
|
35
|
+
function getShortbreadStyle(spec) {
|
|
36
|
+
return new Colorful().build({
|
|
37
|
+
hideLabels: true,
|
|
38
|
+
tilesUrls: spec.tiles,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function getInspectorStyle(spec) {
|
|
42
|
+
const sourceName = 'vectorSource';
|
|
43
|
+
const layers = { background: [], circle: [], line: [], fill: [] };
|
|
44
|
+
layers.background.push({ 'id': 'background', 'type': 'background', 'paint': { 'background-color': '#fff' } });
|
|
45
|
+
const randomColor = randomColorGenerator();
|
|
46
|
+
spec.vector_layers.forEach((vector_layer) => {
|
|
47
|
+
let luminosity = 'bright', saturation, hue;
|
|
48
|
+
if (/water|ocean|lake|sea|river/.test(vector_layer.id))
|
|
49
|
+
hue = 'blue';
|
|
50
|
+
if (/state|country|place/.test(vector_layer.id))
|
|
51
|
+
hue = 'pink';
|
|
52
|
+
if (/road|highway|transport|streets/.test(vector_layer.id))
|
|
53
|
+
hue = 'orange';
|
|
54
|
+
if (/contour|building/.test(vector_layer.id))
|
|
55
|
+
hue = 'monochrome';
|
|
56
|
+
if (/building/.test(vector_layer.id))
|
|
57
|
+
luminosity = 'dark';
|
|
58
|
+
if (/contour|landuse/.test(vector_layer.id))
|
|
59
|
+
hue = 'yellow';
|
|
60
|
+
if (/wood|forest|park|landcover|land/.test(vector_layer.id))
|
|
61
|
+
hue = 'green';
|
|
62
|
+
if (/point/.test(vector_layer.id)) {
|
|
63
|
+
saturation = 'strong';
|
|
64
|
+
luminosity = 'light';
|
|
65
|
+
}
|
|
66
|
+
const color = randomColor({
|
|
67
|
+
hue,
|
|
68
|
+
luminosity,
|
|
69
|
+
saturation,
|
|
70
|
+
seed: vector_layer.id,
|
|
71
|
+
opacity: 0.6,
|
|
72
|
+
});
|
|
73
|
+
layers.circle.push({
|
|
74
|
+
id: `${sourceName}-${vector_layer.id}-circle`,
|
|
75
|
+
'source-layer': vector_layer.id,
|
|
76
|
+
source: sourceName,
|
|
77
|
+
type: 'circle',
|
|
78
|
+
filter: ['==', '$type', 'Point'],
|
|
79
|
+
paint: { 'circle-color': color, 'circle-radius': 2 },
|
|
80
|
+
});
|
|
81
|
+
layers.line.push({
|
|
82
|
+
id: `${sourceName}-${vector_layer.id}-line`,
|
|
83
|
+
'source-layer': vector_layer.id,
|
|
84
|
+
source: sourceName,
|
|
85
|
+
type: 'line',
|
|
86
|
+
filter: ['==', '$type', 'LineString'],
|
|
87
|
+
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
|
88
|
+
paint: { 'line-color': color },
|
|
89
|
+
});
|
|
90
|
+
layers.fill.push({
|
|
91
|
+
id: `${sourceName}-${vector_layer.id}-fill`,
|
|
92
|
+
'source-layer': vector_layer.id,
|
|
93
|
+
source: sourceName,
|
|
94
|
+
type: 'fill',
|
|
95
|
+
filter: ['==', '$type', 'Polygon'],
|
|
96
|
+
paint: { 'fill-color': color, 'fill-opacity': 0.3, 'fill-antialias': true, 'fill-outline-color': color },
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
version: 8,
|
|
101
|
+
sources: {
|
|
102
|
+
[sourceName]: spec,
|
|
103
|
+
},
|
|
104
|
+
layers: [
|
|
105
|
+
...layers.background,
|
|
106
|
+
...layers.fill,
|
|
107
|
+
...layers.line,
|
|
108
|
+
...layers.circle,
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function getImageStyle(spec) {
|
|
113
|
+
const sourceName = 'rasterSource';
|
|
114
|
+
return {
|
|
115
|
+
version: 8,
|
|
116
|
+
sources: { [sourceName]: spec },
|
|
117
|
+
layers: [{
|
|
118
|
+
id: 'raster',
|
|
119
|
+
type: 'raster',
|
|
120
|
+
source: sourceName,
|
|
121
|
+
}],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
2
|
+
import guessStyle from './style_guesser.js';
|
|
3
|
+
describe('guessStyle', () => {
|
|
4
|
+
const tiles = ['https://example.com/tiles/{z}/{x}/{y}'];
|
|
5
|
+
it('should build raster styles', () => {
|
|
6
|
+
const type = 'raster';
|
|
7
|
+
const format = 'avif';
|
|
8
|
+
expect(guessStyle({ type, tiles, format }))
|
|
9
|
+
.toStrictEqual({
|
|
10
|
+
version: 8,
|
|
11
|
+
sources: { rasterSource: { format, tilejson: '3.0.0', tiles, type } },
|
|
12
|
+
layers: [{ id: 'raster', source: 'rasterSource', type }],
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
it('should build vector styles', () => {
|
|
16
|
+
const type = 'vector';
|
|
17
|
+
const format = 'pbf';
|
|
18
|
+
const vector_layers = [{ id: 'geometry', fields: { label: 'String', height: 'Number' } }];
|
|
19
|
+
expect(guessStyle({ type, tiles, format, vector_layers }))
|
|
20
|
+
.toStrictEqual({
|
|
21
|
+
version: 8,
|
|
22
|
+
sources: { vectorSource: { format, tilejson: '3.0.0', tiles, type, vector_layers } },
|
|
23
|
+
layers: [
|
|
24
|
+
{
|
|
25
|
+
id: 'background',
|
|
26
|
+
type: 'background',
|
|
27
|
+
paint: { 'background-color': '#fff' },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'vectorSource-geometry-fill',
|
|
31
|
+
type: 'fill',
|
|
32
|
+
source: 'vectorSource',
|
|
33
|
+
'source-layer': 'geometry',
|
|
34
|
+
filter: ['==', '$type', 'Polygon'],
|
|
35
|
+
paint: { 'fill-antialias': true, 'fill-color': 'hsla(7,57%,56%,0.6)', 'fill-opacity': 0.3, 'fill-outline-color': 'hsla(7,57%,56%,0.6)' },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'vectorSource-geometry-line',
|
|
39
|
+
type: 'line',
|
|
40
|
+
source: 'vectorSource',
|
|
41
|
+
'source-layer': 'geometry',
|
|
42
|
+
filter: ['==', '$type', 'LineString'],
|
|
43
|
+
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
|
44
|
+
paint: { 'line-color': 'hsla(7,57%,56%,0.6)' },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'vectorSource-geometry-circle',
|
|
48
|
+
type: 'circle',
|
|
49
|
+
source: 'vectorSource',
|
|
50
|
+
'source-layer': 'geometry',
|
|
51
|
+
filter: ['==', '$type', 'Point'],
|
|
52
|
+
paint: { 'circle-color': 'hsla(7,57%,56%,0.6)', 'circle-radius': 2 },
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BackgroundLayerSpecification, FillLayerSpecification, FilterSpecification, LineLayerSpecification, StyleSpecification, SymbolLayerSpecification } from '@maplibre/maplibre-gl-style-spec';
|
|
2
2
|
import type Color from 'color';
|
|
3
|
+
import type StyleBuilder from './style_builder.ts';
|
|
3
4
|
export type TileFormat = 'avif' | 'bin' | 'geojson' | 'jpg' | 'json' | 'pbf' | 'png' | 'svg' | 'topojson' | 'webp';
|
|
4
5
|
export type MaplibreLayer = BackgroundLayerSpecification | FillLayerSpecification | LineLayerSpecification | SymbolLayerSpecification;
|
|
5
6
|
export type MaplibreLayerDefinition = BackgroundLayerSpecification | Omit<FillLayerSpecification, 'source'> | Omit<LineLayerSpecification, 'source'> | Omit<SymbolLayerSpecification, 'source'>;
|
|
@@ -47,14 +48,18 @@ export type MaplibreStyleVector = Omit<StyleSpecification, 'sources'> & {
|
|
|
47
48
|
};
|
|
48
49
|
export type MaplibreStyle = MaplibreStyleRaster | MaplibreStyleVector;
|
|
49
50
|
export type StyleRuleValue = boolean | number | object | string;
|
|
50
|
-
export type StyleRule = Record<string, StyleRuleValue>;
|
|
51
|
-
export type StyleRules = Record<string, StyleRule>;
|
|
52
|
-
export type StylemakerColorLookup = Record<string, Color>;
|
|
53
|
-
export type StylemakerStringLookup = Record<string, string>;
|
|
51
|
+
export type StyleRule = Record<string, StyleRuleValue | undefined>;
|
|
52
|
+
export type StyleRules = Record<string, StyleRule | undefined>;
|
|
54
53
|
export type LanguageSuffix = '_de' | '_en' | '';
|
|
55
|
-
export
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
export type StylemakerColorKeys<T extends StyleBuilder<T>> = keyof T['defaultColors'];
|
|
55
|
+
export type StylemakerFontKeys<T extends StyleBuilder<T>> = keyof T['defaultFonts'];
|
|
56
|
+
export type StylemakerColorStrings<T extends StyleBuilder<T>> = Record<StylemakerColorKeys<T>, string>;
|
|
57
|
+
export type StylemakerFontStrings<T extends StyleBuilder<T>> = Record<StylemakerFontKeys<T>, string>;
|
|
58
|
+
export type StylemakerColors<T extends StyleBuilder<T>> = Record<StylemakerColorKeys<T>, Color>;
|
|
59
|
+
export type StylemakerFonts<T extends StyleBuilder<T>> = Record<StylemakerFontKeys<T>, string>;
|
|
60
|
+
export interface StyleRulesOptions<T extends StyleBuilder<T>> {
|
|
61
|
+
colors: StylemakerColors<T>;
|
|
62
|
+
fonts: StylemakerFontStrings<T>;
|
|
58
63
|
languageSuffix: string;
|
|
59
64
|
}
|
|
60
65
|
export interface RecolorOptions {
|
|
@@ -67,4 +72,15 @@ export interface RecolorOptions {
|
|
|
67
72
|
tint?: number;
|
|
68
73
|
tintColor?: string;
|
|
69
74
|
}
|
|
75
|
+
export interface StylemakerOptions<T extends StyleBuilder<T>> {
|
|
76
|
+
baseUrl?: string;
|
|
77
|
+
glyphsUrl?: string;
|
|
78
|
+
spriteUrl?: string;
|
|
79
|
+
tilesUrls?: string[];
|
|
80
|
+
hideLabels?: boolean;
|
|
81
|
+
languageSuffix?: LanguageSuffix;
|
|
82
|
+
colors?: Partial<StylemakerColorStrings<T>>;
|
|
83
|
+
fonts?: Partial<StylemakerFontStrings<T>>;
|
|
84
|
+
recolor?: RecolorOptions;
|
|
85
|
+
}
|
|
70
86
|
export declare function isTileJSONSpecification(obj: unknown): obj is TileJSONSpecification;
|
package/dist/style/colorful.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import StyleBuilder from '../lib/style_builder.js';
|
|
2
2
|
import type { StyleRules, StyleRulesOptions } from '../lib/types.js';
|
|
3
|
-
export default class Colorful extends StyleBuilder {
|
|
3
|
+
export default class Colorful extends StyleBuilder<Colorful> {
|
|
4
4
|
readonly name: string;
|
|
5
|
-
|
|
5
|
+
defaultFonts: {
|
|
6
6
|
regular: string;
|
|
7
7
|
bold: string;
|
|
8
8
|
};
|
|
9
|
-
|
|
9
|
+
defaultColors: {
|
|
10
10
|
land: string;
|
|
11
11
|
water: string;
|
|
12
12
|
glacier: string;
|
|
@@ -49,5 +49,5 @@ export default class Colorful extends StyleBuilder {
|
|
|
49
49
|
hospital: string;
|
|
50
50
|
poi: string;
|
|
51
51
|
};
|
|
52
|
-
protected getStyleRules(options: StyleRulesOptions): StyleRules;
|
|
52
|
+
protected getStyleRules(options: StyleRulesOptions<Colorful>): StyleRules;
|
|
53
53
|
}
|
package/dist/style/colorful.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
2
2
|
import StyleBuilder from '../lib/style_builder.js';
|
|
3
3
|
export default class Colorful extends StyleBuilder {
|
|
4
|
-
name = '
|
|
5
|
-
|
|
4
|
+
name = 'Colorful';
|
|
5
|
+
defaultFonts = {
|
|
6
6
|
regular: 'noto_sans_regular',
|
|
7
7
|
bold: 'noto_sans_bold',
|
|
8
8
|
};
|
|
9
|
-
|
|
9
|
+
defaultColors = {
|
|
10
10
|
land: '#F9F4EE',
|
|
11
11
|
water: '#BEDDF3',
|
|
12
12
|
glacier: '#FFFFFF',
|
package/dist/style/graybeard.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import Colorful from './colorful.js';
|
|
2
2
|
export default class Graybeard extends Colorful {
|
|
3
|
-
name = '
|
|
3
|
+
name = 'Graybeard';
|
|
4
4
|
constructor() {
|
|
5
5
|
super();
|
|
6
|
-
this.
|
|
6
|
+
this.transformDefaultColors(color => color.desaturate(1));
|
|
7
7
|
}
|
|
8
8
|
}
|
package/dist/style/neutrino.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import StyleBuilder from '../lib/style_builder.js';
|
|
2
2
|
import type { StyleRules, StyleRulesOptions } from '../lib/types.js';
|
|
3
|
-
export default class Neutrino extends StyleBuilder {
|
|
3
|
+
export default class Neutrino extends StyleBuilder<Neutrino> {
|
|
4
4
|
readonly name: string;
|
|
5
|
-
|
|
5
|
+
defaultFonts: {
|
|
6
6
|
regular: string;
|
|
7
7
|
bold: string;
|
|
8
8
|
};
|
|
9
|
-
|
|
9
|
+
defaultColors: {
|
|
10
10
|
land: string;
|
|
11
11
|
water: string;
|
|
12
12
|
grass: string;
|
|
@@ -20,5 +20,5 @@ export default class Neutrino extends StyleBuilder {
|
|
|
20
20
|
rail: string;
|
|
21
21
|
label: string;
|
|
22
22
|
};
|
|
23
|
-
protected getStyleRules(options: StyleRulesOptions): StyleRules;
|
|
23
|
+
protected getStyleRules(options: StyleRulesOptions<Neutrino>): StyleRules;
|
|
24
24
|
}
|
package/dist/style/neutrino.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
2
2
|
import StyleBuilder from '../lib/style_builder.js';
|
|
3
3
|
export default class Neutrino extends StyleBuilder {
|
|
4
|
-
name = '
|
|
5
|
-
|
|
4
|
+
name = 'Neutrino';
|
|
5
|
+
defaultFonts = {
|
|
6
6
|
regular: 'noto_sans_regular',
|
|
7
7
|
bold: 'noto_sans_bold',
|
|
8
8
|
};
|
|
9
|
-
|
|
9
|
+
defaultColors = {
|
|
10
10
|
land: '#f6f0f6',
|
|
11
11
|
water: '#cbd2df',
|
|
12
12
|
grass: '#e7e9e5',
|
package/package.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@versatiles/style",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "Generate StyleJSON for MapLibre",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"check": "npm run
|
|
8
|
+
"check": "npm run build && npm run lint && npm run test",
|
|
9
9
|
"build": "npm run build-browser && npm run build-node && npm run build-styles",
|
|
10
10
|
"build-browser": "rollup -c=rollup.config.js",
|
|
11
11
|
"build-node": "rm -rf dist && tsc -p tsconfig.node.json && chmod +x dist/index.js",
|
|
12
12
|
"build-styles": "tsx scripts/build-styles.ts",
|
|
13
|
-
"doc": "vrt ts2md src/index.ts tsconfig.node.json | vrt insertmd README.md '
|
|
14
|
-
"lint": "eslint --color .",
|
|
15
|
-
"prepublish": "npm run build && npm run doc",
|
|
13
|
+
"doc": "vrt ts2md src/index.ts tsconfig.node.json | vrt insertmd README.md '# API'",
|
|
14
|
+
"lint": "npm run build-browser && eslint --color .",
|
|
16
15
|
"release": "tsx scripts/release.ts",
|
|
17
|
-
"test": "npm run test-
|
|
18
|
-
"test-browser": "npm run build-browser &&
|
|
16
|
+
"test": "npm run test-typescript && npm run test-node && npm run test-browser",
|
|
17
|
+
"test-browser": "npm run build-browser && jest -c=jest.config.browser.ts",
|
|
19
18
|
"test-coverage": "NODE_OPTIONS=--experimental-vm-modules jest -c=jest.config.coverage.ts",
|
|
20
19
|
"test-node": "npm run build-node && NODE_OPTIONS=--experimental-vm-modules jest -c=jest.config.node.ts",
|
|
20
|
+
"test-typescript": "NODE_OPTIONS=--experimental-vm-modules jest -c=jest.config.typescript.ts",
|
|
21
21
|
"upgrade": "npm-check-updates -u && rm -f package-lock.json && rm -rf node_modules; npm i"
|
|
22
22
|
},
|
|
23
23
|
"repository": {
|
|
@@ -44,21 +44,22 @@
|
|
|
44
44
|
"@rollup/plugin-typescript": "^11.1.5",
|
|
45
45
|
"@types/brace-expansion": "^1.1.2",
|
|
46
46
|
"@types/inquirer": "^9.0.7",
|
|
47
|
-
"@types/jest": "^29.5.
|
|
48
|
-
"@types/node": "^20.
|
|
49
|
-
"@typescript-eslint/eslint-plugin": "^6.
|
|
50
|
-
"@typescript-eslint/parser": "^6.
|
|
51
|
-
"@versatiles/release-tool": "^1.0.
|
|
47
|
+
"@types/jest": "^29.5.10",
|
|
48
|
+
"@types/node": "^20.10.0",
|
|
49
|
+
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
|
50
|
+
"@typescript-eslint/parser": "^6.13.0",
|
|
51
|
+
"@versatiles/release-tool": "^1.0.3",
|
|
52
52
|
"eslint": "^8.54.0",
|
|
53
53
|
"inquirer": "^9.2.12",
|
|
54
54
|
"jest": "^29.7.0",
|
|
55
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
55
56
|
"jest-ts-webcompat-resolver": "^1.0.0",
|
|
56
57
|
"npm-check-updates": "^16.14.11",
|
|
57
|
-
"rollup": "^4.
|
|
58
|
+
"rollup": "^4.6.0",
|
|
58
59
|
"rollup-plugin-dts": "^6.1.0",
|
|
59
60
|
"ts-jest": "^29.1.1",
|
|
60
61
|
"ts-node": "^10.9.1",
|
|
61
|
-
"tsx": "^4.
|
|
62
|
-
"typescript": "^5.
|
|
62
|
+
"tsx": "^4.5.0",
|
|
63
|
+
"typescript": "^5.3.2"
|
|
63
64
|
}
|
|
64
65
|
}
|