Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
## main

### ✨ Features and improvements
- _...Add new stuff here..._

- Add new `contour` source type that renders contour lines from a `raster-dem` source [#623](https://github.com/maplibre/maplibre-style-spec/pull/623)

### 🐞 Bug fixes

Expand Down
20 changes: 20 additions & 0 deletions build/generate-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,26 @@ function createSourcesContent() {
}
}
},
'contour': {
doc: 'Contour lines generated from a [\`raster-dem\`](#raster-dem) source.',
example: {
'maplibre-terrain-rgb': {
'type': 'raster-dem',
'encoding': 'mapbox',
'tiles': [
'http://a.example.com/dem-tiles/{z}/{x}/{y}.png'
],
},
'contour': {
'type': 'contour',
'source': 'maplibre-terrain-rgb'
}
},
'sdk-support': {
'basic functionality': {
}
}
},
geojson: {
doc: 'A [GeoJSON](http://geojson.org/) source. Data must be provided via a \`"data"\` property, whose value can be a URL or inline GeoJSON. When using in a browser, the GeoJSON data must be on the same domain as the map or served with [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) headers.',
example: {
Expand Down
6 changes: 5 additions & 1 deletion build/generate-style-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,14 @@ ${objectDeclaration('TerrainSpecification', spec.terrain)}

${spec.source.map(key => {
let str = objectDeclaration(sourceTypeName(key), spec[key]);
// This are done in order to overcome the type system's inability to express these types:
if (sourceTypeName(key) === 'GeoJSONSourceSpecification') {
// This is done in order to overcome the type system's inability to express this type:
str = str.replace(/unknown/, 'GeoJSON.GeoJSON | string');
}
if (sourceTypeName(key) === 'ContourSourceSpecification') {
str = str.replace(/("unit"\?: )unknown/, '$1"meters" | "feet" | number');
str = str.replaceAll(/unknown/g, 'PropertyValueSpecification<number>');
}
return str;
}).join('\n\n')}

Expand Down
4 changes: 2 additions & 2 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,11 @@ function validSchema(k, v, obj, ref, version, kind) {
if (Array.isArray(obj.type) || typeof obj.type === 'string') {
// schema must have only known keys
for (const attr in obj) {
expect(keys.indexOf(attr) !== -1).toBeTruthy();
expect(keys).toContain(attr);
}

// schema type must be js native, 'color', or present in ref root object.
expect(types.indexOf(obj.type) !== -1).toBeTruthy();
expect(types).toContain(obj.type);

// schema type is an enum, it must have 'values' and they must be
// objects (>=v8) or scalars (<=v7). If objects, check that doc key
Expand Down
38 changes: 38 additions & 0 deletions src/reference/v8.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
"source_vector",
"source_raster",
"source_raster_dem",
"source_contour",
"source_geojson",
"source_video",
"source_image"
Expand Down Expand Up @@ -445,6 +446,43 @@
"doc": "Other keys to configure the data source."
}
},
"source_contour": {
"type": {
"required": true,
"type": "enum",
"values": {
"contour": {
"doc": "Contour lines derived from a [`raster-dem`](#raster-dem) source"
}
},
"doc": "The type of the source."
},
"source": {
"type": "string",
"doc": "ID of the [`raster-dem`](#raster-dem) source for the contour lines.",
"required": true
},
"unit": {
"type": "*",
"default": "meters",
"doc": "Elevation unit of the generated contour lines: `\"feet\"`, `\"meters\"`, or a number to specify the length of a custom unit to divide raw elevation in meters by."
},
"intervals": {
"type": "*",
"default": 100,
"doc": "Vertical spacing between contour lines in the unit specified. This can be a constant value like `100` or an expression that changes by zoom level like `[\"step\", [\"zoom\"], 100, 12, 50]` to use 100 at z11 and below or 50 at z12 and higher."
},
"majorMultiplier": {
"type": "*",
"default": 5,
"doc": "Set `major=true` tag on every Nth contour line to help create \"index\" contours. This can be a constant like `5` or an expression that changes by zoom level like `[\"step\", [\"zoom\"], 5, 12, 10]` to use 5 at z11 and below or 10 at z12 and higher."
},
"overzoom": {
"type": "number",
"default": 1,
"doc": "Generate contours at each zoom from `raster-dem` tiles at a lower zoom level so they appear smoother and to limit overfetching border tiles. For example `overzoom: 1` uses z13 `raster-dem` tiles to render z14 contour lines."
}
},
"source_geojson": {
"type": {
"required": true,
Expand Down
255 changes: 255 additions & 0 deletions src/validate/validate_contour_source.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import validateSpec from './validate';
import v8 from '../reference/v8.json' assert {type: 'json'};
import validateContourSource from './validate_contour_source';
import {ContourSourceSpecification, PropertyValueSpecification, StyleSpecification} from '../types.g';

function checkErrorMessage(message: string, key: string, expectedType: string, foundType: string) {
expect(message).toContain(key);
expect(message).toContain(expectedType);
expect(message).toContain(foundType);
}

describe('Validate source_contour', () => {
test('Should pass when value is undefined', () => {
const errors = validateContourSource({validateSpec, value: undefined, styleSpec: v8, style: {} as any});
expect(errors).toHaveLength(0);
});

test('Should return error when value is not an object', () => {
const errors = validateContourSource({validateSpec, value: '' as unknown as ContourSourceSpecification, styleSpec: v8, style: {} as any});
expect(errors).toHaveLength(1);
expect(errors[0].message).toContain('object');
expect(errors[0].message).toContain('expected');
});

test('Should return error in case of unknown property', () => {
const errors = validateContourSource({validateSpec, value: {a: 1} as any, styleSpec: v8, style: {} as any});
expect(errors).toHaveLength(2);
expect(errors[1].message).toContain('a');
expect(errors[1].message).toContain('unknown');
});

test('Should return errors according to spec violations', () => {
const errors = validateContourSource({
validateSpec,
value: {type: 'contour', source: {} as any, unit: 'garbage' as any, intervals: {} as any, majorMultiplier: {} as any, overzoom: '3' as any}, styleSpec: v8, style: {} as any,
sourceName: 'contour-source'
});
expect(errors).toHaveLength(6);
checkErrorMessage(errors[0].message, 'source', 'raster-dem', 'contour-source');
checkErrorMessage(errors[1].message, 'source', 'string', 'object');
checkErrorMessage(errors[2].message, 'unit', '[meters, feet] or number', 'garbage');
checkErrorMessage(errors[3].message, 'intervals', 'literal', 'Bare object');
checkErrorMessage(errors[4].message, 'majorMultiplier', 'literal', 'Bare object');
checkErrorMessage(errors[5].message, 'overzoom', 'number', 'string');
});

test('Should return errors if interval or major definitions are malformed', () => {
const contour: ContourSourceSpecification = {
type: 'contour',
source: 'dem',
unit: 1.5,
intervals: ['step', ['zoom'], 1, 10],
majorMultiplier: ['step', ['zoom'], 1, 10, 3, 9, 4]
};
const style: StyleSpecification = {
sources: {
dem: {
type: 'raster-dem',
maxzoom: 11
},
contour
},
version: 8,
layers: []
};
const errors = validateContourSource({
validateSpec,
value: contour,
styleSpec: v8,
style
});
expect(errors).toHaveLength(2);
checkErrorMessage(errors[0].message, 'intervals', 'at least 4 arguments', 'only 3');
checkErrorMessage(errors[1].message, 'majorMultiplier', 'strictly', 'ascending');
});

test('Should return errors when source is missing', () => {
const contour: ContourSourceSpecification = {
type: 'contour',
source: 'dem',
unit: 'feet',
};
const style: StyleSpecification = {
sources: {
contour
},
version: 8,
layers: []
};
const errors = validateContourSource({
validateSpec,
value: contour,
styleSpec: v8,
style,
sourceName: 'contour'
});
expect(errors).toHaveLength(1);
checkErrorMessage(errors[0].message, 'source', 'raster-dem', 'contour');
});

test('Should return errors when source has wrong type', () => {
const contour: ContourSourceSpecification = {
type: 'contour',
source: 'dem',
unit: 'feet',
};
const style: StyleSpecification = {
sources: {
dem: {
type: 'raster'
},
contour
},
version: 8,
layers: []
};
const errors = validateContourSource({
validateSpec,
value: contour,
styleSpec: v8,
style,
sourceName: 'contour'
});
expect(errors).toHaveLength(1);
checkErrorMessage(errors[0].message, 'source', 'raster-dem', 'contour');
});

test('Should pass if everything is according to spec', () => {
const contour: ContourSourceSpecification = {
type: 'contour',
source: 'dem',
intervals: ['step', ['zoom'], 5, 10, 3],
majorMultiplier: 500,
overzoom: 2,
unit: 'feet',
};
const style: StyleSpecification = {
sources: {
dem: {
type: 'raster-dem',
maxzoom: 11
},
contour
},
version: 8,
layers: []
};
const errors = validateContourSource({
validateSpec,
value: contour,
styleSpec: v8,
style
});
expect(errors).toHaveLength(0);
});

test('Should pass if everything is according to spec using numeric unit', () => {
const contour: ContourSourceSpecification = {
type: 'contour',
source: 'dem',
unit: 1.5,
};
const style: StyleSpecification = {
sources: {
dem: {
type: 'raster-dem',
maxzoom: 11
},
contour
},
version: 8,
layers: []
};
const errors = validateContourSource({
validateSpec,
value: contour,
styleSpec: v8,
style
});
expect(errors).toHaveLength(0);
});

const goodExpressions: Array<PropertyValueSpecification<number>> = [
5,
['step', ['zoom'], 100, 10, 50],
['interpolate', ['linear'], ['zoom'], 1, 5, 10, 10],
['*', ['zoom'], 10],
['*', 2, 3],
];

for (const expr of goodExpressions) {
test(`Expression should be allowed: ${JSON.stringify(expr)}`, () => {
const contour: ContourSourceSpecification = {
type: 'contour',
source: 'dem',
intervals: expr,
majorMultiplier: expr,
};
const style: StyleSpecification = {
sources: {
dem: {
type: 'raster-dem'
},
contour
},
version: 8,
layers: []
};
expect(validateContourSource({
validateSpec,
value: contour,
styleSpec: v8,
style
})).toHaveLength(0);
});
}

const badExpressions: Array<PropertyValueSpecification<number>> = [
['geometry-type'],
['get', 'x'],
['interpolate', ['linear'], ['get', 'prop'], 1, 5, 10, 10],
['feature-state', 'key'],
['step', ['zoom'], 100, 10, ['get', 'value']],
];

for (const expr of badExpressions) {
test(`Expression should not be allowed: ${JSON.stringify(expr)}`, () => {
const contour: ContourSourceSpecification = {
type: 'contour',
source: 'dem',
intervals: expr,
majorMultiplier: expr,
};
const style: StyleSpecification = {
sources: {
dem: {
type: 'raster-dem'
},
contour
},
version: 8,
layers: []
};
const errors = validateContourSource({
validateSpec,
value: contour,
styleSpec: v8,
style
});
expect(errors).toHaveLength(2);
checkErrorMessage(errors[0].message, 'intervals', '\"zoom\"-based expressions', 'contour source expressions');
checkErrorMessage(errors[1].message, 'majorMultiplier', '\"zoom\"-based expressions', 'contour source expressions');
});
}
});
Loading