Skip to content

Commit

Permalink
feat: simplify API
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelKreil committed Nov 28, 2023
1 parent d37b1cf commit b7019d9
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 126 deletions.
46 changes: 18 additions & 28 deletions browser_test/browser.test.ts
Original file line number Diff line number Diff line change
@@ -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}}`));
});
});
}
Expand Down
31 changes: 10 additions & 21 deletions scripts/build-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';



Expand All @@ -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 {
Expand Down
40 changes: 22 additions & 18 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -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());
Expand All @@ -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',
Expand All @@ -54,7 +58,7 @@ describe('guessStyle', () => {


it('should build vector styles', () => {
const style = builderClasses.guessStyle({
const style = builders.guessStyle({
type: 'vector',
tiles: [],
format: 'pbf',
Expand Down
23 changes: 18 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
2 changes: 1 addition & 1 deletion src/lib/recolor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
3 changes: 1 addition & 2 deletions src/lib/style_builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
91 changes: 45 additions & 46 deletions src/lib/style_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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':
Expand All @@ -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();
}
Expand Down
Loading

0 comments on commit b7019d9

Please sign in to comment.