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

MapBox VectorTile: bug fixes using an official MapBox stream #2469

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
06804f1
feat(VectorTile): add support for relative url in style
ftoromanoff Oct 29, 2024
5c62908
fix(VectorTile): fix {z}/{y}/{x}
ftoromanoff Oct 29, 2024
f903a1a
fix(MVTStyle): icon properties -> fix return of function when id incl…
ftoromanoff Nov 25, 2024
328cc2c
feat(Source): add propertie 'warn' for avoiding multiple console.warn
ftoromanoff Oct 29, 2024
a96f21b
test(VectorTileSource): fix test
ftoromanoff Oct 29, 2024
d756acb
refactor(VectorTileParser): cleanup
ftoromanoff Nov 4, 2024
a881fac
fix(MVTLayers): add MVTLayer where MVTStyle.layer has 'ref' properties
ftoromanoff Nov 22, 2024
b77965b
fix(Style): take style.zoom into account for LabelLayer and Feature2T…
ftoromanoff Nov 6, 2024
7f27ef8
fix(examples): fix linked with zoom properties well used
ftoromanoff Nov 21, 2024
c7a1c32
fix(MVTParser): supp use of layer.style.zoom in parser
ftoromanoff Nov 25, 2024
6d568fa
fix(Style): Don't draw Polygon when fill.color is undefined
ftoromanoff Nov 22, 2024
5f63ee2
fix(Style): Don't draw stroke when width is 0
ftoromanoff Nov 26, 2024
af2e0f1
fix(LabelLayer): gestion simplified of line and polygon Label
ftoromanoff Nov 25, 2024
049dd6a
fix(MVTStyle): Doing recoloring only with sdf icons.
ftoromanoff Nov 21, 2024
efa1777
fix(Label): Multiple labels with same textContent
ftoromanoff Nov 26, 2024
36bcd33
refactor(MVTParser): 1 feature per vtfeature
ftoromanoff Nov 26, 2024
151e9af
fix(VectorTile): supp order in Style as it's only a Label properties …
ftoromanoff Nov 14, 2024
bb8f57d
example(MVT): add example with official MapBox style file
ftoromanoff Nov 26, 2024
18b1d2c
fix(Style): dont draw icon when size is 0
ftoromanoff Nov 27, 2024
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
1 change: 0 additions & 1 deletion examples/source_file_kml_raster_usgs.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@
}

var kmlStyle = {
zoom: { min: 10, max: 20 },
text: {
field: '{name}',
haloColor: 'black',
Expand Down
98 changes: 98 additions & 0 deletions examples/vector_tile_raster_2d_mapbox.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<html>
<head>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add it to the examples list please?

<title>Itowns - Globe</title>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you update the title please?


<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<link
rel="stylesheet"
type="text/css"
href="css/example.css"
/>
<link
rel="stylesheet"
type="text/css"
href="css/LoadingScreen.css"
/>
<link
rel="stylesheet"
type="text/css"
href="https://www.itowns-project.org/itowns/examples/css/widgets.css"
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need widgets in this example


<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.6/dat.gui.min.js"></script>
</head>
<body>
<div id="viewerDiv"></div>

<!-- Import iTowns source code -->
<script src="../dist/itowns.js"></script>
<script src="../dist/debug.js"></script>
<!-- Import iTowns Widgets plugin -->
<script src="../dist/itowns_widgets.js"></script>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need widgets in this example

<!-- Import iTowns LoadingScreen and GuiTools plugins -->
<script src="js/GUI/GuiTools.js"></script>
<script src="js/GUI/LoadingScreen.js"></script>

<script>
const typeView = 'Planar';
// const typeView = 'Globe';

// `viewerDiv` will contain iTowns' rendering area (`<canvas>`)
var viewerDiv = document.getElementById('viewerDiv');
let view;

if (typeView === 'Globe') {
const coord = new itowns.Coordinates('EPSG:4326', 2.351323, 48.856712); // Paris
// const coord = new itowns.Coordinates("EPSG:4326", 60.599525, 56.834341); // Yekaterinburg
const placement = {
coord,
range: 950000,
};
view = new itowns.GlobeView(viewerDiv, placement);
} else if (typeView === 'Planar') {
// Define geographic extent: CRS, min/max X, min/max Y
var extent = new itowns.Extent(
'EPSG:3857',
-20037508.342789244, 20037508.342789244,
-20037508.342789255, 20037508.342789244);

// Instanciate PlanarView
view = new itowns.PlanarView(viewerDiv, extent, {
maxSubdivisionLevel: 20,
controls: {
// We do not want the user to zoom out too much
maxAltitude: 40000000,
// We want to keep the rotation disabled, to only have a view from the top
enableRotation: false,
// Don't zoom too much on smart zoom
smartTravelHeightMax: 100000,
},
});
}

const debugMenu = new GuiTools("menuDiv", view);
setupLoadingScreen(viewerDiv, view);

const source = new itowns.VectorTilesSource({
style: "mapbox://styles/mapbox/streets-v8",
accessToken:
"pk.eyJ1IjoiZ2VvdXJmbyIsImEiOiJjbHdyZWVrd2owOW1rMmlxdmx2Z3Fwb2JhIn0.sg5en26yZ6aSEg6CsfUByA",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is that your personal access token?

});

const layer = new itowns.ColorLayer("vector-map", {
source,
noTextureParentOutsideLimit: true,
addLabelLayer: true,
});

view.addLayer(layer).then(() => {
debugMenu.addLayerGUI.bind(debugMenu);
itowns.ColorLayersOrdering.moveLayerToIndex(view, "vector-map", 15);
});

debug.createTileDebugUI(debugMenu.gui, view);
</script>
</body>
</html>
3 changes: 3 additions & 0 deletions src/Converter/Feature2Texture.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
108 changes: 71 additions & 37 deletions src/Core/Style.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
},
Expand Down Expand Up @@ -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.<br/>
* 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
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you provide more details about why you remove it please?

Expand Down Expand Up @@ -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 || {};
Expand Down Expand Up @@ -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: {},
Expand All @@ -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;
Expand All @@ -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' });
Expand All @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for this comment I think, the code is clear enough

// overlapping order
style.text.zOrder = readVectorProperty(layer.layout['symbol-z-order']);
if (style.text.zOrder == 'auto') {
Expand All @@ -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']);
Expand All @@ -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) {
Expand All @@ -871,29 +875,60 @@ 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];
}),
};
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;
Expand All @@ -919,29 +954,28 @@ 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we check for stroke properties too, like for fill ?

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we remove canBeFilled ? Its value is feature.type == FEATURE_TYPES.POLYGON . We are in a method named applyToCanvasPolygon so aren't already expecting to handle only polygons ?

this._applyFillToPolygon(txtrCtx, invCtxScale, polygon, context);
this._applyFillToPolygon(txtrCtx, invCtxScale, polygon);
}
}

_applyStrokeToPolygon(txtrCtx, invCtxScale, polygon) {
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;
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down
Loading