Skip to content

Commit

Permalink
feature(vector tiles): best support for vector tiles.
Browse files Browse the repository at this point in the history
supports:
 * layers ordering
 * pattern
 * fill stroke
 * line cap and join
 * dash line
 * multi styles
 * styles by zoom level
  • Loading branch information
gchoqueux committed Nov 5, 2019
1 parent 4b1adf2 commit a882b50
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 42 deletions.
2 changes: 1 addition & 1 deletion examples/js/plugins/FeatureToolTip.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ var FeatureToolTip = (function _() {
feature = features[p];

geometry = feature.geometry;
style = (layer.style && layer.style.isStyle) ? layer.style : geometry.properties.style;
style = (layer.style && layer.style.isStyle) ? layer.style : (geometry._feature.style || geometry.properties.style);
fill = style.fill.color;
stroke = '1.25px ' + style.stroke.color;

Expand Down
19 changes: 11 additions & 8 deletions src/Converter/Feature2Texture.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ function fillStyle(style, ctx, invCtxScale) {
} else {
console.warn('Raster pattern isn\'t completely supported on Ie and edge');
}
ctx.fillStyle.src = style.fill.pattern.src;
} else if (style.fill.color && ctx.fillStyle !== style.fill.color) {
ctx.fillStyle = style.fill.color;
}
Expand All @@ -69,14 +68,18 @@ function strokeStyle(style, ctx, invCtxScale) {
if (ctx.strokeStyle !== style.stroke.color) {
ctx.strokeStyle = style.stroke.color;
}
const width = Math.round((style.stroke.width || 2.0)) * invCtxScale;
const width = (style.stroke.width || 2.0) * invCtxScale;
if (ctx.lineWidth !== width) {
ctx.lineWidth = width;
}
const alpha = style.stroke.opacity == undefined ? 1.0 : style.stroke.opacity;
if (alpha !== ctx.globalAlpha && typeof alpha == 'number') {
ctx.globalAlpha = alpha;
}
if (ctx.lineCap !== style.stroke.lineCap) {
ctx.lineCap = style.stroke.lineCap;
}
ctx.setLineDash(style.stroke.dasharray.map(a => a * invCtxScale * 2));
}

function drawPoint(ctx, x, y, style = {}, invCtxScale) {
Expand Down Expand Up @@ -106,24 +109,23 @@ function drawFeature(ctx, feature, extent, style, invCtxScale) {

for (const geometry of feature.geometry) {
if (geometry.extent.intersectsExtent(extent)) {
const properties = geometry.properties;
const geometryStyle = style.isStyle ? style : properties.style;
const geoStyle = style.isStyle ? style : geometry.properties.style;
if (feature.type === FEATURE_TYPES.POINT) {
// cross multiplication to know in the extent system the real size of
// the point
const px = (Math.round(geometryStyle.point.radius * invCtxScale) || 3 * invCtxScale) * scaleRadius;
const px = (Math.round(geoStyle.point.radius * invCtxScale) || 3 * invCtxScale) * scaleRadius;
for (const indice of geometry.indices) {
const offset = indice.offset * feature.size;
const count = offset + indice.count * feature.size;
for (let j = offset; j < count; j += feature.size) {
coord.setFromArray(feature.vertices, j);
if (extent.isPointInside(coord, px)) {
drawPoint(ctx, feature.vertices[j], feature.vertices[j + 1], geometryStyle, invCtxScale);
drawPoint(ctx, feature.vertices[j], feature.vertices[j + 1], geoStyle, invCtxScale);
}
}
}
} else {
drawPolygon(ctx, feature.vertices, geometry.indices, geometryStyle, feature.size, extent, invCtxScale, (feature.type == FEATURE_TYPES.POLYGON));
drawPolygon(ctx, feature.vertices, geometry.indices, geoStyle, feature.size, extent, invCtxScale, (feature.type == FEATURE_TYPES.POLYGON));
}
}
}
Expand Down Expand Up @@ -157,6 +159,7 @@ export default {
}
ctx.globalCompositeOperation = style.globalCompositeOperation || 'source-over';
ctx.imageSmoothingEnabled = false;
ctx.lineJoin = 'round';

const ex = collection.crs == extent.crs ? extent : extent.as(collection.crs, _extent);
const t = collection.translation;
Expand All @@ -172,7 +175,7 @@ export default {

// Draw the canvas
for (const feature of collection.features) {
drawFeature(ctx, feature, extentTransformed, style, invCtxScale);
drawFeature(ctx, feature, extentTransformed, feature.style || style, invCtxScale);
}

texture = new THREE.CanvasTexture(c);
Expand Down
52 changes: 43 additions & 9 deletions src/Core/Feature.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,15 +274,8 @@ export class FeatureCollection {
this.updateExtent(feature.extent);
}

/**
* Returns the Feature by type if `mergeFeatures` is `true` or returns the
* new instance of typed Feature.
*
* @param {string} type the type requested
* @returns {Feature}
*/
getFeatureByType(type) {
const feature = this.features.find(feature => feature.type === type);
requestFeature(type, callback) {
const feature = this.features.find(callback);
if (feature && this.optionsFeature.mergeFeatures) {
return feature;
} else {
Expand All @@ -291,4 +284,45 @@ export class FeatureCollection {
return newFeature;
}
}

/**
* Returns the Feature by type if `mergeFeatures` is `true` or returns the
* new instance of typed Feature.
*
* @param {string} type the type requested
* @returns {Feature}
*/
requestFeatureByType(type) {
return this.requestFeature(type, feature => feature.type === type);
}

/**
* Returns the Feature by type if `mergeFeatures` is `true` or returns the
* new instance of typed Feature.
*
* @param {string} id the id requested
* @param {string} type the type requested
* @returns {Feature}
*/
requestFeatureById(id, type) {
return this.requestFeature(type, feature => feature.id === id);
}
/**
* Add a new feature with references to all properties.
* It allows to have features with different styles
* without having to duplicate the geometry.
* @param {Feature} feature The feature to reference.
* @return {Feature} The new referenced feature
*/
newFeatureByReference(feature) {
const ref = new Feature(feature.type, this.crs, this.optionsFeature);
ref.extent = feature.extent;
ref.geometry = feature.geometry;
ref.normals = feature.normals;
ref.size = feature.size;
ref.vertices = feature.vertices;
ref._pos = feature._pos;
this.features.push(ref);
return ref;
}
}
56 changes: 45 additions & 11 deletions src/Core/Style.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FEATURE_TYPES } from 'Core/Feature';

const inv255 = 1 / 255;
const canvas = document.createElement('canvas');

function rgba2rgb(orig) {
const result = orig.match(/(?:((hsl|rgb)a? *\(([\d.%]+(?:deg|g?rad|turn)?)[ ,]*([\d.%]+)[ ,]*([\d.%]+)[ ,/]*([\d.%]*)\))|(#((?:[\d\w]{3}){1,2})([\d\w]{1,2})?))/i);
Expand All @@ -17,6 +18,18 @@ function rgba2rgb(orig) {
}
}


function readVectorProperty(property, zoom) {
if (property == undefined) {
//
} else if (property.stops) {
const p = property.stops.slice().reverse().find(stop => zoom >= stop[0]);
return p ? p[1] : property.stops[0][1];
} else {
return property.base || property;
}
}

/**
* Style defines {@link Feature} style.
* @property {object} fill fill style.
Expand Down Expand Up @@ -55,6 +68,7 @@ class Style {
color: params.stroke.color,
opacity: params.stroke.opacity,
width: params.stroke.width,
dasharray: params.stroke.dasharray || [],
};
this.point = {
color: params.point.color,
Expand Down Expand Up @@ -88,35 +102,55 @@ class Style {
}
return this;
}

/**
* set Style from vector tile layer properties.
* @param {object} layer vector tile layer.
* @param {Number} zoom vector tile layer.
* @param {Object} sprites vector tile layer.
* @returns {Style}
*/
setFromVectorTileLayer(layer) {
setFromVectorTileLayer(layer, zoom, sprites) {
if (layer.type === 'fill' && !this.fill.color) {
const { color, opacity } = rgba2rgb(layer.paint['fill-color']);
const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['fill-color'] || layer.paint['fill-pattern']));
this.fill.color = color;
this.fill.opacity = layer.paint['fill-opacity'] || opacity || 1.0;
this.fill.opacity = readVectorProperty(layer.paint['fill-opacity'], zoom) || opacity || 1.0;
if (layer.paint['fill-pattern'] && sprites) {
const sprite = sprites[layer.paint['fill-pattern']];
canvas.width = sprite.width;
canvas.height = sprite.height;
canvas.getContext('2d').drawImage(sprites.img, sprite.x, sprite.y, sprite.width, sprite.height, 0, 0, sprite.width, sprite.height);
this.fill.pattern = document.createElement('img');
this.fill.pattern.src = canvas.toDataURL('image/png');
}

if (layer.paint['fill-outline-color']) {
const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['fill-outline-color']));
this.stroke.color = color;
this.stroke.opacity = opacity;
this.stroke.width = 1.0;
this.stroke.dasharray = [];
}
}
if (layer.type === 'line' && !this.stroke.color) {
const { color, opacity } = rgba2rgb(layer.paint['line-color']);
const prepare = readVectorProperty(layer.paint['line-color'], zoom);
const { color, opacity } = rgba2rgb(prepare);
this.stroke.dasharray = readVectorProperty(layer.paint['line-dasharray'], zoom) || [];
this.stroke.color = color;
if ('line-width' in layer.paint) {
this.stroke.width = layer.paint['line-width'].base || 3.0;
}
this.stroke.opacity = layer.paint['line-opacity'] || opacity || 1.0;
this.stroke.lineCap = layer.layout && layer.layout['line-cap'];
this.stroke.width = readVectorProperty(layer.paint['line-width'], zoom) || 3.0;
this.stroke.opacity = readVectorProperty(layer.paint['line-opacity'], zoom) || opacity || 1.0;
}
if (layer.type === 'symbol') {
const { color, opacity } = rgba2rgb(layer.paint['text-color']);
const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['text-color'] || '#000000', zoom));
this.point.color = color;
this.point.opacity = opacity;
this.point.radius = 1.5;
} else if (layer.type === 'circle') {
const { color, opacity } = rgba2rgb(layer.paint['circle-color']);
const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['circle-color']), zoom);
this.point.color = color;
this.point.opacity = opacity;
this.point.radius = layer.paint['circle-radius'];
this.point.radius = readVectorProperty(layer.paint['circle-radius'], zoom);
}
return this;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Parser/GeoJsonParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ function jsonFeatureToFeature(crsIn, crsOut, json, filteringExtent, options, fea

const jsonType = json.geometry.type.toLowerCase();
const featureType = toFeatureType(jsonType);
const feature = options.mergeFeatures ? featureCollection.getFeatureByType(featureType) : new Feature(featureType, crsOut, options);
const feature = options.mergeFeatures ? featureCollection.requestFeatureByType(featureType) : new Feature(featureType, crsOut, options);
const geometryCount = feature.geometryCount;
const coordinates = jsonType != 'point' ? json.geometry.coordinates : [json.geometry.coordinates];
const setAltitude = !options.overrideAltitudeInToZero && options.withAltitude;
Expand Down
40 changes: 28 additions & 12 deletions src/Parser/VectorTileParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ function readPBF(file, options) {
features.filter = options.filter || defaultFilter;

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 center = -0.5 * size;
features.scale.set(size, -size, 1).divide(globalExtent);
Expand All @@ -137,26 +138,41 @@ function readPBF(file, options) {

sourceLayers.forEach((layer_id) => {
const sourceLayer = vectorTile.layers[layer_id];
const layersStyle = allLayers.filter(l => sourceLayer.name == l['source-layer']);
for (let i = 0; i < sourceLayer.length; i++) {
const layersSource = allLayers.filter(l => sourceLayer.name == l['source-layer']);
for (let i = sourceLayer.length - 1; i >= 0; i--) {
const vtFeature = sourceLayer.feature(i);
const layerStyle = layersStyle.filter(l => l.filterExpression({ zoom: extentSource.zoom }, vtFeature))[0];
if (layerStyle) {
const properties = vtFeature.properties;
properties.style = styleCache.get(layerStyle.id);
if (!properties.style) {
properties.style = new Style();
properties.style.setFromVectorTileLayer(layerStyle);
styleCache.set(layerStyle.id, properties.style);
const layers = layersSource.filter(l => l.filterExpression({ zoom: z }, vtFeature) && (!l.minzoom || l.minzoom <= z) && (!l.maxzoom || l.maxzoom >= z));
let feature;
for (const layer of layers) {
const tag = `${layer.id}_zoom_${z}`;
// Fix doens't pass instance of properties
let style = styleCache.get(tag);
if (!style) {
style = new Style();
style.setFromVectorTileLayer(layer, extentSource.zoom, options.sprites);
styleCache.set(tag, style);
}
const order = allLayers.findIndex(l => l.id == layer.id);
if (!feature) {
feature = features.requestFeatureById(layer.id, vtFeature.type - 1);
feature.id = layer.id;
feature.order = order;
feature.style = style;
vtFeatureToFeatureGeometry(vtFeature, feature);
} else if (!features.features.find(f => f.id === layer.id)) {
feature = features.newFeatureByReference(feature);
feature.id = layer.id;
feature.order = order;
feature.style = style;
}
const feature = features.getFeatureByType(vtFeature.type - 1);
vtFeatureToFeatureGeometry(vtFeature, feature);
}
}
});

features.removeEmptyFeature();
// TODO verify if is needed to updateExtent for previous features.
features.updateExtent();
features.features.sort((a, b) => (a.order - b.order));
features.extent = extentSource;
return Promise.resolve(features);
}
Expand Down
1 change: 1 addition & 0 deletions src/Provider/DataSourceProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ function parseSourceData(data, extDest, layer) {
buildExtent: source.isFileSource || !layer.isGeometryLayer,
crsIn: source.projection,
crsOut: layer.projection,
sprites: layer.sprites,
// TODO FIXME: error in filtering vector tile
// filteringExtent: extentDestination.as(layer.projection),
filteringExtent: !source.isFileSource && layer.isGeometryLayer ? extDest.as(source.projection) : undefined,
Expand Down

0 comments on commit a882b50

Please sign in to comment.