Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion examples/source_file_geojson_raster_2.html
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
view.addLayer(earthquakeLayer).then(FeatureToolTip.addLayer);

// Request redraw
view.notifyChange(true);
view.notifyChange();

debug.createTileDebugUI(menuGlobe.gui, view);
window.view = view;
Expand Down
182 changes: 148 additions & 34 deletions packages/Main/src/Converter/Feature2Texture.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,88 @@
import * as THREE from 'three';
import { FEATURE_TYPES } from 'Core/Feature';
import { Extent, Coordinates } from '@itowns/geographic';
import Style, { StyleContext } from 'Core/Style';
import Style, { StyleContext, loadImage, cropImage } from 'Core/Style';
import { createContext2D, sharedContext2D } from 'Utils/CanvasUtils';

let _matrix;
function matrix() {
if (!_matrix) {
_matrix = document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGMatrix();
}
return _matrix;
}

export function applyStrokeToPolygon(
/** @type {CanvasRenderingContext2D} */context,
/** @type {Path2D} */polygon,
stroke,
/** @type {number} */scale,
) {
if (context.strokeStyle !== stroke.color) {
context.strokeStyle = stroke.color;
}
const width = stroke.width * scale;
if (context.lineWidth !== width) {
context.lineWidth = width;
}
const alpha = stroke.opacity;
if (alpha !== context.globalAlpha && typeof alpha == 'number') {
context.globalAlpha = alpha;
}
if (context.lineCap !== stroke.lineCap) {
context.lineCap = stroke.lineCap;
}
context.setLineDash(stroke.dasharray.map(a => a * scale * 2));
context.stroke(polygon);
}

async function createPattern(
/** @type {CanvasRenderingContext2D} */context,
pattern,
) {
if (typeof pattern == 'object' && 'source' in pattern) { // ImageRegion
const { source, cropValues = {} } = pattern;
const img = await loadImage(source);
const {
x = 0,
y = 0,
width = 'naturalWidth' in img ? img.naturalWidth : img.width,
height = 'naturalHeight' in img ? img.naturalHeight : img.height,
} = cropValues;

const cropCtx = sharedContext2D();
cropCtx.canvas.width = width;
cropCtx.canvas.height = height;
cropImage(cropCtx, img, x, y, width, height);
return context.createPattern(cropCtx.canvas, 'repeat');
} else { // string | HTMLImageElement | HTMLCanvasElement
const img = await loadImage(pattern);
context.drawImage(img, 0, 0);
}
return context.createPattern(context.canvas, 'repeat');
}

export async function applyFillToPolygon(
/** @type {CanvasRenderingContext2D} */context,
/** @type {Path2D} */polygon,
fill,
/** @type {number} */scale,
) {
if (fill.pattern) {
const fillStyle = await createPattern(context, fill.pattern); // by image already loaded
fillStyle.setTransform(matrix().scale(scale));
context.fillStyle = fillStyle;
} else if (context.fillStyle !== fill.color) {
context.fillStyle = fill.color;
}
if (fill.opacity !== context.globalAlpha) {
context.globalAlpha = fill.opacity;
}
context.fill(polygon);
}

const defaultStyle = new Style();
const context = new StyleContext();
let style;

/**
* Draw polygon (contour, line edge and fill) based on feature vertices into canvas
Expand All @@ -16,12 +93,22 @@
* @param {object[]} indices - Contains the indices that define the geometry.
* Objects stored in this array have two properties, an `offset` and a `count`.
* The offset is related to the overall number of vertices in the Feature.
* @param {object} style - The style to apply for this feature.

Check warning on line 96 in packages/Main/src/Converter/Feature2Texture.js

View workflow job for this annotation

GitHub Actions / Build bundle, check Linter and generate documentation

Expected @param names to be "ctx, vertices, indices, size, style, extent, invCtxScale, canBeFilled". Got "ctx, vertices, indices, style, size, extent, invCtxScale, canBeFilled"
* @param {number} size - The size of the feature.
* @param {number} extent - The extent.
* @param {number} invCtxScale - The ration to scale line width and radius circle.
* @param {boolean} canBeFilled - true if feature.type == FEATURE_TYPES.POLYGON
*/
function drawPolygon(ctx, vertices, indices = [{ offset: 0, count: 1 }], size, extent, invCtxScale, canBeFilled) {
function drawPolygon(
ctx,
vertices,
indices = [{ offset: 0, count: 1 }],
size,
style,
extent,
invCtxScale,
canBeFilled,
) {
if (vertices.length === 0) {
return;
}
Expand All @@ -38,59 +125,83 @@
}
}
}
style.applyToCanvasPolygon(ctx, path, invCtxScale, canBeFilled);

const { stroke, fill } = style;

if (stroke && stroke.width > 0) {
// TO DO add possibility of using a pattern (https://github.com/iTowns/itowns/issues/2210)
applyStrokeToPolygon(ctx, path, stroke, invCtxScale);
}

if (canBeFilled && fill && (fill.pattern || fill.color)) {
applyFillToPolygon(ctx, path, fill, invCtxScale);
}
}

function drawPoint(ctx, x, y, invCtxScale) {
function drawPoint(
/** @type {CanvasRenderingContext2D} */ctx,
/** @type {number} */x,
/** @type {number} */y,
point,
/** @type {number} */scale,
) {
const { color, radius = 3.0, line, width = 1.0, opacity = 1.0 } = point;

ctx.beginPath();
const opacity = style.point.opacity == undefined ? 1.0 : style.point.opacity;
ctx.arc(x, y, radius * scale, 0, 2 * Math.PI, false);

if (opacity !== ctx.globalAlpha) {
ctx.globalAlpha = opacity;
}

ctx.arc(x, y, (style.point.radius || 3.0) * invCtxScale, 0, 2 * Math.PI, false);
if (style.point.color) {
ctx.fillStyle = style.point.color;
if (color) {
ctx.fillStyle = color;
ctx.fill();
}
if (style.point.line) {
ctx.lineWidth = (style.point.width || 1.0) * invCtxScale;
ctx.strokeStyle = style.point.line;
if (line) {
ctx.lineWidth = width * scale;
ctx.strokeStyle = line;
ctx.stroke();
}
}

const coord = new Coordinates('EPSG:4326', 0, 0, 0);

function drawFeature(ctx, feature, extent, invCtxScale) {
function drawFeature(
/** @type {CanvasRenderingContext2D} */ctx,
/** @type {Feature} */feature,
style,
/** @type {Extent} */extent,
/** @type {number} */invCtxScale,
) {
const extentDim = extent.planarDimensions();
const scaleRadius = extentDim.x / ctx.canvas.width;
const { zoom } = style;
const { min = 0, max = Infinity } = zoom;

for (const geometry of feature.geometries) {
if (Extent.intersectsExtent(geometry.extent, extent)) {
if (geometry.extent && Extent.intersectsExtent(geometry.extent, extent)) {
context.setGeometry(geometry);
if (style.zoom.min > style.context.zoom || style.zoom.max <= style.context.zoom) {
if (min > style.context.zoom || max <= style.context.zoom) {
return;
}

if (
feature.type === FEATURE_TYPES.POINT && style.point
) {
if (feature.type === FEATURE_TYPES.POINT && style.point) {
const { radius = 3.0 } = style.point;
// cross multiplication to know in the extent system the real size of
// the point
const px = (Math.round(style.point.radius * invCtxScale) || 3 * invCtxScale) * scaleRadius;
const px = Math.round(radius * 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], invCtxScale);
drawPoint(ctx, feature.vertices[j], feature.vertices[j + 1], style.point, invCtxScale);
}
}
}
} else {
drawPolygon(ctx, feature.vertices, geometry.indices, feature.size, extent, invCtxScale, (feature.type == FEATURE_TYPES.POLYGON));
drawPolygon(ctx, feature.vertices, geometry.indices, feature.size, style, extent, invCtxScale, (feature.type == FEATURE_TYPES.POLYGON));
}
}
}
Expand All @@ -109,29 +220,32 @@
export default {
// backgroundColor is a THREE.Color to specify a color to fill the texture
// with, given there is no feature passed in parameter
createTextureFromFeature(collection, extent, sizeTexture, layerStyle, backgroundColor) {
style = layerStyle || defaultStyle;
createTextureFromFeature(
/** @type {FeatureCollection | null} */collection,
/** @type {Extent} */extent,
/** @type {number} */zoom,
/** @type {number} */sizeTexture,
/** @type {object} */layerStyle,
/** @type {THREE.Color} */backgroundColor,
) {
const style = layerStyle ?? defaultStyle;
style.setContext(context);
let texture;
let /** @type {THREE.Texture} */texture;

if (collection) {
// A texture is instancied drawn canvas
// origin and dimension are used to transform the feature's coordinates to canvas's space
extent.planarDimensions(dimension);
const c = document.createElement('canvas');

coord.crs = extent.crs;

c.width = sizeTexture;
c.height = sizeTexture;
const ctx = c.getContext('2d', { willReadFrequently: true });
const ctx = createContext2D(sizeTexture, sizeTexture);
if (backgroundColor) {
ctx.fillStyle = backgroundColor.getStyle();
ctx.fillRect(0, 0, sizeTexture, sizeTexture);
}

// Documentation needed !!
ctx.globalCompositeOperation = layerStyle.globalCompositeOperation || 'source-over';
ctx.globalCompositeOperation = style.globalCompositeOperation || 'source-over';
ctx.imageSmoothingEnabled = false;
ctx.lineJoin = 'round';

Expand Down Expand Up @@ -160,15 +274,15 @@
// to scale line width and radius circle
const invCtxScale = Math.abs(1 / scale.x);

context.setZoom(extent.zoom);
context.setZoom(zoom);

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

texture = new THREE.CanvasTexture(c);
texture = new THREE.CanvasTexture(ctx.canvas);
texture.flipY = collection.isInverted;
} else if (backgroundColor) {
const data = new Uint8Array(3);
Expand Down
10 changes: 9 additions & 1 deletion packages/Main/src/Converter/textureConverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,15 @@ export default {
undefined;

destinationTile.toExtent(layer.crs, extentTexture);
texture = Feature2Texture.createTextureFromFeature(data, extentTexture, layer.subdivisionThreshold, layer.style, backgroundColor);
texture = Feature2Texture.createTextureFromFeature(
data,
extentTexture,
destinationTile.zoom,
layer.subdivisionThreshold,
layer.style,
backgroundColor,
destinationTile.zoom,
);
texture.features = data;
texture.extent = destinationTile;
} else if (data.isTexture) {
Expand Down
Loading
Loading