From b7019d963bf9c6563cb4ee6fc7475d7afe22c21e Mon Sep 17 00:00:00 2001 From: Michael Kreil <github@michael-kreil.de> Date: Tue, 28 Nov 2023 14:03:56 +0100 Subject: [PATCH] feat: simplify API --- browser_test/browser.test.ts | 46 +++++++----------- scripts/build-styles.ts | 31 ++++-------- src/index.test.ts | 40 ++++++++------- src/index.ts | 23 +++++++-- src/lib/recolor.test.ts | 2 +- src/lib/style_builder.test.ts | 3 +- src/lib/style_builder.ts | 91 +++++++++++++++++------------------ src/lib/style_guesser.ts | 10 ++-- src/lib/types.ts | 12 +++++ 9 files changed, 132 insertions(+), 126 deletions(-) diff --git a/browser_test/browser.test.ts b/browser_test/browser.test.ts index 0674ef6..4c1a473 100644 --- a/browser_test/browser.test.ts +++ b/browser_test/browser.test.ts @@ -1,45 +1,35 @@ import type { SymbolLayerSpecification } from '@maplibre/maplibre-gl-style-spec'; import * as V from '../release/versatiles-style.js'; -import type { LanguageSuffix } from '../src/lib/types.js'; +import type { LanguageSuffix, MaplibreStyle, StylemakerOptions } from '../src/lib/types.js'; +import type Colorful from '../src/style/colorful.js'; +import type StyleBuilder from '../src/lib/style_builder.js'; +import type Graybeard from '../src/style/graybeard.ts'; +import type Neutrino from '../src/style/neutrino.ts'; -type Builder = typeof V.Colorful | typeof V.Neutrino; -type Style = V.Colorful | V.Neutrino; +type Builder<T extends StyleBuilder<T>> = (options?: StylemakerOptions<T>) => MaplibreStyle; +type GenericBuilder = Builder<Colorful> | Builder<Graybeard> | Builder<Neutrino>; -interface StyleTestConfig { - name: string; - builder: Builder; - labelLayers?: RegExp; -} - -[ - { name: 'Colorful', builder: V.Colorful, labelLayers: /^label-(street|place|boundary|transit)-/ }, - { name: 'Graybeard', builder: V.Graybeard, labelLayers: /^label-(street|place|boundary|transit)-/ }, - { name: 'Neutrino', builder: V.Neutrino }, -].forEach(config => { - test(config); -}); - -function test(config: StyleTestConfig): void { +test('Colorful', V.colorful, /^label-(street|place|boundary|transit)-/); +test('Graybeard', V.graybeard, /^label-(street|place|boundary|transit)-/); +test('Neutrino', V.neutrino); - describe('Style: ' + config.name, () => { - let style: Style; - beforeEach(() => style = new config.builder()); +function test(name: string, build: GenericBuilder, labelLayers?: RegExp): void { + describe('Style: ' + name, () => { it('should be buildable', () => { - expect(style.build()).toBeTruthy(); + expect(build()).toBeTruthy(); }); - if (config.labelLayers) { + if (labelLayers) { it('should use correct language suffix', () => { - (['', '_de', '_en'] as LanguageSuffix[]).forEach(langSuffix => { - style.languageSuffix = langSuffix; - const textLayers = style.build().layers - .filter(l => config.labelLayers?.test(l.id)) + (['', '_de', '_en'] as LanguageSuffix[]).forEach(languageSuffix => { + const textLayers = build({ languageSuffix }).layers + .filter(l => labelLayers.test(l.id)) .filter(l => l.type === 'symbol') as SymbolLayerSpecification[]; const layersTextFields = textLayers.map(l => l.layout?.['text-field'] as unknown); expect(layersTextFields) - .toMatchObject(layersTextFields.slice().fill(`{name${langSuffix}}`)); + .toMatchObject(layersTextFields.slice().fill(`{name${languageSuffix}}`)); }); }); } diff --git a/scripts/build-styles.ts b/scripts/build-styles.ts index e2b89ca..01838de 100755 --- a/scripts/build-styles.ts +++ b/scripts/build-styles.ts @@ -2,11 +2,10 @@ import { mkdirSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; -import { Colorful, Graybeard, Neutrino } from '../src/index.js'; +import { colorful, graybeard, neutrino } from '../src/index.js'; import { validateStyleMin } from '@maplibre/maplibre-gl-style-spec'; import { prettyStyleJSON } from './lib/utils.js'; -import type { MaplibreStyle } from '../src/index.js'; -import StyleBuilder from '../src/lib/style_builder.ts'; +import type { MaplibreStyle } from '../src/lib/types.js'; @@ -17,24 +16,14 @@ mkdirSync(dirDst, { recursive: true }); // load styles [ - Colorful, - Graybeard, - Neutrino, -].forEach(styleBuilderClass => { - const styleBuilder = new styleBuilderClass(); - const { name } = styleBuilder; - - styleBuilder.languageSuffix = ''; - produce(name, styleBuilder.build()); - - styleBuilder.languageSuffix = '_en'; - produce(name + '.en', styleBuilder.build()); - - styleBuilder.languageSuffix = '_de'; - produce(name + '.de', styleBuilder.build()); - - styleBuilder.hideLabels = true; - produce(name + '.nolabel', styleBuilder.build()); + { name: 'colorful', build: colorful }, + { name: 'graybeard', build: graybeard }, + { name: 'neutrino', build: neutrino }, +].forEach(({ name, build }) => { + produce(name, build({ languageSuffix: '' })); + produce(name + '.en', build({ languageSuffix: '_en' })); + produce(name + '.de', build({ languageSuffix: '_de' })); + produce(name + '.nolabel', build({ hideLabels: true })); }) function produce(name: string, style: MaplibreStyle): void { diff --git a/src/index.test.ts b/src/index.test.ts index 8a74f6c..758784a 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,23 +1,18 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import * as builderClasses from './index.js'; -import StyleBuilder from './lib/style_builder.js'; +import * as builders from './index.js'; describe('Style Builders', () => { const styles = [ - { name: 'Colorful', builderClass: builderClasses.Colorful }, - { name: 'Graybeard', builderClass: builderClasses.Graybeard }, - { name: 'Neutrino', builderClass: builderClasses.Neutrino }, + { name: 'Colorful', builder: builders.colorful }, + { name: 'Graybeard', builder: builders.graybeard }, + { name: 'Neutrino', builder: builders.neutrino }, ]; - styles.forEach(({ name, builderClass }) => { + styles.forEach(({ name, builder }) => { it(`should create and test an instance of ${name}`, () => { - const builder = new builderClass(); - expect(builder).toBeInstanceOf(StyleBuilder); - expect(typeof builder.name).toBe('string'); - expect(builder.name).toBe(name); + expect(typeof builder).toBe('function'); - builder.baseUrl = 'https://example.org'; - const style = builder.build(); + const style = builder({ baseUrl: 'https://example.org' }); expect(JSON.stringify(style).length).toBeGreaterThan(50000); expect(style.name).toBe('versatiles-' + name.toLowerCase()); @@ -31,16 +26,25 @@ describe('Style Builders', () => { }); describe('Colorful', () => { - const colorful = new builderClasses.Colorful(); - colorful.baseUrl = 'https://dev.null'; - colorful.defaultColors.commercial = '#f00'; - const style = colorful.build(); + const style = builders.colorful({ + baseUrl: 'https://dev.null', + colors: { commercial: '#f00' }, + }); expect(style.glyphs).toBe('https://dev.null/assets/fonts/{fontstack}/{range}.pbf'); + const paint = style.layers.find(l => l.id === 'land-commercial')?.paint; + + expect(paint).toBeDefined(); + if (paint == null) throw Error(); + + expect(paint).toHaveProperty('fill-color'); + if (!('fill-color' in paint)) throw Error(); + + expect(paint['fill-color']).toBe('#ff0000'); }); describe('guessStyle', () => { it('should build raster styles', () => { - const style = builderClasses.guessStyle({ + const style = builders.guessStyle({ type: 'raster', tiles: [], format: 'avif', @@ -54,7 +58,7 @@ describe('guessStyle', () => { it('should build vector styles', () => { - const style = builderClasses.guessStyle({ + const style = builders.guessStyle({ type: 'vector', tiles: [], format: 'pbf', diff --git a/src/index.ts b/src/index.ts index b3b5bfc..009a618 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,21 @@ +import type { MaplibreStyle, StylemakerOptions } from './lib/types.js'; -export type { MaplibreStyle, TileJSONSpecification, TileJSONSpecificationRaster, TileJSONSpecificationVector } from './lib/types.js'; +import Colorful from './style/colorful.js'; +export function colorful(options?: StylemakerOptions<Colorful>): MaplibreStyle { + return new Colorful().build(options); +} + +import Graybeard from './style/graybeard.js'; +export function graybeard(options?: StylemakerOptions<Graybeard>): MaplibreStyle { + return new Graybeard().build(options); +} + +import Neutrino from './style/neutrino.js'; +export function neutrino(options?: StylemakerOptions<Neutrino>): MaplibreStyle { + return new Neutrino().build(options); +} -export { default as guessStyle } from './lib/style_guesser.js'; -export { default as Colorful } from './style/colorful.js'; -export { default as Graybeard } from './style/graybeard.js'; -export { default as Neutrino } from './style/neutrino.js'; +export type { TileJSONSpecification, TileJSONSpecificationRaster, TileJSONSpecificationVector } from './lib/types.js'; + +export { default as guessStyle } from './lib/style_guesser.js'; diff --git a/src/lib/recolor.test.ts b/src/lib/recolor.test.ts index 8b37bc5..ba4442c 100644 --- a/src/lib/recolor.test.ts +++ b/src/lib/recolor.test.ts @@ -225,5 +225,5 @@ export default class TestStyle extends StyleBuilder<TestStyle> { function getDefaultColors(): StylemakerColors<TestStyle> { const style = new TestStyle(); - return style.getColors(); + return style.getColors(style.defaultColors); } diff --git a/src/lib/style_builder.test.ts b/src/lib/style_builder.test.ts index 89ccb85..8964e5d 100644 --- a/src/lib/style_builder.test.ts +++ b/src/lib/style_builder.test.ts @@ -67,8 +67,7 @@ describe('StyleBuilder', () => { }); it('should resolve urls correctly', () => { - builder.baseUrl = 'https://my.base.url/'; - const style: MaplibreStyle = builder.build(); + const style: MaplibreStyle = builder.build({ baseUrl: 'https://my.base.url/' }); expect(style.glyphs).toBe('https://my.base.url/assets/fonts/{fontstack}/{range}.pbf'); expect(style.sprite).toBe('https://my.base.url/assets/sprites/sprites'); diff --git a/src/lib/style_builder.ts b/src/lib/style_builder.ts index 9f0861e..e79dccc 100644 --- a/src/lib/style_builder.ts +++ b/src/lib/style_builder.ts @@ -5,35 +5,20 @@ import { decorate } from './decorator.js'; import { getDefaultRecolorFlags, recolor } from './recolor.js'; import { deepClone } from './utils.js'; import type { - LanguageSuffix, MaplibreLayer, MaplibreLayerDefinition, MaplibreStyle, - RecolorOptions, StyleRules, StyleRulesOptions, StylemakerColorKeys, StylemakerColorStrings, StylemakerColors, StylemakerFontStrings, + StylemakerOptions, } from './types.js'; // Stylemaker class definition export default abstract class StyleBuilder<Subclass extends StyleBuilder<Subclass>> { - public baseUrl: string; - - public glyphsUrl = '/assets/fonts/{fontstack}/{range}.pbf'; - - public spriteUrl = '/assets/sprites/sprites'; - - public tilesUrls: string[] = ['/tiles/osm/{z}/{x}/{y}']; - - public hideLabels = false; - - public languageSuffix: LanguageSuffix = ''; - - public recolor: RecolorOptions = getDefaultRecolorFlags(); - readonly #sourceName = 'versatiles-shortbread'; public abstract readonly name: string; @@ -42,44 +27,51 @@ export default abstract class StyleBuilder<Subclass extends StyleBuilder<Subclas public abstract readonly defaultFonts: StylemakerFontStrings<Subclass>; - // Constructor - public constructor() { - try { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.baseUrl = document?.location?.href; - } catch (e) { } + public build(options?: StylemakerOptions<Subclass>): MaplibreStyle { + + options ??= {}; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.baseUrl ??= 'https://tiles.versatiles.org'; + const baseUrl = options.baseUrl ?? globalThis?.document?.location?.href ?? 'https://tiles.versatiles.org'; + const glyphsUrl = options.glyphsUrl ?? '/assets/fonts/{fontstack}/{range}.pbf'; + + const spriteUrl = options.spriteUrl ?? '/assets/sprites/sprites'; + const tilesUrls = options.tilesUrls ?? ['/tiles/osm/{z}/{x}/{y}']; + const hideLabels = options.hideLabels ?? false; + const languageSuffix = options.languageSuffix ?? ''; + const recolorOptions = options.recolor ?? getDefaultRecolorFlags(); + + const colors = this.getColors(this.defaultColors); + if (options.colors) { + for (const key in options.colors) colors[key] = Color(options.colors[key]); + } - } + // transform colors + recolor(colors, recolorOptions); + + const fonts = deepClone(this.defaultColors); + if (options.fonts) { + for (const key in options.fonts) { + const fontName = options.fonts[key]; + if (fontName != null) fonts[key] = fontName; + } + } - public getColors(): StylemakerColors<Subclass> { - const entriesString = Object.entries(this.defaultColors) as [StylemakerColorKeys<Subclass>, StylemakerColorStrings<Subclass>][]; - const entriesColor = entriesString.map(([key, value]) => [key, Color(value)]) as [StylemakerColorKeys<Subclass>, StylemakerColors<Subclass>][]; - const result = Object.fromEntries(entriesColor) as StylemakerColors<Subclass>; - return result; - } - public build(): MaplibreStyle { // get empty shortbread style const style: MaplibreStyle = getShortbreadTemplate(); - const colors = this.getColors(); - - // transform colors - recolor(colors, this.recolor); - const styleRuleOptions: StyleRulesOptions<typeof this> = { colors, fonts: deepClone(this.defaultFonts), - languageSuffix: this.languageSuffix, + languageSuffix, }; // get layer style rules from child class const layerStyleRules = this.getStyleRules(styleRuleOptions); // get shortbread layers - const layerDefinitions: MaplibreLayerDefinition[] = getShortbreadLayers({ languageSuffix: this.languageSuffix }); + const layerDefinitions: MaplibreLayerDefinition[] = getShortbreadLayers({ languageSuffix }); let layers: MaplibreLayer[] = layerDefinitions.map(layer => { switch (layer.type) { case 'background': @@ -98,29 +90,36 @@ export default abstract class StyleBuilder<Subclass extends StyleBuilder<Subclas layers = decorate(layers, layerStyleRules); // hide labels, if wanted - if (this.hideLabels) layers = layers.filter(l => l.type !== 'symbol'); + if (hideLabels) layers = layers.filter(l => l.type !== 'symbol'); style.layers = layers; style.name = 'versatiles-' + this.name.toLowerCase(); - style.glyphs = resolveUrl(this.baseUrl, this.glyphsUrl); - style.sprite = resolveUrl(this.baseUrl, this.spriteUrl); + style.glyphs = resolveUrl(baseUrl, glyphsUrl); + style.sprite = resolveUrl(baseUrl, spriteUrl); const source = style.sources[this.#sourceName]; - if ('tiles' in source) source.tiles = this.tilesUrls.map(url => resolveUrl(this.baseUrl, url)); + if ('tiles' in source) source.tiles = tilesUrls.map(url => resolveUrl(baseUrl, url)); return style; - function resolveUrl(baseUrl: string, url: string): string { - if (!Boolean(baseUrl)) return url; - url = new URL(url, baseUrl).href; + function resolveUrl(base: string, url: string): string { + if (!Boolean(base)) return url; + url = new URL(url, base).href; url = url.replace(/%7B/gi, '{'); url = url.replace(/%7D/gi, '}'); return url; } } + public getColors(colors: StylemakerColorStrings<Subclass>): StylemakerColors<Subclass> { + const entriesString = Object.entries(colors) as [StylemakerColorKeys<Subclass>, StylemakerColorStrings<Subclass>][]; + const entriesColor = entriesString.map(([key, value]) => [key, Color(value)]) as [StylemakerColorKeys<Subclass>, StylemakerColors<Subclass>][]; + const result = Object.fromEntries(entriesColor) as StylemakerColors<Subclass>; + return result; + } + protected transformDefaultColors(callback: (color: Color) => Color): void { - const colors = this.getColors(); + const colors = this.getColors(this.defaultColors); for (const key in colors) { this.defaultColors[key] = callback(colors[key]).hexa(); } diff --git a/src/lib/style_guesser.ts b/src/lib/style_guesser.ts index 073ea6b..d8382a1 100644 --- a/src/lib/style_guesser.ts +++ b/src/lib/style_guesser.ts @@ -2,10 +2,10 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type { BackgroundLayerSpecification, CircleLayerSpecification, FillLayerSpecification, LineLayerSpecification } from '@maplibre/maplibre-gl-style-spec'; -import { Colorful } from '../index.js'; import type { MaplibreStyle, TileJSONSpecification, TileJSONSpecificationRaster, TileJSONSpecificationVector, VectorLayer } from './types.js'; import { isTileJSONSpecification } from './types.js'; import randomColorGenerator from './random_color.js'; +import Colorful from '../style/colorful.js'; @@ -39,10 +39,10 @@ function isShortbread(spec: TileJSONSpecificationVector): boolean { } function getShortbreadStyle(spec: TileJSONSpecificationVector): MaplibreStyle { - const builder = new Colorful(); - builder.hideLabels = true; - builder.tilesUrls = spec.tiles; - return builder.build(); + return new Colorful().build({ + hideLabels: true, + tilesUrls: spec.tiles, + }); } function getInspectorStyle(spec: TileJSONSpecificationVector): MaplibreStyle { diff --git a/src/lib/types.ts b/src/lib/types.ts index 491e751..f483e36 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -97,6 +97,18 @@ export interface RecolorOptions { tintColor?: string; } +export interface StylemakerOptions<T extends StyleBuilder<T>> { + baseUrl?: string; + glyphsUrl?: string; + spriteUrl?: string; + tilesUrls?: string[]; + hideLabels?: boolean; + languageSuffix?: LanguageSuffix; + colors?: Partial<StylemakerColorStrings<T>>; + fonts?: Partial<StylemakerFontStrings<T>>; + recolor?: RecolorOptions; +} + export function isTileJSONSpecification(obj: unknown): obj is TileJSONSpecification {