Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add contour source #623

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
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