@versatiles/style 5.2.6 → 5.2.7

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.
Files changed (130) hide show
  1. package/dist/index.d.ts +274 -14
  2. package/dist/index.js +3650 -11
  3. package/dist/index.js.map +1 -1
  4. package/package.json +8 -8
  5. package/src/color/abstract.ts +83 -0
  6. package/src/color/hsl.test.ts +182 -0
  7. package/src/color/hsl.ts +122 -0
  8. package/src/color/hsv.test.ts +174 -0
  9. package/src/color/hsv.ts +100 -0
  10. package/src/color/index.test.ts +119 -0
  11. package/src/color/index.ts +38 -0
  12. package/src/color/random.test.ts +35 -0
  13. package/src/color/random.ts +165 -0
  14. package/src/color/rgb.test.ts +227 -0
  15. package/src/color/rgb.ts +248 -0
  16. package/src/color/utils.test.ts +86 -0
  17. package/src/color/utils.ts +13 -0
  18. package/src/guess_style/guess_style.test.ts +134 -0
  19. package/src/guess_style/guess_style.ts +166 -0
  20. package/{dist/guess_style/index.d.ts → src/guess_style/index.ts} +1 -0
  21. package/src/index.test.ts +77 -0
  22. package/src/index.ts +18 -0
  23. package/src/lib/utils.test.ts +197 -0
  24. package/src/lib/utils.ts +134 -0
  25. package/{dist/shortbread/index.d.ts → src/shortbread/index.ts} +1 -0
  26. package/src/shortbread/layers.test.ts +36 -0
  27. package/src/shortbread/layers.ts +564 -0
  28. package/src/shortbread/properties.test.ts +44 -0
  29. package/src/shortbread/properties.ts +142 -0
  30. package/src/shortbread/template.test.ts +43 -0
  31. package/src/shortbread/template.ts +343 -0
  32. package/src/style_builder/decorator.test.ts +67 -0
  33. package/src/style_builder/decorator.ts +135 -0
  34. package/src/style_builder/recolor.test.ts +306 -0
  35. package/src/style_builder/recolor.ts +110 -0
  36. package/src/style_builder/style_builder.test.ts +103 -0
  37. package/src/style_builder/style_builder.ts +134 -0
  38. package/src/style_builder/types.ts +141 -0
  39. package/src/styles/LICENSE.md +41 -0
  40. package/src/styles/colorful.ts +1041 -0
  41. package/src/styles/eclipse.ts +11 -0
  42. package/{dist/styles/empty.d.ts → src/styles/empty.ts} +7 -3
  43. package/src/styles/graybeard.ts +11 -0
  44. package/src/styles/index.ts +33 -0
  45. package/src/styles/neutrino.ts +429 -0
  46. package/{dist/types/index.d.ts → src/types/index.ts} +1 -0
  47. package/{dist/types/maplibre.d.ts → src/types/maplibre.ts} +3 -0
  48. package/src/types/tilejson.test.ts +94 -0
  49. package/src/types/tilejson.ts +125 -0
  50. package/src/types/vector_layer.test.ts +64 -0
  51. package/src/types/vector_layer.ts +69 -0
  52. package/dist/color/abstract.d.ts +0 -34
  53. package/dist/color/abstract.js +0 -53
  54. package/dist/color/abstract.js.map +0 -1
  55. package/dist/color/hsl.d.ts +0 -23
  56. package/dist/color/hsl.js +0 -98
  57. package/dist/color/hsl.js.map +0 -1
  58. package/dist/color/hsv.d.ts +0 -20
  59. package/dist/color/hsv.js +0 -100
  60. package/dist/color/hsv.js.map +0 -1
  61. package/dist/color/index.d.ts +0 -6
  62. package/dist/color/index.js +0 -29
  63. package/dist/color/index.js.map +0 -1
  64. package/dist/color/random.d.ts +0 -9
  65. package/dist/color/random.js +0 -134
  66. package/dist/color/random.js.map +0 -1
  67. package/dist/color/rgb.d.ts +0 -28
  68. package/dist/color/rgb.js +0 -195
  69. package/dist/color/rgb.js.map +0 -1
  70. package/dist/color/utils.d.ts +0 -3
  71. package/dist/color/utils.js +0 -10
  72. package/dist/color/utils.js.map +0 -1
  73. package/dist/guess_style/guess_style.d.ts +0 -8
  74. package/dist/guess_style/guess_style.js +0 -147
  75. package/dist/guess_style/guess_style.js.map +0 -1
  76. package/dist/guess_style/index.js +0 -2
  77. package/dist/guess_style/index.js.map +0 -1
  78. package/dist/lib/utils.d.ts +0 -6
  79. package/dist/lib/utils.js +0 -126
  80. package/dist/lib/utils.js.map +0 -1
  81. package/dist/shortbread/index.js +0 -3
  82. package/dist/shortbread/index.js.map +0 -1
  83. package/dist/shortbread/layers.d.ts +0 -5
  84. package/dist/shortbread/layers.js +0 -521
  85. package/dist/shortbread/layers.js.map +0 -1
  86. package/dist/shortbread/properties.d.ts +0 -7
  87. package/dist/shortbread/properties.js +0 -125
  88. package/dist/shortbread/properties.js.map +0 -1
  89. package/dist/shortbread/template.d.ts +0 -4
  90. package/dist/shortbread/template.js +0 -339
  91. package/dist/shortbread/template.js.map +0 -1
  92. package/dist/style_builder/decorator.d.ts +0 -4
  93. package/dist/style_builder/decorator.js +0 -127
  94. package/dist/style_builder/decorator.js.map +0 -1
  95. package/dist/style_builder/recolor.d.ts +0 -22
  96. package/dist/style_builder/recolor.js +0 -89
  97. package/dist/style_builder/recolor.js.map +0 -1
  98. package/dist/style_builder/style_builder.d.ts +0 -15
  99. package/dist/style_builder/style_builder.js +0 -106
  100. package/dist/style_builder/style_builder.js.map +0 -1
  101. package/dist/style_builder/types.d.ts +0 -122
  102. package/dist/style_builder/types.js +0 -3
  103. package/dist/style_builder/types.js.map +0 -1
  104. package/dist/styles/colorful.d.ts +0 -11
  105. package/dist/styles/colorful.js +0 -956
  106. package/dist/styles/colorful.js.map +0 -1
  107. package/dist/styles/eclipse.d.ts +0 -5
  108. package/dist/styles/eclipse.js +0 -9
  109. package/dist/styles/eclipse.js.map +0 -1
  110. package/dist/styles/empty.js +0 -8
  111. package/dist/styles/empty.js.map +0 -1
  112. package/dist/styles/graybeard.d.ts +0 -5
  113. package/dist/styles/graybeard.js +0 -9
  114. package/dist/styles/graybeard.js.map +0 -1
  115. package/dist/styles/index.d.ts +0 -11
  116. package/dist/styles/index.js +0 -20
  117. package/dist/styles/index.js.map +0 -1
  118. package/dist/styles/neutrino.d.ts +0 -11
  119. package/dist/styles/neutrino.js +0 -401
  120. package/dist/styles/neutrino.js.map +0 -1
  121. package/dist/types/index.js +0 -3
  122. package/dist/types/index.js.map +0 -1
  123. package/dist/types/maplibre.js +0 -2
  124. package/dist/types/maplibre.js.map +0 -1
  125. package/dist/types/tilejson.d.ts +0 -32
  126. package/dist/types/tilejson.js +0 -87
  127. package/dist/types/tilejson.js.map +0 -1
  128. package/dist/types/vector_layer.d.ts +0 -14
  129. package/dist/types/vector_layer.js +0 -51
  130. package/dist/types/vector_layer.js.map +0 -1
@@ -0,0 +1,135 @@
1
+
2
+ import { Color } from '../color/index.js';
3
+ import expandBraces from 'brace-expansion';
4
+ import maplibreProperties from '../shortbread/properties.js';
5
+ import { deepMerge } from '../lib/utils.js';
6
+ import type { MaplibreLayer } from '../types/index.js';
7
+ import type { StyleRule, StyleRuleValue, StyleRules } from './types.js';
8
+ import type { CachedRecolor } from './recolor.js';
9
+
10
+
11
+
12
+ export function decorate(layers: MaplibreLayer[], rules: StyleRules, recolor: CachedRecolor): MaplibreLayer[] {
13
+ const layerIds = layers.map(l => l.id);
14
+ const layerIdSet = new Set(layerIds);
15
+
16
+ // Initialize a new map to hold final styles for layers
17
+ const layerStyles = new Map<string, StyleRule>();
18
+
19
+ // Iterate through the generated layer style rules
20
+ Object.entries(rules).forEach(([idDef, layerStyle]) => {
21
+ if (layerStyle == null) return;
22
+
23
+ // Expand any braces in IDs and filter them through a RegExp if necessary
24
+ const ids = expandBraces(idDef).flatMap(id => {
25
+ if (!id.includes('*')) return id;
26
+ const regExpString = id.replace(/[^a-z_:-]/g, c => {
27
+ if (c === '*') return '[a-z_-]*';
28
+ throw new Error('unknown char to process. Do not know how to make a RegExp from: ' + JSON.stringify(c));
29
+ });
30
+ const regExp = new RegExp(`^${regExpString}$`, 'i');
31
+ return layerIds.filter(layerId => regExp.test(layerId));
32
+ });
33
+
34
+ ids.forEach(id => {
35
+ if (!layerIdSet.has(id)) return;
36
+ layerStyles.set(id, deepMerge(layerStyles.get(id) ?? {}, layerStyle));
37
+ });
38
+ });
39
+
40
+ // Deep clone the original layers and apply styles
41
+ return layers.flatMap(layer => {
42
+ // Get the id and style of the layer
43
+ const layerStyle = layerStyles.get(layer.id);
44
+
45
+ // Don't export layers that have no style
46
+ if (!layerStyle) return [];
47
+
48
+ processStyling(layer, layerStyle);
49
+
50
+ return [layer];
51
+ });
52
+
53
+ // Function to process each style attribute for the layer
54
+ function processStyling(layer: MaplibreLayer, styleRule: StyleRule): void {
55
+
56
+ for (const [ruleKeyCamelCase, ruleValue] of Object.entries(styleRule)) {
57
+ if (ruleValue == null) continue;
58
+
59
+ // CamelCase to not-camel-case
60
+ const ruleKey = ruleKeyCamelCase.replace(/[A-Z]/g, c => '-' + c.toLowerCase());
61
+
62
+ const propertyDefs = maplibreProperties.get(layer.type + '/' + ruleKey);
63
+ if (!propertyDefs) continue;
64
+
65
+ propertyDefs.forEach(propertyDef => {
66
+ const { key } = propertyDef;
67
+ let value: StyleRuleValue = ruleValue;
68
+
69
+ switch (propertyDef.valueType) {
70
+ case 'color': value = processExpression(value, processColor); break;
71
+ case 'fonts': value = processExpression(value, processFont); break;
72
+ case 'resolvedImage':
73
+ case 'formatted':
74
+ case 'array':
75
+ case 'boolean':
76
+ case 'enum':
77
+ case 'number': value = processExpression(value); break;
78
+ default: throw new Error(`unknown propertyDef.valueType "${propertyDef.valueType}" for key "${key}"`);
79
+ }
80
+
81
+ switch (propertyDef.parent) {
82
+ case 'layer':
83
+ // @ts-expect-error: too complex to handle
84
+ layer[key] = value;
85
+ break;
86
+ case 'layout':
87
+ if (!layer.layout) layer.layout = {};
88
+ // @ts-expect-error: too complex to handle
89
+ layer.layout[key] = value;
90
+ break;
91
+ case 'paint':
92
+ if (!layer.paint) layer.paint = {};
93
+ // @ts-expect-error: too complex to handle
94
+ layer.paint[key] = value;
95
+ break;
96
+ default:
97
+
98
+ throw new Error(`unknown parent "${propertyDef.parent}" for key "${key}"`);
99
+ }
100
+ });
101
+ }
102
+
103
+ function processColor(value: StyleRuleValue): string {
104
+ if (typeof value === 'string') value = Color.parse(value);
105
+ if (value instanceof Color) {
106
+ const color = recolor.do(value as Color);
107
+ return color.asString()
108
+ }
109
+ throw new Error(`unknown color type "${typeof value}"`);
110
+ }
111
+
112
+ function processFont(value: StyleRuleValue): string[] {
113
+ if (typeof value === 'string') return [value];
114
+ throw new Error(`unknown font type "${typeof value}"`);
115
+ }
116
+
117
+ function processExpression(value: StyleRuleValue, cbValue?: (value: StyleRuleValue) => StyleRuleValue): StyleRuleValue {
118
+ if (typeof value === 'object') {
119
+ if (value instanceof Color) return processColor(value);
120
+ if (!Array.isArray(value)) {
121
+ return processZoomStops(value as Record<string, StyleRuleValue>, cbValue);
122
+ }
123
+ }
124
+ return cbValue ? cbValue(value) : value;
125
+ }
126
+
127
+ function processZoomStops(obj: Record<string, StyleRuleValue>, cbValue?: (value: StyleRuleValue) => StyleRuleValue): { stops: StyleRuleValue[] } {
128
+ return {
129
+ stops: Object.entries(obj)
130
+ .map(([z, v]) => [parseInt(z, 10), cbValue ? cbValue(v) : v] as [number, StyleRuleValue])
131
+ .sort((a, b) => a[0] - b[0]),
132
+ };
133
+ }
134
+ }
135
+ }
@@ -0,0 +1,306 @@
1
+ import { Color } from '../color/index.js';
2
+ import { CachedRecolor, getDefaultRecolorFlags, recolorArray, recolorObject } from './recolor.js';
3
+
4
+
5
+ describe('recolor', () => {
6
+ describe('getDefaultRecolorFlags', () => {
7
+ it('should return the default color transformer flags', () => {
8
+ const defaultFlags = getDefaultRecolorFlags();
9
+ expect(defaultFlags).toEqual({
10
+ invertBrightness: false,
11
+ rotate: 0,
12
+ saturate: 0,
13
+ gamma: 1,
14
+ contrast: 1,
15
+ brightness: 0,
16
+ tint: 0,
17
+ tintColor: '#FF0000',
18
+ });
19
+ });
20
+ });
21
+
22
+ it('should not alter the colors if no flags are provided', () => {
23
+ const colors = getTestColors();
24
+ recolorArray(colors, {});
25
+ expect(colors).toEqual(getTestColors());
26
+ });
27
+
28
+ describe('invertBrightness', () => {
29
+ it('should invert brightness when invert flag is true', () => {
30
+ const colors = getTestColors();
31
+ recolorArray(colors, { invertBrightness: true });
32
+ expect(colors2string(colors)).toBe('AA550000,00FFAA55,5500FFAA,FFAA55,AA7755');
33
+ });
34
+ });
35
+
36
+ describe('rotate', () => {
37
+ it('should rotate colors 120°', () => {
38
+ const colors = getTestColors();
39
+ recolorArray(colors, { rotate: 120 });
40
+ expect(colors2string(colors)).toBe('55FFAA00,AA00FF55,FF5500AA,00AA55,55AA77');
41
+ });
42
+
43
+ it('should rotate colors 180°', () => {
44
+ const colors = getTestColors();
45
+ recolorArray(colors, { rotate: 180 });
46
+ expect(colors2string(colors)).toBe('55AAFF00,FF005555,AAFF00AA,0055AA,5588AA');
47
+ });
48
+
49
+ it('should rotate colors 240°', () => {
50
+ const colors = getTestColors();
51
+ recolorArray(colors, { rotate: 240 });
52
+ expect(colors2string(colors)).toBe('AA55FF00,FFAA0055,00FF55AA,5500AA,7755AA');
53
+ });
54
+ });
55
+
56
+ describe('saturation', () => {
57
+ it('should remove any saturation', () => {
58
+ const colors = getTestColors();
59
+ recolorArray(colors, { saturate: -1.0 });
60
+ expect(colors2string(colors)).toBe('AAAAAA00,80808055,808080AA,555555,808080');
61
+ });
62
+
63
+ it('should decrease saturation', () => {
64
+ const colors = getTestColors();
65
+ recolorArray(colors, { saturate: -0.5 });
66
+ expect(colors2string(colors)).toBe('D4AA7F00,40BF9555,6A40BFAA,7F552A,957B6A');
67
+ });
68
+
69
+ it('should increase saturation', () => {
70
+ const colors = getTestColors();
71
+ recolorArray(colors, { saturate: 0.5 });
72
+ expect(colors2string(colors)).toBe('FFAA5500,00FFAA55,5500FFAA,AA5500,BF7340');
73
+ });
74
+
75
+ it('should maximize saturation', () => {
76
+ const colors = getTestColors();
77
+ recolorArray(colors, { saturate: 1.0 });
78
+ expect(colors2string(colors)).toBe('FFAA5500,00FFAA55,5500FFAA,AA5500,D46F2B');
79
+ });
80
+ });
81
+
82
+ describe('gamma', () => {
83
+ it('should decrease gamma', () => {
84
+ const colors = getTestColors();
85
+ recolorArray(colors, { gamma: 0.5 });
86
+ expect(colors2string(colors)).toBe('FFD09300,00FFD055,9300FFAA,D09300,D0AE93');
87
+ });
88
+
89
+ it('should increase gamma', () => {
90
+ const colors = getTestColors();
91
+ recolorArray(colors, { gamma: 2 });
92
+ expect(colors2string(colors)).toBe('FF711C00,00FF7155,1C00FFAA,711C00,71381C');
93
+ });
94
+ });
95
+
96
+ describe('contrast', () => {
97
+ it('should remove any contrast', () => {
98
+ const colors = getTestColors();
99
+ recolorArray(colors, { contrast: 0 });
100
+ expect(colors2string(colors)).toBe('80808000,80808055,808080AA,808080,808080');
101
+ });
102
+
103
+ it('should decrease contrast', () => {
104
+ const colors = getTestColors();
105
+ recolorArray(colors, { contrast: 0.5 });
106
+ expect(colors2string(colors)).toBe('BF956A00,40BF9555,6A40BFAA,956A40,957B6A');
107
+ });
108
+
109
+ it('should increase contrast', () => {
110
+ const colors = getTestColors();
111
+ recolorArray(colors, { contrast: 2 });
112
+ expect(colors2string(colors)).toBe('FFD52B00,00FFD555,2B00FFAA,D52B00,D56F2B');
113
+ });
114
+
115
+ it('should maximize contrast', () => {
116
+ const colors = getTestColors();
117
+ recolorArray(colors, { contrast: Infinity });
118
+ expect(colors2string(colors)).toBe('FFFF0000,00FFFF55,0000FFAA,FF0000,FF0000');
119
+ });
120
+ })
121
+
122
+ describe('brightness', () => {
123
+ it('should remove any brightness', () => {
124
+ const colors = getTestColors();
125
+ recolorArray(colors, { brightness: -1 });
126
+ expect(colors2string(colors)).toBe('00000000,00000055,000000AA,000000,000000');
127
+ });
128
+
129
+ it('should decrease brightness', () => {
130
+ const colors = getTestColors();
131
+ recolorArray(colors, { brightness: -0.5 });
132
+ expect(colors2string(colors)).toBe('80552B00,00805555,2B0080AA,552B00,553C2B');
133
+ });
134
+
135
+ it('should increase brightness', () => {
136
+ const colors = getTestColors();
137
+ recolorArray(colors, { brightness: 0.5 });
138
+ expect(colors2string(colors)).toBe('FFD5AA00,80FFD555,AA80FFAA,D5AA80,D5BBAA');
139
+ });
140
+
141
+ it('should maximize brightness', () => {
142
+ const colors = getTestColors();
143
+ recolorArray(colors, { brightness: 1 });
144
+ expect(colors2string(colors)).toBe('FFFFFF00,FFFFFF55,FFFFFFAA,FFFFFF,FFFFFF');
145
+ });
146
+ });
147
+
148
+ describe('tint', () => {
149
+ it('should not tint at all', () => {
150
+ const colors = getTestColors();
151
+ recolorArray(colors, { tint: 0, tintColor: '#F00' });
152
+ expect(colors2string(colors)).toBe('FFAA5500,00FFAA55,5500FFAA,AA5500,AA7755');
153
+ });
154
+
155
+ it('should tint a little bit red', () => {
156
+ const colors = getTestColors();
157
+ recolorArray(colors, { tint: 0.5, tintColor: '#F00' });
158
+ expect(colors2string(colors)).toBe('FF805500,80805555,AA0080AA,AA2B00,AA6655');
159
+ });
160
+
161
+ it('should tint a little bit yellow', () => {
162
+ const colors = getTestColors();
163
+ recolorArray(colors, { tint: 0.2, tintColor: '#FF0' });
164
+ expect(colors2string(colors)).toBe('FFBB5500,33FF8855,7733CCAA,AA6600,AA8155');
165
+ });
166
+
167
+ it('should tint a little bit green', () => {
168
+ const colors = getTestColors();
169
+ recolorArray(colors, { tint: 0.2, tintColor: '#0F0' });
170
+ expect(colors2string(colors)).toBe('DDBB5500,00FF8855,4433CCAA,886600,998155');
171
+ });
172
+
173
+ it('should tint a little bit blue', () => {
174
+ const colors = getTestColors();
175
+ recolorArray(colors, { tint: 0.2, tintColor: '#00F' });
176
+ expect(colors2string(colors)).toBe('DD997700,00CCBB55,4400FFAA,884422,997066');
177
+ });
178
+
179
+ it('should tint strongly orange', () => {
180
+ const colors = getTestColors();
181
+ recolorArray(colors, { tint: 0.8, tintColor: '#F80' });
182
+ expect(colors2string(colors)).toBe('FFAF5500,CCA02255,DD6D33AA,AA5A00,AA8055');
183
+ });
184
+
185
+ it('should tint a strongly blue', () => {
186
+ const colors = getTestColors();
187
+ recolorArray(colors, { tint: 0.8, tintColor: '#00F' });
188
+ expect(colors2string(colors)).toBe('7766DD00,0033EE55,1100FFAA,221188,665C99');
189
+ });
190
+ });
191
+ });
192
+
193
+ describe('recolorObject', () => {
194
+ it('should recolor an object of colors', () => {
195
+ const colors = {
196
+ color1: Color.parse('#FA50'),
197
+ color2: Color.parse('#0FA5'),
198
+ color3: Color.parse('#50FA'),
199
+ };
200
+ recolorObject(colors, { rotate: 120 });
201
+ expect(colors.color1.asHex()).toBe('#55FFAA00');
202
+ expect(colors.color2.asHex()).toBe('#AA00FF55');
203
+ expect(colors.color3.asHex()).toBe('#FF5500AA');
204
+ });
205
+
206
+ it('should not alter colors if options are invalid', () => {
207
+ const colors = {
208
+ color1: Color.parse('#FA50'),
209
+ color2: Color.parse('#0FA5'),
210
+ color3: Color.parse('#50FA'),
211
+ };
212
+ const original = { ...colors };
213
+ recolorObject(colors, {});
214
+ expect(colors).toEqual(original);
215
+ });
216
+ });
217
+
218
+ describe('recolorArray', () => {
219
+ it('should recolor an array of colors with valid options', () => {
220
+ const colors = getTestColors();
221
+ recolorArray(colors, { rotate: 120 });
222
+ expect(colors.map((c) => c.asHex())).toEqual([
223
+ '#55FFAA00',
224
+ '#AA00FF55',
225
+ '#FF5500AA',
226
+ '#00AA55',
227
+ '#55AA77',
228
+ ]);
229
+ });
230
+
231
+ it('should not alter the array if options are invalid', () => {
232
+ const colors = getTestColors();
233
+ const original = [...colors];
234
+ recolorArray(colors, {});
235
+ expect(colors).toEqual(original);
236
+ });
237
+
238
+ it('should handle multiple transformations on the same array', () => {
239
+ const colors = getTestColors();
240
+ recolorArray(colors, { saturate: 0.5 });
241
+ expect(colors.map((c) => c.asHex())).toEqual([
242
+ '#FFAA5500',
243
+ '#00FFAA55',
244
+ '#5500FFAA',
245
+ '#AA5500',
246
+ '#BF7340',
247
+ ]);
248
+
249
+ recolorArray(colors, { brightness: 0.5 });
250
+ expect(colors.map((c) => c.asHex())).toEqual([
251
+ '#FFD5AA00',
252
+ '#80FFD555',
253
+ '#AA80FFAA',
254
+ '#D5AA80',
255
+ '#DFB99F',
256
+ ]);
257
+ });
258
+ });
259
+
260
+ describe('CachedRecolor', () => {
261
+ it('should apply recolor transformations and cache the results', () => {
262
+ const cachedRecolor = new CachedRecolor({ rotate: 120 });
263
+ const color = Color.parse('#FA50');
264
+ const recolored = cachedRecolor.do(color);
265
+ expect(recolored.asHex()).toBe('#55FFAA00');
266
+
267
+ // Verify cached result
268
+ const cachedResult = cachedRecolor.do(color);
269
+ expect(cachedResult).toBe(recolored); // Cached object should be the same instance
270
+ });
271
+
272
+ it('should skip recoloring if options are invalid', () => {
273
+ const cachedRecolor = new CachedRecolor({});
274
+ const color = Color.parse('#FA50');
275
+ const recolored = cachedRecolor.do(color);
276
+ expect(recolored).toBe(color); // No changes applied
277
+ });
278
+
279
+ it('should handle multiple recoloring operations', () => {
280
+ const cachedRecolor = new CachedRecolor({ saturate: 0.5 });
281
+ const colors = getTestColors();
282
+
283
+ const recoloredColors = colors.map((color) => cachedRecolor.do(color));
284
+ expect(recoloredColors.map((c) => c.asHex())).toEqual([
285
+ '#FFAA5500',
286
+ '#00FFAA55',
287
+ '#5500FFAA',
288
+ '#AA5500',
289
+ '#BF7340',
290
+ ]);
291
+ });
292
+ });
293
+
294
+ function getTestColors(): Color[] {
295
+ return [
296
+ Color.parse('#FA50'),
297
+ Color.parse('#0FA5'),
298
+ Color.parse('#50FA'),
299
+ Color.parse('#A50F'),
300
+ Color.parse('#A75F'),
301
+ ];
302
+ }
303
+
304
+ function colors2string(colors: Color[]): string {
305
+ return colors.map(c => c.asHex().slice(1)).join(',');
306
+ }
@@ -0,0 +1,110 @@
1
+ import { Color } from '../color/index.js';
2
+
3
+ export interface RecolorOptions {
4
+ // If true, inverts all colors.
5
+ invertBrightness?: boolean;
6
+
7
+ // Rotate the hue of all colors (in degrees).
8
+ rotate?: number;
9
+
10
+ // Adjusts the saturation level of all colors. Positive values increase saturation, negative values decrease it.
11
+ saturate?: number;
12
+
13
+ // Adjusts the gamma of all colors. Affects the brightness in a non-linear manner.
14
+ gamma?: number;
15
+
16
+ // Adjusts the contrast of all colors. Higher values produce more contrast.
17
+ contrast?: number;
18
+
19
+ // Adjusts the brightness of all colors. Positive values make it brighter, negative values make it darker.
20
+ brightness?: number;
21
+
22
+ // Specifies the intensity of the tinting effect. Ranges from 0 (no effect) to 1 (full effect).
23
+ tint?: number;
24
+
25
+ // Specifies the color used for tinting, in a string format (e.g., '#FF0000').
26
+ tintColor?: string;
27
+ }
28
+
29
+ export function getDefaultRecolorFlags(): RecolorOptions {
30
+ return {
31
+ invertBrightness: false,
32
+ rotate: 0,
33
+ saturate: 0,
34
+ gamma: 1,
35
+ contrast: 1,
36
+ brightness: 0,
37
+ tint: 0,
38
+ tintColor: '#FF0000',
39
+ };
40
+ }
41
+
42
+ function isValidRecolorOptions(opt?: RecolorOptions): opt is RecolorOptions {
43
+ if (!opt) return false;
44
+ if ((opt.invertBrightness != null) && opt.invertBrightness) return true;
45
+ if ((opt.rotate != null) && (opt.rotate !== 0)) return true;
46
+ if ((opt.saturate != null) && (opt.saturate !== 0)) return true;
47
+ if ((opt.gamma != null) && (opt.gamma !== 1)) return true;
48
+ if ((opt.contrast != null) && (opt.contrast !== 1)) return true;
49
+ if ((opt.brightness != null) && (opt.brightness !== 0)) return true;
50
+ if ((opt.tint != null) && (opt.tint !== 0)) return true;
51
+ if ((opt.tintColor != null) && (opt.tintColor !== '#FF0000')) return true;
52
+ return false;
53
+ }
54
+
55
+ export function recolorObject(colors: Record<string, Color>, opt?: RecolorOptions): void {
56
+ if (!isValidRecolorOptions(opt)) return;
57
+
58
+ for (const [k, c] of Object.entries(colors)) {
59
+ colors[k] = recolor(c, opt);
60
+ }
61
+ }
62
+
63
+ export function recolorArray(colors: Color[], opt?: RecolorOptions): void {
64
+ if (!isValidRecolorOptions(opt)) return;
65
+
66
+ for (let i = 0; i < colors.length; i++) {
67
+ colors[i] = recolor(colors[i], opt);
68
+ }
69
+ }
70
+
71
+ export class CachedRecolor {
72
+ private readonly skip: boolean;
73
+
74
+ private readonly opt?: RecolorOptions;
75
+
76
+ private readonly cache: Map<string, Color>;
77
+
78
+ public constructor(opt?: RecolorOptions) {
79
+ this.skip = !isValidRecolorOptions(opt);
80
+ this.cache = new Map();
81
+ this.opt = opt;
82
+ }
83
+
84
+ public do(color: Color): Color {
85
+ if (this.skip) return color;
86
+
87
+ const key = color.asHex();
88
+
89
+ const result = this.cache.get(key);
90
+ if (result) return result;
91
+
92
+ color = recolor(color, this.opt);
93
+ this.cache.set(key, color);
94
+ return color;
95
+ }
96
+ }
97
+
98
+ export function recolor(color: Color, opt?: RecolorOptions): Color {
99
+ if (!isValidRecolorOptions(opt)) return color;
100
+
101
+ if (opt.invertBrightness ?? false) color = color.invertLuminosity();
102
+ if ((opt.rotate !== undefined) && (opt.rotate !== 0)) color = color.rotateHue(opt.rotate);
103
+ if ((opt.saturate !== undefined) && (opt.saturate !== 0)) color = color.saturate(opt.saturate);
104
+ if ((opt.gamma !== undefined) && (opt.gamma !== 1)) color = color.gamma(opt.gamma);
105
+ if ((opt.contrast !== undefined) && (opt.contrast !== 1)) color = color.contrast(opt.contrast);
106
+ if ((opt.brightness !== undefined) && (opt.brightness !== 0)) color = color.brightness(opt.brightness);
107
+ if ((opt.tint !== undefined) && (opt.tintColor !== undefined) && (opt.tint !== 0)) color = color.tint(opt.tint, Color.parse(opt.tintColor));
108
+
109
+ return color;
110
+ }
@@ -0,0 +1,103 @@
1
+
2
+ import { Color } from '../color/index.js';
3
+ import type { StyleRules, StyleRulesOptions } from './types.js';
4
+ import { StyleBuilder } from './style_builder.js';
5
+ import { VectorSourceSpecification } from '@maplibre/maplibre-gl-style-spec';
6
+ import Colorful from '../styles/colorful.js';
7
+
8
+ // Mock class for abstract class StyleBuilder
9
+ class MockStyleBuilder extends Colorful {
10
+ public readonly name = 'mock';
11
+
12
+ public defaultFonts = { regular: 'Arial', bold: 'Courier' };
13
+
14
+ public invertColors(): void {
15
+ this.transformDefaultColors(color => color.invert());
16
+ }
17
+
18
+ protected getStyleRules(opt: StyleRulesOptions): StyleRules {
19
+ for (const color of Object.values(opt.colors)) if (!(color instanceof Color)) throw Error();
20
+ for (const font of Object.values(opt.fonts)) if (typeof font !== 'string') throw Error();
21
+
22
+ return {
23
+ 'water-area': {
24
+ textColor: opt.colors.land,
25
+ textSize: 12,
26
+ textFont: opt.fonts.regular,
27
+ },
28
+ };
29
+ }
30
+ }
31
+
32
+ describe('StyleBuilder', () => {
33
+ let builder: MockStyleBuilder;
34
+
35
+ beforeEach(() => {
36
+ builder = new MockStyleBuilder();
37
+ });
38
+
39
+ it('should create an instance of StyleBuilder', () => {
40
+ expect(builder).toBeInstanceOf(StyleBuilder);
41
+ });
42
+
43
+ it('should build a MaplibreStyle object', () => {
44
+ const style = builder.build();
45
+ expect(style).toBeDefined();
46
+ expect(style).toHaveProperty('name');
47
+ expect(style).toHaveProperty('layers');
48
+ expect(style).toHaveProperty('glyphs');
49
+ expect(style).toHaveProperty('sprite');
50
+ });
51
+
52
+ it('should transform colors correctly', () => {
53
+ const initialColor = Color.parse(builder.defaultColors.land).asHex();
54
+ builder.invertColors();
55
+ const newColor = Color.parse(builder.defaultColors.land).asHex();
56
+ expect(newColor).not.toBe(initialColor);
57
+ expect(newColor).toBe(Color.parse(initialColor).invert().asHex());
58
+ });
59
+
60
+ it('should create default options', () => {
61
+ expect(builder.getDefaultOptions()).toStrictEqual({
62
+ baseUrl: '',
63
+ colors: expect.any(Object),
64
+ fonts: { regular: 'Arial', bold: 'Courier' },
65
+ glyphs: '',
66
+ hideLabels: false,
67
+ language: undefined,
68
+ recolor: {
69
+ brightness: 0,
70
+ contrast: 1,
71
+ gamma: 1,
72
+ invertBrightness: false,
73
+ rotate: 0,
74
+ saturate: 0,
75
+ tint: 0,
76
+ tintColor: '#FF0000',
77
+ },
78
+ sprite: '',
79
+ tiles: [],
80
+ });
81
+ });
82
+
83
+ describe('build method', () => {
84
+ it('should create a style object', () => {
85
+ const style = builder.build();
86
+ expect(style).toBeDefined();
87
+ expect(style).toHaveProperty('layers');
88
+ expect(style).toHaveProperty('name');
89
+ expect(style).toHaveProperty('glyphs');
90
+ expect(style).toHaveProperty('sprite');
91
+ });
92
+
93
+ it('should resolve urls correctly', () => {
94
+ const style = builder.build({ baseUrl: 'https://my.base.url/' });
95
+ expect(style.glyphs).toBe('https://my.base.url/assets/glyphs/{fontstack}/{range}.pbf');
96
+ expect(style.sprite).toStrictEqual([{ id: 'basics', url: 'https://my.base.url/assets/sprites/basics/sprites' }]);
97
+
98
+ const source = style.sources['versatiles-shortbread'] as VectorSourceSpecification;
99
+ expect(source).toHaveProperty('tiles');
100
+ expect(source.tiles).toStrictEqual(['https://my.base.url/tiles/osm/{z}/{x}/{y}']);
101
+ });
102
+ });
103
+ });