Skip to content

Commit

Permalink
refactor(StyleOptions): move StyleOptions from Style to its own file
Browse files Browse the repository at this point in the history
  • Loading branch information
ftoromanoff committed Oct 15, 2024
1 parent cfb9d0f commit 99afbfa
Show file tree
Hide file tree
Showing 6 changed files with 421 additions and 266 deletions.
4 changes: 2 additions & 2 deletions src/Core/Feature.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as THREE from 'three';
import Extent from 'Core/Geographic/Extent';
import Coordinates from 'Core/Geographic/Coordinates';
import CRS from 'Core/Geographic/Crs';
import Style from 'Core/Style';
import StyleOptions from 'Core/StyleOptions';

function defaultExtent(crs) {
return new Extent(crs, Infinity, -Infinity, Infinity, -Infinity);
Expand Down Expand Up @@ -251,7 +251,7 @@ class Feature {
}
this._pos = 0;
this._pushValues = (this.size === 3 ? push3DValues : push2DValues).bind(this);
this.style = Style.setFromProperties;
this.style = StyleOptions.setFromProperties;
}
/**
* Instance a new {@link FeatureGeometry} and push in {@link Feature}.
Expand Down
246 changes: 0 additions & 246 deletions src/Core/Style.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { FEATURE_TYPES } from 'Core/Feature';
import Cache from 'Core/Scheduler/Cache';
import Fetcher from 'Provider/Fetcher';
import * as mapbox from '@mapbox/mapbox-gl-style-spec';
import { Color } from 'three';
import { deltaE } from 'Renderer/Color';
import Coordinates from 'Core/Geographic/Coordinates';
Expand All @@ -13,8 +11,6 @@ const cacheStyle = new Cache();
const matrix = document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGMatrix();
const canvas = document.createElement('canvas');

const inv255 = 1 / 255;

function baseAltitudeDefault(properties, ctx) {
return ctx?.coordinates?.z || 0;
}
Expand Down Expand Up @@ -47,37 +43,6 @@ export function readExpression(property, ctx) {
return property;
}

function rgba2rgb(orig) {
if (!orig) {
return {};
} else if (orig.stops || orig.expression) {
return { color: orig };
} else if (typeof orig == 'string') {
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);
if (result === null) {
return { color: orig, opacity: 1.0 };
} else if (result[7]) {
let opacity = 1.0;
if (result[9]) {
opacity = parseInt(result[9].length == 1 ? `${result[9]}${result[9]}` : result[9], 16) * inv255;
}
return { color: `#${result[8]}`, opacity };
} else if (result[1]) {
return { color: `${result[2]}(${result[3]},${result[4]},${result[5]})`, opacity: (result[6] ? Number(result[6]) : 1.0) };
}
}
}

function readVectorProperty(property, options) {
if (property != undefined) {
if (mapbox.expression.isExpression(property)) {
return mapbox.expression.createExpression(property, options).value;
} else {
return property;
}
}
}

async function loadImage(source) {
let promise = cacheStyle.get(source, 'null');
if (!promise) {
Expand Down Expand Up @@ -700,217 +665,6 @@ class Style {
this.context = ctx;
}

/**
* set Style from (geojson-like) properties.
* @param {Object} properties (geojson-like) properties.
* @param {FeatureContext} featCtx the context of the feature
*
* @returns {StyleOptions} containing all properties for itowns.Style
*/
static setFromProperties(properties, featCtx) {
const type = featCtx.type;
const style = {};
if (type === FEATURE_TYPES.POINT) {
const point = {
...(properties.fill !== undefined && { color: properties.fill }),
...(properties['fill-opacity'] !== undefined && { opacity: properties['fill-opacity'] }),
...(properties.stroke !== undefined && { line: properties.stroke }),
...(properties.radius !== undefined && { radius: properties.radius }),
};
if (Object.keys(point).length) {
style.point = point;
}
const text = {
...(properties['label-color'] !== undefined && { color: properties['label-color'] }),
...(properties['label-opacity'] !== undefined && { opacity: properties['label-opacity'] }),
...(properties['label-size'] !== undefined && { size: properties['label-size'] }),
};
if (Object.keys(point).length) {
style.text = text;
}
const icon = {
...(properties.icon !== undefined && { source: properties.icon }),
...(properties['icon-scale'] !== undefined && { size: properties['icon-scale'] }),
...(properties['icon-opacity'] !== undefined && { opacity: properties['icon-opacity'] }),
...(properties['icon-color'] !== undefined && { color: properties['icon-color'] }),
};
if (Object.keys(icon).length) {
style.icon = icon;
}
} else {
const stroke = {
...(properties.stroke !== undefined && { color: properties.stroke }),
...(properties['stroke-width'] !== undefined && { width: properties['stroke-width'] }),
...(properties['stroke-opacity'] !== undefined && { opacity: properties['stroke-opacity'] }),
};
if (Object.keys(stroke).length) {
style.stroke = stroke;
}
if (type !== FEATURE_TYPES.LINE) {
const fill = {
...(properties.fill !== undefined && { color: properties.fill }),
...(properties['fill-opacity'] !== undefined && { opacity: properties['fill-opacity'] }),
};
if (Object.keys(fill).length) {
style.fill = fill;
}
}
}
return 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]
*
* @returns {StyleOptions} containing all properties for itowns.Style
*/
static setFromVectorTileLayer(layer, sprites, order = 0, symbolToCircle = false) {
const style = {
fill: {},
stroke: {},
point: {},
text: {},
icon: {},
};

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;
style.fill.opacity = readVectorProperty(layer.paint['fill-opacity']) || opacity;
if (layer.paint['fill-pattern']) {
try {
style.fill.pattern = {
id: layer.paint['fill-pattern'],
source: sprites.source,
cropValues: sprites[layer.paint['fill-pattern']],
};
} catch (err) {
err.message = `VTlayer '${layer.id}': argument sprites must not be null when using layer.paint['fill-pattern']`;
throw err;
}
}

if (layer.paint['fill-outline-color']) {
const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['fill-outline-color'], { type: 'color' }));
style.stroke.color = color;
style.stroke.opacity = opacity;
style.stroke.width = 1.0;
style.stroke.dasharray = [];
}
} else if (layer.type === 'line') {
const prepare = readVectorProperty(layer.paint['line-color'], { type: 'color' });
const { color, opacity } = rgba2rgb(prepare);
style.stroke.dasharray = readVectorProperty(layer.paint['line-dasharray']);
style.stroke.color = color;
style.stroke.lineCap = layer.layout['line-cap'];
style.stroke.width = readVectorProperty(layer.paint['line-width']);
style.stroke.opacity = readVectorProperty(layer.paint['line-opacity']) || opacity;
} else if (layer.type === 'circle' || symbolToCircle) {
const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['circle-color'], { type: 'color' }));
style.point.color = color;
style.point.opacity = opacity;
style.point.radius = readVectorProperty(layer.paint['circle-radius']);
} else if (layer.type === 'symbol') {
// overlapping order
style.text.zOrder = readVectorProperty(layer.layout['symbol-z-order']);
if (style.text.zOrder == 'auto') {
style.text.zOrder = readVectorProperty(layer.layout['symbol-sort-key']) || 'Y';
} else if (style.text.zOrder == 'viewport-y') {
style.text.zOrder = 'Y';
} else if (style.text.zOrder == 'source') {
style.text.zOrder = 0;
}

// position
style.text.anchor = readVectorProperty(layer.layout['text-anchor']);
style.text.offset = readVectorProperty(layer.layout['text-offset']);
style.text.padding = readVectorProperty(layer.layout['text-padding']);
style.text.size = readVectorProperty(layer.layout['text-size']);
style.text.placement = readVectorProperty(layer.layout['symbol-placement']);
style.text.rotation = readVectorProperty(layer.layout['text-rotation-alignment']);

// content
style.text.field = readVectorProperty(layer.layout['text-field']);
style.text.wrap = readVectorProperty(layer.layout['text-max-width']);
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']);

// appearance
const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['text-color'], { type: 'color' }));
style.text.color = color;
style.text.opacity = readVectorProperty(layer.paint['text-opacity']) || (opacity !== undefined && opacity);

style.text.font = readVectorProperty(layer.layout['text-font']);
const haloColor = readVectorProperty(layer.paint['text-halo-color'], { type: 'color' });
if (haloColor) {
style.text.haloColor = haloColor.color || haloColor;
style.text.haloWidth = readVectorProperty(layer.paint['text-halo-width']);
style.text.haloBlur = readVectorProperty(layer.paint['text-halo-blur']);
}

// additional icon
const iconImg = readVectorProperty(layer.layout['icon-image']);
if (iconImg) {
try {
style.icon.id = iconImg;
if (iconImg.stops) {
const iconCropValue = {
...(iconImg.base !== undefined && { base: iconImg.base }),
stops: iconImg.stops.map((stop) => {
let cropValues = sprites[stop[1]];
if (stop[1].includes('{')) {
cropValues = function _(p) {
const id = stop[1].replace(/\{(.+?)\}/g, (a, b) => (p[b] || '')).trim();
cropValues = sprites[id];
return sprites[id];
};
}
return [stop[0], cropValues];
}),
};
style.icon.cropValues = iconCropValue;
} else {
style.icon.cropValues = sprites[iconImg];
if (iconImg[0].includes('{')) {
style.icon.cropValues = function _(p) {
const id = iconImg.replace(/\{(.+?)\}/g, (a, b) => (p[b] || '')).trim();
style.icon.cropValues = sprites[id];
return sprites[id];
};
}
}
style.icon.source = sprites.source;
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);
} catch (err) {
err.message = `VTlayer '${layer.id}': argument sprites must not be null when using layer.layout['icon-image']`;
throw err;
}
}
}
// VectorTileSet: by default minZoom = 0 and maxZoom = 24
// https://docs.mapbox.com/style-spec/reference/layers/#maxzoom and #minzoom
// Should be move to layer properties, when (if) one mapBox layer will be considered as several itowns layers.
// issue https://github.com/iTowns/itowns/issues/2153 (last point)
style.zoom = {
min: layer.minzoom || 0,
max: layer.maxzoom || 24,
};
return style;
}

/**
* Applies the style.fill to a polygon of the texture canvas.
* @param {CanvasRenderingContext2D} txtrCtx The Context 2D of the texture canvas.
Expand Down
Loading

0 comments on commit 99afbfa

Please sign in to comment.