diff --git a/examples/config.json b/examples/config.json index 318b70e726..6b7648a23a 100644 --- a/examples/config.json +++ b/examples/config.json @@ -31,6 +31,7 @@ "Vector tiles": { "vector_tile_raster_3d": "Raster on 3D map", "vector_tile_raster_2d": "Raster on 2D map", + "vector_tile_mapbox_raster": "Mapbox Vector Tiles to raster", "vector_tile_3d_mesh": "Vector tile to 3d objects", "vector_tile_3d_mesh_mapbox": "Mapbox Vector tile to 3d objects", "vector_tile_dragndrop": "Drag and drop a style" diff --git a/examples/source_file_kml_raster_usgs.html b/examples/source_file_kml_raster_usgs.html index db83222b2b..b755f0890e 100644 --- a/examples/source_file_kml_raster_usgs.html +++ b/examples/source_file_kml_raster_usgs.html @@ -69,7 +69,6 @@ } var kmlStyle = { - zoom: { min: 10, max: 20 }, text: { field: '{name}', haloColor: 'black', diff --git a/examples/vector_tile_mapbox_raster.html b/examples/vector_tile_mapbox_raster.html new file mode 100644 index 0000000000..01b704bd0f --- /dev/null +++ b/examples/vector_tile_mapbox_raster.html @@ -0,0 +1,91 @@ + + + Itowns - vector-tiles 2d + + + + + + + + + + +
+ + + + + + + + + + + diff --git a/src/Converter/Feature2Texture.js b/src/Converter/Feature2Texture.js index b197e7fe2b..82c13a50eb 100644 --- a/src/Converter/Feature2Texture.js +++ b/src/Converter/Feature2Texture.js @@ -70,6 +70,9 @@ function drawFeature(ctx, feature, extent, invCtxScale) { for (const geometry of feature.geometries) { if (Extent.intersectsExtent(geometry.extent, extent)) { context.setGeometry(geometry); + if (style.zoom.min > style.context.zoom || style.zoom.max <= style.context.zoom) { + return; + } if ( feature.type === FEATURE_TYPES.POINT && style.point diff --git a/src/Core/Style.js b/src/Core/Style.js index 3e716bd26a..7374c7b35c 100644 --- a/src/Core/Style.js +++ b/src/Core/Style.js @@ -87,14 +87,18 @@ async function loadImage(source) { return (await promise).image; } -function cropImage(img, cropValues = { width: img.naturalWidth, height: img.naturalHeight }) { - canvas.width = cropValues.width; - canvas.height = cropValues.height; +function cropImage(img, cropValues) { + const x = cropValues.x || 0; + const y = cropValues.y || 0; + const width = cropValues.width || img.naturalWidth; + const height = cropValues.height || img.naturalHeight; + canvas.width = width; + canvas.height = height; const ctx = canvas.getContext('2d', { willReadFrequently: true }); ctx.drawImage(img, - cropValues.x || 0, cropValues.y || 0, cropValues.width, cropValues.height, - 0, 0, cropValues.width, cropValues.height); - return ctx.getImageData(0, 0, cropValues.width, cropValues.height); + x, y, width, height, + 0, 0, width, height); + return ctx.getImageData(0, 0, width, height); } function replaceWhitePxl(imgd, color, id) { @@ -158,7 +162,7 @@ function defineStyleProperty(style, category, parameter, userValue, defaultValue const dataValue = style.context.featureStyle?.[category]?.[parameter]; if (dataValue != undefined) { return readExpression(dataValue, style.context); } if (defaultValue instanceof Function) { - return defaultValue(style.context.properties, style.context); + return defaultValue(style.context.properties, style.context) ?? defaultValue; } return defaultValue; }, @@ -298,15 +302,12 @@ function _addIcon(icon, domElement, opt) { } /** - * An object that can contain any properties (order, zoom, fill, stroke, point, + * An object that can contain any properties (zoom, fill, stroke, point, * text or/and icon) and sub properties of a Style.
* Used for the instanciation of a {@link Style}. * * @typedef {Object} StyleOptions * - * @property {Number} [order] - Order of the features that will be associated to - * the style. It can helps sorting and prioritizing features if needed. - * * @property {Object} [zoom] - Level on which to display the feature * @property {Number} [zoom.max] - max level * @property {Number} [zoom.min] - min level @@ -464,8 +465,6 @@ function _addIcon(icon, domElement, opt) { * The first parameter of functions used to set `Style` properties is always an object containing * the properties of the features displayed with the current `Style` instance. * - * @property {Number} order - Order of the features that will be associated to - * the style. It can helps sorting and prioritizing features if needed. * @property {Object} fill - Polygons and fillings style. * @property {String|Function|THREE.Color} fill.color - Defines the main color of the filling. Can be * any [valid color @@ -615,15 +614,13 @@ function _addIcon(icon, domElement, opt) { class Style { /** * @param {StyleOptions} [params={}] An object that contain any properties - * (order, zoom, fill, stroke, point, text or/and icon) + * (zoom, fill, stroke, point, text or/and icon) * and sub properties of a Style ({@link StyleOptions}). */ constructor(params = {}) { this.isStyle = true; this.context = new StyleContext(); - this.order = params.order || 0; - params.zoom = params.zoom || {}; params.fill = params.fill || {}; params.stroke = params.stroke || {}; @@ -763,12 +760,12 @@ class Style { * set Style from vector tile layer properties. * @param {Object} layer vector tile layer. * @param {Object} sprites vector tile layer. - * @param {Number} [order=0] * @param {Boolean} [symbolToCircle=false] + * @param {Set} warn Set storing all warnings encountered by the source. * * @returns {StyleOptions} containing all properties for itowns.Style */ - static setFromVectorTileLayer(layer, sprites, order = 0, symbolToCircle = false) { + static setFromVectorTileLayer(layer, sprites, symbolToCircle = false, warn) { const style = { fill: {}, stroke: {}, @@ -780,8 +777,6 @@ class Style { layer.layout = layer.layout || {}; layer.paint = layer.paint || {}; - style.order = order; - if (layer.type === 'fill') { const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['fill-color'] || layer.paint['fill-pattern'], { type: 'color' })); style.fill.color = color; @@ -804,7 +799,8 @@ class Style { style.stroke.color = color; style.stroke.opacity = opacity; style.stroke.width = 1.0; - style.stroke.dasharray = []; + } else { + style.stroke.width = 0.0; } } else if (layer.type === 'line') { const prepare = readVectorProperty(layer.paint['line-color'], { type: 'color' }); @@ -820,6 +816,8 @@ class Style { style.point.opacity = opacity; style.point.radius = readVectorProperty(layer.paint['circle-radius']); } else if (layer.type === 'symbol') { + // if symbol we shouldn't draw stroke but defaut value is 1. + style.stroke.width = 0.0; // overlapping order style.text.zOrder = readVectorProperty(layer.layout['symbol-z-order']); if (style.text.zOrder == 'auto') { @@ -840,7 +838,7 @@ class Style { // content style.text.field = readVectorProperty(layer.layout['text-field']); - style.text.wrap = readVectorProperty(layer.layout['text-max-width']); + style.text.wrap = readVectorProperty(layer.layout['text-max-width']);// Units ems style.text.spacing = readVectorProperty(layer.layout['text-letter-spacing']); style.text.transform = readVectorProperty(layer.layout['text-transform']); style.text.justify = readVectorProperty(layer.layout['text-justify']); @@ -861,6 +859,12 @@ class Style { // additional icon const iconImg = readVectorProperty(layer.layout['icon-image']); if (iconImg) { + const cropValueDefault = { + x: 0, + y: 0, + width: 1, + height: 1, + }; try { style.icon.id = iconImg; if (iconImg.stops) { @@ -871,9 +875,23 @@ class Style { if (stop[1].includes('{')) { cropValues = function _(p) { const id = stop[1].replace(/\{(.+?)\}/g, (a, b) => (p[b] || '')).trim(); - cropValues = sprites[id]; + if (cropValues === undefined) { + const warning = `WARNING: "${id}" not found in sprite file`; + if (!warn.has(warning)) { + warn.add(warning); + console.warn(warning); + } + sprites[id] = cropValueDefault;// or return cropValueDefault; + } return sprites[id]; }; + } else if (cropValues === undefined) { + const warning = `WARNING: "${stop[1]}" not found in sprite file`; + if (!warn.has(warning)) { + warn.add(warning); + console.warn(warning); + } + cropValues = cropValueDefault; } return [stop[0], cropValues]; }), @@ -881,19 +899,36 @@ class Style { style.icon.cropValues = iconCropValue; } else { style.icon.cropValues = sprites[iconImg]; - if (iconImg[0].includes('{')) { + if (iconImg.includes('{')) { style.icon.cropValues = function _(p) { const id = iconImg.replace(/\{(.+?)\}/g, (a, b) => (p[b] || '')).trim(); - style.icon.cropValues = sprites[id]; + if (sprites[id] === undefined) { + const warning = `WARNING: "${id}" not found in sprite file`; + if (!warn.has(warning)) { + warn.add(warning); + console.warn(warning); + } + sprites[id] = cropValueDefault;// or return cropValueDefault; + } return sprites[id]; }; + } else if (sprites[iconImg] === undefined) { + const warning = `WARNING: "${iconImg}" not found in sprite file`; + if (!warn.has(warning)) { + warn.add(warning); + console.warn(warning); + } + style.icon.cropValues = cropValueDefault; } } style.icon.source = sprites.source; - style.icon.size = readVectorProperty(layer.layout['icon-size']) || 1; + style.icon.size = readVectorProperty(layer.layout['icon-size']) ?? 1; const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['icon-color'], { type: 'color' })); - style.icon.color = color; - style.icon.opacity = readVectorProperty(layer.paint['icon-opacity']) || (opacity !== undefined && opacity); + // https://docs.mapbox.com/style-spec/reference/layers/#paint-symbol-icon-color + if (iconImg.sdf) { + style.icon.color = color; + } + style.icon.opacity = readVectorProperty(layer.paint['icon-opacity']) ?? (opacity !== undefined && opacity); } catch (err) { err.message = `VTlayer '${layer.id}': argument sprites must not be null when using layer.layout['icon-image']`; throw err; @@ -919,17 +954,16 @@ class Style { * @param {Boolean} canBeFilled - true if feature.type == FEATURE_TYPES.POLYGON. */ applyToCanvasPolygon(txtrCtx, polygon, invCtxScale, canBeFilled) { - const context = this.context; // draw line or edge of polygon - if (this.stroke) { + if (this.stroke.width > 0) { // TO DO add possibility of using a pattern (https://github.com/iTowns/itowns/issues/2210) - this._applyStrokeToPolygon(txtrCtx, invCtxScale, polygon, context); + this._applyStrokeToPolygon(txtrCtx, invCtxScale, polygon); } // fill inside of polygon - if (canBeFilled && this.fill) { + if (canBeFilled && (this.fill.pattern || this.fill.color)) { // canBeFilled can be move to StyleContext in the later PR - this._applyFillToPolygon(txtrCtx, invCtxScale, polygon, context); + this._applyFillToPolygon(txtrCtx, invCtxScale, polygon); } } @@ -937,11 +971,11 @@ class Style { if (txtrCtx.strokeStyle !== this.stroke.color) { txtrCtx.strokeStyle = this.stroke.color; } - const width = (this.stroke.width || 2.0) * invCtxScale; + const width = this.stroke.width * invCtxScale; if (txtrCtx.lineWidth !== width) { txtrCtx.lineWidth = width; } - const alpha = this.stroke.opacity == undefined ? 1.0 : this.stroke.opacity; + const alpha = this.stroke.opacity; if (alpha !== txtrCtx.globalAlpha && typeof alpha == 'number') { txtrCtx.globalAlpha = alpha; } @@ -957,7 +991,7 @@ class Style { // need doc for the txtrCtx.fillStyle.src that seems to always be undefined if (this.fill.pattern) { let img = this.fill.pattern; - const cropValues = this.fill.pattern.cropValues; + const cropValues = { ...this.fill.pattern.cropValues }; if (this.fill.pattern.source) { img = await loadImage(this.fill.pattern.source); } @@ -1032,7 +1066,7 @@ class Style { if (!this.icon.cropValues && !this.icon.color) { icon.src = this.icon.source; } else { - const cropValues = this.icon.cropValues; + const cropValues = { ...this.icon.cropValues }; const color = this.icon.color; const id = this.icon.id || this.icon.source; const img = await loadImage(this.icon.source); diff --git a/src/Layer/LabelLayer.js b/src/Layer/LabelLayer.js index 1dcd649b00..1b2607aa6c 100644 --- a/src/Layer/LabelLayer.js +++ b/src/Layer/LabelLayer.js @@ -254,9 +254,18 @@ class LabelLayer extends GeometryLayer { context.setZoom(extentOrTile.zoom); data.features.forEach((f) => { - // TODO: add support for LINE and POLYGON + if (f.style.text) { + if (Object.keys(f.style.text).length === 0) { + return; + } + } + if (f.type !== FEATURE_TYPES.POINT) { - return; + const warn = `Type label ${Object.keys(FEATURE_TYPES).filter(ft => FEATURE_TYPES[ft] === f.type)} not well supported`; + if (!this.source.warn.has(warn)) { + this.source.warn.add(warn); + console.warn(warn); + } } context.setFeature(f); @@ -270,19 +279,11 @@ class LabelLayer extends GeometryLayer { labels.needsAltitude = labels.needsAltitude || this.forceClampToTerrain === true || (isDefaultElevationStyle && !f.hasRawElevationData); f.geometries.forEach((g) => { - // NOTE: this only works because only POINT is supported, it - // needs more work for LINE and POLYGON - coord.setFromArray(f.vertices, g.size * g.indices[0].offset); - // Transform coordinate to data.crs projection - coord.applyMatrix4(data.matrixWorld); - - if (!_extent.isPointInside(coord)) { return; } - const geometryField = g.properties.style && g.properties.style.text && g.properties.style.text.field; - context.setGeometry(g); - let content; this.style.setContext(context); const layerField = this.style.text && this.style.text.field; + const geometryField = g.properties.style && g.properties.style.text && g.properties.style.text.field; + let content; if (this.labelDomelement) { content = readExpression(this.labelDomelement, context); } else if (!geometryField && !featureField && !layerField) { @@ -294,12 +295,28 @@ class LabelLayer extends GeometryLayer { } } - const label = new Label(content, coord.clone(), this.style); + if (this.style.zoom.min > this.style.context.zoom || this.style.zoom.max <= this.style.context.zoom) { + return; + } + + // NOTE: this only works fine for POINT. + // It needs more work for LINE and POLYGON as we currently only use the first point of the entity - label.layerId = this.id; - label.padding = this.margin || label.padding; + g.indices.forEach((i) => { + coord.setFromArray(f.vertices, g.size * i.offset); + // Transform coordinate to data.crs projection + coord.applyMatrix4(data.matrixWorld); - labels.push(label); + if (!_extent.isPointInside(coord)) { return; } + + const label = new Label(content, coord.clone(), this.style); + + label.layerId = this.id; + label.order = f.order; + label.padding = this.margin || label.padding; + + labels.push(label); + }); }); }); diff --git a/src/Parser/VectorTileParser.js b/src/Parser/VectorTileParser.js index bff52dc251..ca6a9063f3 100644 --- a/src/Parser/VectorTileParser.js +++ b/src/Parser/VectorTileParser.js @@ -116,10 +116,11 @@ function vtFeatureToFeatureGeometry(vtFeature, feature, classify = false) { function readPBF(file, options) { options.out = options.out || {}; const vectorTile = new VectorTile(new Protobuf(file)); - const sourceLayers = Object.keys(vectorTile.layers); + const vtLayerNames = Object.keys(vectorTile.layers); - if (sourceLayers.length < 1) { - return; + const collection = new FeatureCollection(options.out); + if (vtLayerNames.length < 1) { + return Promise.resolve(collection); } // x,y,z tile coordinates @@ -130,41 +131,33 @@ function readPBF(file, options) { // Only if the layer.origin is top const y = options.in.isInverted ? options.extent.row : (1 << z) - options.extent.row - 1; - const collection = new FeatureCollection(options.out); - - const vFeature = vectorTile.layers[sourceLayers[0]]; - // TODO: verify if size is correct because is computed with only one feature (vFeature). - const size = vFeature.extent * 2 ** z; + const vFeature0 = vectorTile.layers[vtLayerNames[0]]; + // TODO: verify if size is correct because is computed with only one feature (vFeature0). + const size = vFeature0.extent * 2 ** z; const center = -0.5 * size; collection.scale.set(globalExtent.x / size, -globalExtent.y / size, 1); - collection.position.set(vFeature.extent * x + center, vFeature.extent * y + center, 0).multiply(collection.scale); + collection.position.set(vFeature0.extent * x + center, vFeature0.extent * y + center, 0).multiply(collection.scale); collection.updateMatrixWorld(); - sourceLayers.forEach((layer_id) => { - if (!options.in.layers[layer_id]) { return; } + vtLayerNames.forEach((vtLayerName) => { + if (!options.in.layers[vtLayerName]) { return Promise.resolve(collection); } - const sourceLayer = vectorTile.layers[layer_id]; + const vectorTileLayer = vectorTile.layers[vtLayerName]; - for (let i = sourceLayer.length - 1; i >= 0; i--) { - const vtFeature = sourceLayer.feature(i); + for (let i = vectorTileLayer.length - 1; i >= 0; i--) { + const vtFeature = vectorTileLayer.feature(i); vtFeature.tileNumbers = { x, y: options.extent.row, z }; - const layers = options.in.layers[layer_id].filter(l => l.filterExpression.filter({ zoom: z }, vtFeature) && z >= l.zoom.min && z < l.zoom.max); - let feature; + // Find layers where this vtFeature is used + const layers = options.in.layers[vtLayerName] + .filter(l => l.filterExpression.filter({ zoom: z }, vtFeature)); for (const layer of layers) { - if (!feature) { - feature = collection.requestFeatureById(layer.id, vtFeature.type - 1); - feature.id = layer.id; - feature.order = layer.order; - feature.style = options.in.styles[feature.id]; - vtFeatureToFeatureGeometry(vtFeature, feature); - } else if (!collection.features.find(f => f.id === layer.id)) { - feature = collection.newFeatureByReference(feature); - feature.id = layer.id; - feature.order = layer.order; - feature.style = options.in.styles[feature.id]; - } + const feature = collection.requestFeatureById(layer.id, vtFeature.type - 1); + feature.id = layer.id; + feature.order = layer.order; + feature.style = options.in.styles[feature.id]; + vtFeatureToFeatureGeometry(vtFeature, feature); } } }); diff --git a/src/Renderer/Label2DRenderer.js b/src/Renderer/Label2DRenderer.js index f9ac17ab60..6e927844df 100644 --- a/src/Renderer/Label2DRenderer.js +++ b/src/Renderer/Label2DRenderer.js @@ -186,13 +186,15 @@ class Label2DRenderer { if (!frustum.containsPoint(worldPosition.applyMatrix4(camera.matrixWorldInverse)) || // Check if globe horizon culls the label // Do some horizon culling (if possible) if the tiles level is small enough. - label.horizonCullingPoint && GlobeLayer.horizonCulling(label.horizonCullingPoint) || - // Check if content isn't present in visible labels - this.grid.visible.some((l) => { - // TODO for icon without text filter by position - const textContent = label.content.textContent; - return textContent !== '' && l.content.textContent.toLowerCase() == textContent.toLowerCase(); - })) { + label.horizonCullingPoint && GlobeLayer.horizonCulling(label.horizonCullingPoint) + // Why do we might need this part ? + // || // Check if content isn't present in visible labels + // this.grid.visible.some((l) => { + // // TODO for icon without text filter by position + // const textContent = label.content.textContent; + // return textContent !== '' && l.content.textContent.toLowerCase() == textContent.toLowerCase(); + // }) + ) { label.visible = false; } else { // projecting world position label diff --git a/src/Source/Source.js b/src/Source/Source.js index 11c54b5f20..c3590fa15d 100644 --- a/src/Source/Source.js +++ b/src/Source/Source.js @@ -107,6 +107,7 @@ class Source extends InformationsData { constructor(source) { super(source); this.isSource = true; + this.warn = new Set(); if (!source.url) { throw new Error('New Source: url is required'); diff --git a/src/Source/VectorTilesSource.js b/src/Source/VectorTilesSource.js index ed5b9562ed..31e8081f5f 100644 --- a/src/Source/VectorTilesSource.js +++ b/src/Source/VectorTilesSource.js @@ -20,6 +20,16 @@ function mergeCollections(collections) { return collection; } +// A deprecated (but still in use) Mapbox spec allows using 'ref' as a propertie to reference an other layer +// instead of duplicating the following properties: 'type', 'source', 'source-layer', 'minzoom', 'maxzoom', 'filter', 'layout' +function getPropertiesFromRefLayer(layers, layer) { + const refProperties = ['type', 'source', 'source-layer', 'minzoom', 'maxzoom', 'filter', 'layout']; + const refLayer = layers.filter(l => l.id === layer.ref)[0]; + refProperties.forEach((prop) => { + layer[prop] = refLayer[prop]; + }); +} + /** * VectorTilesSource are object containing informations on how to fetch vector * tiles resources. @@ -71,10 +81,11 @@ class VectorTilesSource extends TMSSource { this.accessToken = source.accessToken; + let mvtStyleUrl; if (source.style) { if (typeof source.style == 'string') { - const styleUrl = urlParser.normalizeStyleURL(source.style, this.accessToken); - promise = Fetcher.json(styleUrl, this.networkOptions); + mvtStyleUrl = urlParser.normalizeStyleURL(source.style, this.accessToken); + promise = Fetcher.json(mvtStyleUrl, this.networkOptions); } else { promise = Promise.resolve(source.style); } @@ -82,27 +93,31 @@ class VectorTilesSource extends TMSSource { throw new Error('New VectorTilesSource: style is required'); } - this.whenReady = promise.then((style) => { - this.jsonStyle = style; - const baseurl = source.sprite || style.sprite; + this.whenReady = promise.then((mvtStyle) => { + this.jsonStyle = mvtStyle; + let baseurl = source.sprite || mvtStyle.sprite; if (baseurl) { + baseurl = new URL(baseurl, mvtStyleUrl).toString(); const spriteUrl = urlParser.normalizeSpriteURL(baseurl, '', '.json', this.accessToken); return Fetcher.json(spriteUrl, this.networkOptions).then((sprites) => { this.sprites = sprites; const imgUrl = urlParser.normalizeSpriteURL(baseurl, '', '.png', this.accessToken); this.sprites.source = imgUrl; - return style; + return mvtStyle; }); } - return style; - }).then((style) => { - style.layers.forEach((layer, order) => { + return mvtStyle; + }).then((mvtStyle) => { + mvtStyle.layers.forEach((layer, order) => { layer.sourceUid = this.uid; if (layer.type === 'background') { this.backgroundLayer = layer; } else if (ffilter(layer)) { - const style = Style.setFromVectorTileLayer(layer, this.sprites, order, this.symbolToCircle); + if (layer['source-layer'] === undefined) { + getPropertiesFromRefLayer(mvtStyle.layers, layer); + } + const style = Style.setFromVectorTileLayer(layer, this.sprites, this.symbolToCircle, this.warn); this.styles[layer.id] = style; if (!this.layers[layer['source-layer']]) { @@ -112,20 +127,18 @@ class VectorTilesSource extends TMSSource { id: layer.id, order, filterExpression: featureFilter(layer.filter), - zoom: { - min: layer.minzoom || 0, - max: layer.maxzoom || 24, - }, }); } }); if (this.url == '.') { - const TMSUrlList = Object.values(style.sources).map((sourceVT) => { + const TMSUrlList = Object.values(mvtStyle.sources).map((sourceVT) => { if (sourceVT.url) { + sourceVT.url = new URL(sourceVT.url, mvtStyleUrl).toString(); const urlSource = urlParser.normalizeSourceURL(sourceVT.url, this.accessToken); return Fetcher.json(urlSource, this.networkOptions).then((tileJSON) => { if (tileJSON.tiles[0]) { + tileJSON.tiles[0] = decodeURIComponent(new URL(tileJSON.tiles[0], urlSource).toString()); return toTMSUrl(tileJSON.tiles[0]); } }); @@ -136,7 +149,7 @@ class VectorTilesSource extends TMSSource { }); return Promise.all(TMSUrlList); } - return (Promise.resolve([this.url])); + return (Promise.resolve([toTMSUrl(this.url)])); }).then((TMSUrlList) => { this.urls = Array.from(new Set(TMSUrlList)); }); diff --git a/test/data/vectortiles/style.json b/test/data/vectortiles/style.json index 1af58000a3..ffc6a8c707 100644 --- a/test/data/vectortiles/style.json +++ b/test/data/vectortiles/style.json @@ -19,7 +19,8 @@ "maxzoom": 13, "paint": { "fill-color": "rgb(255, 0, 0)" - } + }, + "source-layer": "source_layer" } ] } \ No newline at end of file diff --git a/test/unit/vectortiles.js b/test/unit/vectortiles.js index fa93cfdca4..3b907531ad 100644 --- a/test/unit/vectortiles.js +++ b/test/unit/vectortiles.js @@ -13,7 +13,7 @@ import sprite from '../data/vectortiles/sprite.json'; import mapboxStyle from '../data/mapboxMulti.json'; const resources = { - 'test/data/vectortiles/style.json': style, + 'https://test/data/vectortiles/style.json': style, 'https://test/tilejson.json': tilejson, 'https://test/sprite.json': sprite, 'https://api.mapbox.com/v4/mapbox.mapbox-terrain-v2,mapbox.mapbox-streets-v7.json': mapboxStyle, @@ -67,15 +67,17 @@ describe('Vector tiles', function () { }).catch(done); }); - it('returns nothing', (done) => { + it('returns an empty collection', (done) => { parse(null).then((collection) => { - assert.equal(collection, undefined); + assert.ok(collection.isFeatureCollection); + assert.equal(collection.features.length, 0); done(); }).catch(done); }); it('filters all features out', (done) => { parse(multipolygon, {}).then((collection) => { + assert.ok(collection.isFeatureCollection); assert.equal(collection.features.length, 0); done(); }).catch(done); @@ -174,6 +176,7 @@ describe('VectorTilesSource', function () { paint: { 'fill-color': 'rgb(255, 0, 0)', }, + 'source-layer': 'source_layer', }], }, }); @@ -187,7 +190,7 @@ describe('VectorTilesSource', function () { it('loads the style from a file', function _it(done) { const source = new VectorTilesSource({ - style: 'test/data/vectortiles/style.json', + style: 'https://test/data/vectortiles/style.json', }); source.whenReady .then(() => { @@ -211,6 +214,7 @@ describe('VectorTilesSource', function () { paint: { 'fill-color': 'rgb(255, 0, 0)', }, + 'source-layer': 'source_layer', }, { // minzoom is 5 (specified) id: 'second', @@ -219,6 +223,7 @@ describe('VectorTilesSource', function () { 'fill-color': 'rgb(255, 0, 0)', }, minzoom: 5, + 'source-layer': 'source_layer', }, { // minzoom is 4 (first stop) // If a style have `stops` expression, should it be used to determine the min zoom? @@ -228,6 +233,7 @@ describe('VectorTilesSource', function () { 'fill-color': 'rgb(255, 0, 0)', 'fill-opacity': { stops: [[4, 1], [7, 0.5]] }, }, + 'source-layer': 'source_layer', }, { // minzoom is 1 (first stop and no specified minzoom) id: 'fourth', @@ -236,6 +242,7 @@ describe('VectorTilesSource', function () { 'fill-color': 'rgb(255, 0, 0)', 'fill-opacity': { stops: [[1, 1], [7, 0.5]] }, }, + 'source-layer': 'source_layer', }, { // minzoom is 4 (first stop is higher than specified) id: 'fifth', @@ -245,6 +252,7 @@ describe('VectorTilesSource', function () { 'fill-opacity': { stops: [[4, 1], [7, 0.5]] }, }, minzoom: 3, + 'source-layer': 'source_layer', }], }, });