Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 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
48 changes: 48 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,53 @@
"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 multiply by the raw elevation in meters for a custom unit."
},
"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."
},
"minzoom": {
"type": "number",
"default": 0,
"doc": "Minimum zoom level for which tiles are available. By default, this will be inherited from the `raster-dem` source."
},
"maxzoom": {
"type": "number",
"default": 22,
"doc": "Maximum zoom level for which tiles are available. When zoomed in past `maxzoom` for the `raster-dem` source, contours will be generated by smoothly overzooming the DEM tiles."
},
"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
259 changes: 259 additions & 0 deletions src/validate/validate_contour_source.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
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, maxzoom: '1' as any, minzoom: '2' as any, overzoom: '3' as any}, styleSpec: v8, style: {} as any,
sourceName: 'contour-source'
});
expect(errors).toHaveLength(8);
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, 'maxzoom', 'number', 'string');
checkErrorMessage(errors[6].message, 'minzoom', 'number', 'string');
checkErrorMessage(errors[7].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,
maxzoom: 16,
minzoom: 4,
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