diff --git a/examples/source_stream_wfs_25d.html b/examples/source_stream_wfs_25d.html index 6395528738..01b691b686 100644 --- a/examples/source_stream_wfs_25d.html +++ b/examples/source_stream_wfs_25d.html @@ -52,10 +52,12 @@ // `viewerDiv` will contain iTowns' rendering area (``) viewerDiv = document.getElementById('viewerDiv'); + const centerMap = new itowns.Coordinates('EPSG:3946', 1840839, 5172718, 0); + // Instanciate PlanarView* view = new itowns.PlanarView(viewerDiv, extent, { placement: { - coord: new itowns.Coordinates('EPSG:3946', 1840839, 5172718, 0), + coord: centerMap, heading: 45, range: 1800, tilt: 30, @@ -104,13 +106,13 @@ var tile; var linesBus = []; - function altitudeLine(properties, contour) { + function altitudeLine(properties, ctx) { var result; var z = 0; - if (contour) { - result = itowns.DEMUtils.getTerrainObjectAt(view.tileLayer, contour, 0, tile); + if (ctx.coordinates) { + result = itowns.DEMUtils.getTerrainObjectAt(view.tileLayer, ctx.coordinates, 0, tile); if (!result) { - result = itowns.DEMUtils.getTerrainObjectAt(view.tileLayer, contour, 0); + result = itowns.DEMUtils.getTerrainObjectAt(view.tileLayer, ctx.coordinates, 0); } if (result) { tile = [result.tile]; @@ -121,14 +123,7 @@ } function acceptFeatureBus(properties) { - if (properties.sens == 'Aller') { - var line = properties.ligne; - if (linesBus.indexOf(line) === -1) { - linesBus.push(line); - return true; - } - } - return false; + return properties.sens === "Aller"; } lyonTclBusSource = new itowns.WFSSource({ @@ -147,37 +142,89 @@ format: 'application/json', }); + const colorsLine = new Map(); + + const colorLine = (properties) => { + const line = properties.ligne; + let color = colorsLine.get(line); + if (color === undefined) { + color = new itowns.THREE.Color(0xffffff * Math.random()); + colorsLine.set(line, color); + } + return colorsLine.get(line); + } + var lyonTclBusLayer = new itowns.FeatureGeometryLayer('lyon_tcl_bus', { filter: acceptFeatureBus, source: lyonTclBusSource, - zoom: { min: 2 }, + zoom: { min: 1 }, style: { stroke: { base_altitude: altitudeLine, + color: colorLine, width: 5, } } }); - view.addLayer(lyonTclBusLayer); + const lyonBusStopSource = new itowns.WFSSource({ + url: "https://data.grandlyon.com/geoserver/sytral/ows?", + protocol: 'wfs', + version: '2.0.0', + id: 'pool', + typeName: "sytral:tcl_sytral.tclarret", + crs: 'EPSG:3946', + extent, + format: 'application/json', + }); - function colorBuildings(properties) { + var lyonBusStopLayer = new itowns.FeatureGeometryLayer('lyon_tcl_bus_stop', { + source: lyonBusStopSource, + zoom: { min: 4 }, + style: { + point: { + base_altitude: altitudeLine, + color: 'DarkTurquoise', + radius: 30, + } + } + }); + + const orange = new itowns.THREE.Color(0xffa400); + const blue = new itowns.THREE.Color(0x47edff); + const black = new itowns.THREE.Color(0x000000); + const red = new itowns.THREE.Color(0xff0000); + + function colorBuildings(properties, ctx) { + const distance = ctx.coordinates.planarDistanceTo(centerMap); if (properties.usage_1 === 'RĂ©sidentiel') { - return color.set(0xFDFDFF); + color.set(0xFDFDFF); } else if (properties.usage_1 === 'Annexe') { - return color.set(0xC6C5B9); + color.set(0xC6C5B9); } else if (properties.usage_1 === 'Commercial et services') { - return color.set(0x62929E); + color.set(0x62929E); } else if (properties.usage_1 === 'Religieux') { - return color.set(0x393D3F); + color.set(0x393D3F); } else if (properties.usage_1 === 'Sportif') { - return color.set(0x546A7B); + color.set(0x546A7B); + } else { + color.set(0x555555); } - return color.set(0x555555); + if (distance < 300) { + return blue; + } else if (distance < 350){ + return black; + } else if (distance < 1000){ + return color; + } else if (distance < 1050){ + return red; + } + return color.lerp(orange, Math.min(distance / 4000, 1.0)); } + function extrudeBuildings(properties) { return properties.hauteur; } @@ -186,28 +233,10 @@ return properties.altitude_minimale_sol; } - meshes = []; - function scaler(/* dt */) { - var i; - var mesh; - if (meshes.length) { - view.notifyChange(); - } - for (i = 0; i < meshes.length; i++) { - mesh = meshes[i]; - mesh.scale.z = Math.min( - 1.0, mesh.scale.z + 0.1); - mesh.updateMatrixWorld(true); - } - meshes = meshes.filter(function filter(m) { return m.scale.z < 1; }); - } - function acceptFeature(properties) { return !!properties.hauteur; } - view.addFrameRequester(itowns.MAIN_LOOP_EVENTS.BEFORE_RENDER, scaler); - var wfsBuildingSource = new itowns.WFSSource({ url: 'https://wxs.ign.fr/topographie/geoportail/wfs?', version: '2.0.0', @@ -224,12 +253,6 @@ }); var wfsBuildingLayer = new itowns.FeatureGeometryLayer('wfsBuilding', { - onMeshCreated: function scaleZ(mesh) { - mesh.children.forEach(c => { - c.scale.z = 0.01; - meshes.push(c); - }) - }, batchId: function (property, featureId) { return featureId; }, filter: acceptFeature, crs: 'EPSG:3946', @@ -260,9 +283,29 @@ zoom: { min: 0, max: 20 }, text: { field: '{toponyme}', - color: 'white', + color: (p) => { + switch (p.importance) { + case 'Quartier de ville': + return 'Cornsilk'; + case 'Hameau': + return 'WhiteSmoke'; + case 'Chef-lieu de commune': + default: + return 'white'; + } + }, transform: 'uppercase', - size: 15, + size: (p) => { + switch (p.importance) { + case 'Quartier de ville': + return 11; + case 'Hameau': + return 13; + case 'Chef-lieu de commune': + default: + return 18; + } + }, haloColor: 'rgba(20,20,20, 0.8)', haloWidth: 3, }, @@ -300,6 +343,14 @@ } } + // Wait for globe initialization since we are using the elevation + // layer to calculate the base altitude of both feature layers. + // See the function `altitudeLine` above. + view.addEventListener(itowns.GLOBE_VIEW_EVENTS.GLOBE_INITIALIZED, function m() { + view.addLayer(lyonTclBusLayer); + view.addLayer(lyonBusStopLayer); + }); + window.addEventListener('mousemove', picking, false); diff --git a/examples/vector_tile_3d_mesh_mapbox.html b/examples/vector_tile_3d_mesh_mapbox.html index 6f80d82939..270f27372a 100644 --- a/examples/vector_tile_3d_mesh_mapbox.html +++ b/examples/vector_tile_3d_mesh_mapbox.html @@ -100,9 +100,9 @@ }); // Get buildings ground elevation - function altitudeBuildings(properties, coord) { - if (coord) { - return itowns.DEMUtils.getElevationValueAt(view.tileLayer, coord); + function altitudeBuildings(properties, ctx) { + if (ctx.coordinates) { + return itowns.DEMUtils.getElevationValueAt(view.tileLayer, ctx.coordinates); } } @@ -116,8 +116,8 @@ style: { fill: { // the building with type = roof doesn't have same properties as the others - base_altitude: (p, c) => { - return (altitudeBuildings(p, c) || 0) + (p.min_height || 0); + base_altitude: (p, ctx) => { + return (altitudeBuildings(p, ctx) || 0) + (p.min_height || 0); }, extrusion_height: (p) => { return p.height || 0; diff --git a/src/Converter/Feature2Mesh.js b/src/Converter/Feature2Mesh.js index fc36953699..f459bb29e5 100644 --- a/src/Converter/Feature2Mesh.js +++ b/src/Converter/Feature2Mesh.js @@ -9,8 +9,55 @@ import OrientationUtils from 'Utils/OrientationUtils'; import Coordinates from 'Core/Geographic/Coordinates'; const coord = new Coordinates('EPSG:4326', 0, 0, 0); + +class FeatureContext { + #worldCoord = new Coordinates('EPSG:4326', 0, 0, 0); + #localCoordinates = new Coordinates('EPSG:4326', 0, 0, 0); + #isProjected = true; + #geometry = {}; + + constructor() { + this.globals = {}; + } + + setGeometry(g) { + this.#geometry = g; + } + + setCollection(c) { + this.collection = c; + this.#localCoordinates.setCrs(c.crs); + } + + setLocalCoordinatesFromArray(vertices, offset) { + this.#isProjected = false; + return this.#localCoordinates.setFromArray(vertices, offset); + } + + get properties() { + return this.#geometry.properties; + } + + get coordinates() { + if (!this.#isProjected) { + this.#isProjected = true; + this.#worldCoord.copy(this.#localCoordinates).applyMatrix4(this.collection.matrixWorld); + if (this.#localCoordinates.crs == 'EPSG:4978') { + return this.#worldCoord.as('EPSG:4326', this.#worldCoord); + } + } + return this.#worldCoord; + } +} + +const context = new FeatureContext(); + const dim_ref = new THREE.Vector2(); const dim = new THREE.Vector2(); +const normal = new THREE.Vector3(); +const base = new THREE.Vector3(); +const extrusion = new THREE.Vector3(); +const inverseScale = new THREE.Vector3(); const extent = new Extent('EPSG:4326', 0, 0, 0, 0); const _color = new THREE.Color(); @@ -26,10 +73,22 @@ class FeatureMesh extends THREE.Group { #place = new THREE.Group(); constructor(meshes, collection) { super(); + this.meshes = new THREE.Group().add(...meshes); + this.#collection = new THREE.Group().add(this.meshes); this.#collection.quaternion.copy(collection.quaternion); this.#collection.position.copy(collection.position); + + if (collection.crs == 'EPSG:4978') { + normal.copy(collection.center.geodesicNormal); + } else { + normal.set(0, 0, 1); + } + + normal.multiplyScalar(collection.center.z); + this.#collection.position.sub(normal); + this.#collection.scale.copy(collection.scale); this.#collection.updateMatrix(); @@ -95,22 +154,6 @@ function toColor(color) { } } -function fillColorArray(colors, length, color, offset = 0) { - offset *= 3; - const len = offset + length * 3; - for (let i = offset; i < len; i += 3) { - colors[i] = color.r * 255; - colors[i + 1] = color.g * 255; - colors[i + 2] = color.b * 255; - } -} - -function fillBatchIdArray(batchId, batchIdArray, start, end) { - for (let i = start; i < end; i++) { - batchIdArray[i] = batchId; - } -} - function getIntArrayFromSize(data, size) { if (size <= maxValueUint8) { return new Uint8Array(data); @@ -136,41 +179,6 @@ function separateMeshes(object3D) { } /** - * Convert coordinates to vertices positionned at a given altitude - * - * @param {number[]} ptsIn - Coordinates of a feature. - * @param {number[]} normals - Coordinates of a feature. - * @param {number[]} target - Target to copy result. - * @param {number} zTranslation - Translation on Z axe. - * @param {number} offsetOut - The offset array value to copy on target - * @param {number} countIn - The count of coordinates to read in ptsIn - * @param {number} startIn - The offser array to strat reading in ptsIn - */ -function coordinatesToVertices(ptsIn, normals, target, zTranslation, offsetOut = 0, countIn = ptsIn.length / 3, startIn = offsetOut) { - startIn *= 3; - countIn *= 3; - offsetOut *= 3; - const endIn = startIn + countIn; - - if (normals) { - for (let i = startIn, j = offsetOut; i < endIn; i += 3, j += 3) { - // move the vertex following the normal, to put the point on the good altitude - // fill the vertices array at the offset position - target[j] = ptsIn[i] + normals[i] * zTranslation; - target[j + 1] = ptsIn[i + 1] + normals[i + 1] * zTranslation; - target[j + 2] = ptsIn[i + 2] + normals[i + 2] * zTranslation; - } - } else { - for (let i = startIn, j = offsetOut; i < endIn; i += 3, j += 3) { - // move the vertex following the z axe - target[j] = ptsIn[i]; - target[j + 1] = ptsIn[i + 1]; - target[j + 2] = ptsIn[i + 2] + zTranslation; - } - } -} - -/* * Add indices for the side faces. * We loop over the contour and create a side face made of two triangles. * @@ -215,39 +223,45 @@ function addExtrudedPolygonSideFaces(indices, length, offset, count, isClockWise function featureToPoint(feature, options) { const ptsIn = feature.vertices; - const normals = feature.normals; const colors = new Uint8Array(ptsIn.length); - const batchIds = options.batchId ? new Uint32Array(ptsIn.length / 3) : undefined; + const batchIds = new Uint32Array(ptsIn.length); + const batchId = options.batchId || ((p, id) => id); + let featureId = 0; + const vertices = new Float32Array(ptsIn); + inverseScale.setFromMatrixScale(context.collection.matrixWorldInverse); + normal.set(0, 0, 1).multiply(inverseScale); + context.globals = { point: true }; - let vertices; - const zTranslation = options.GlobalZTrans - feature.altitude.min; - if (zTranslation !== 0) { - vertices = new Float32Array(ptsIn.length); - coordinatesToVertices(ptsIn, normals, vertices, zTranslation); - } else { - vertices = new Float32Array(ptsIn); - } - const globals = { point: true }; for (const geometry of feature.geometries) { - const context = { globals, properties: () => geometry.properties }; - const style = feature.style.applyContext(context); - const start = geometry.indices[0].offset; const count = geometry.indices[0].count; - fillColorArray(colors, count, toColor(style.point.color), start); + const end = start + count; + const id = batchId(geometry.properties, featureId); + context.setGeometry(geometry); - if (batchIds) { - const id = options.batchId(geometry.properties, featureId); - fillBatchIdArray(id, batchIds, start, start + count); - featureId++; + for (let v = start * 3, j = start; j < end; v += 3, j += 1) { + if (feature.normals) { + normal.fromArray(feature.normals, v).multiply(inverseScale); + } + + coord.copy(context.setLocalCoordinatesFromArray(feature.vertices, v)); + const style = feature.style.applyContext(context); + const { base_altitude, color } = style.point; + coord.z = 0; + + // populate vertices + base.copy(normal).multiplyScalar(base_altitude).add(coord).toArray(vertices, v); + toColor(color).multiplyScalar(255).toArray(colors, v); + batchIds[j] = id; } + featureId++; } const geom = new THREE.BufferGeometry(); geom.setAttribute('position', new THREE.BufferAttribute(vertices, 3)); geom.setAttribute('color', new THREE.BufferAttribute(colors, 3, true)); - if (batchIds) { geom.setAttribute('batchId', new THREE.BufferAttribute(batchIds, 1)); } + geom.setAttribute('batchId', new THREE.BufferAttribute(batchIds, 1)); options.pointMaterial.size = feature.style.point.radius; @@ -256,48 +270,43 @@ function featureToPoint(feature, options) { function featureToLine(feature, options) { const ptsIn = feature.vertices; - const normals = feature.normals; const colors = new Uint8Array(ptsIn.length); const count = ptsIn.length / 3; - const batchIds = options.batchId ? new Uint32Array(count) : undefined; + const batchIds = new Uint32Array(count); + const batchId = options.batchId || ((p, id) => id); let featureId = 0; - let vertices; - const zTranslation = options.GlobalZTrans - feature.altitude.min; - if (zTranslation != 0) { - vertices = new Float32Array(ptsIn.length); - coordinatesToVertices(ptsIn, normals, vertices, zTranslation); - } else { - vertices = new Float32Array(ptsIn); - } + const vertices = new Float32Array(ptsIn.length); const geom = new THREE.BufferGeometry(); geom.setAttribute('position', new THREE.BufferAttribute(vertices, 3)); - let lines; - // TODO CREATE material for each feature options.lineMaterial.linewidth = feature.style.stroke.width; - const globals = { stroke: true }; - if (feature.geometries.length > 1) { - const countIndices = (count - feature.geometries.length) * 2; - const indices = getIntArrayFromSize(countIndices, count); - let i = 0; - // Multi line case - for (const geometry of feature.geometries) { - const context = { globals, properties: () => geometry.properties }; - const style = feature.style.applyContext(context); + context.globals = { stroke: true }; - const start = geometry.indices[0].offset; - // To avoid integer overflow with indice value (16 bits) - if (start > 0xffff) { - console.warn('Feature to Line: integer overflow, too many points in lines'); - break; - } - const count = geometry.indices[0].count; - const end = start + count; - fillColorArray(colors, count, toColor(style.stroke.color), start); - for (let j = start; j < end - 1; j++) { + const countIndices = (count - feature.geometries.length) * 2; + const indices = getIntArrayFromSize(countIndices, count); + + let i = 0; + inverseScale.setFromMatrixScale(context.collection.matrixWorldInverse); + normal.set(0, 0, 1).multiply(inverseScale); + // Multi line case + for (const geometry of feature.geometries) { + context.setGeometry(geometry); + const id = batchId(geometry.properties, featureId); + + const start = geometry.indices[0].offset; + // To avoid integer overflow with indice value (16 bits) + if (start > 0xffff) { + console.warn('Feature to Line: integer overflow, too many points in lines'); + break; + } + const count = geometry.indices[0].count; + const end = start + count; + + for (let v = start * 3, j = start; j < end; v += 3, j += 1) { + if (j < end - 1) { if (j < 0xffff) { indices[i++] = j; indices[i++] = j + 1; @@ -305,52 +314,41 @@ function featureToLine(feature, options) { break; } } - if (batchIds) { - const id = options.batchId(geometry.properties, featureId); - fillBatchIdArray(id, batchIds, start, end); - featureId++; + if (feature.normals) { + normal.fromArray(feature.normals, v).multiply(inverseScale); } + + coord.copy(context.setLocalCoordinatesFromArray(feature.vertices, v)); + const style = feature.style.applyContext(context); + const { base_altitude, color } = style.stroke; + coord.z = 0; + + // populate geometry buffers + base.copy(normal).multiplyScalar(base_altitude).add(coord).toArray(vertices, v); + toColor(color).multiplyScalar(255).toArray(colors, v); + batchIds[j] = id; } - geom.setAttribute('color', new THREE.BufferAttribute(colors, 3, true)); - if (batchIds) { geom.setAttribute('batchId', new THREE.BufferAttribute(batchIds, 1)); } - geom.setIndex(new THREE.BufferAttribute(indices, 1)); - lines = new THREE.LineSegments(geom, options.lineMaterial); - } else { - const context = { globals, properties: () => feature.geometries[0].properties }; - const style = feature.style.applyContext(context); - - fillColorArray(colors, count, toColor(style.stroke.color)); - geom.setAttribute('color', new THREE.BufferAttribute(colors, 3, true)); - if (batchIds) { - const id = options.batchId(feature.geometries[0].properties, featureId); - fillBatchIdArray(id, batchIds, 0, count); - geom.setAttribute('batchId', new THREE.BufferAttribute(batchIds, 1)); - } - lines = new THREE.Line(geom, options.lineMaterial); + + featureId++; } - return lines; + geom.setAttribute('color', new THREE.BufferAttribute(colors, 3, true)); + geom.setAttribute('batchId', new THREE.BufferAttribute(batchIds, 1)); + geom.setIndex(new THREE.BufferAttribute(indices, 1)); + return new THREE.LineSegments(geom, options.lineMaterial); } function featureToPolygon(feature, options) { - const ptsIn = feature.vertices; - const normals = feature.normals; - - let vertices; - const zTranslation = options.GlobalZTrans - feature.altitude.min; - if (zTranslation != 0) { - vertices = new Float32Array(ptsIn.length); - coordinatesToVertices(ptsIn, normals, vertices, zTranslation); - } else { - vertices = new Float32Array(ptsIn); - } - - const colors = new Uint8Array(ptsIn.length); + const vertices = new Float32Array(feature.vertices); + const colors = new Uint8Array(feature.vertices.length); const indices = []; - const batchIds = options.batchId ? new Uint32Array(vertices.length / 3) : undefined; - let featureId = 0; + const batchIds = new Uint32Array(vertices.length / 3); + const batchId = options.batchId || ((p, id) => id); + context.globals = { fill: true }; - const globals = { fill: true }; + inverseScale.setFromMatrixScale(context.collection.matrixWorldInverse); + normal.set(0, 0, 1).multiply(inverseScale); + let featureId = 0; for (const geometry of feature.geometries) { const start = geometry.indices[0].offset; @@ -359,14 +357,32 @@ function featureToPolygon(feature, options) { console.warn('Feature to Polygon: integer overflow, too many points in polygons'); break; } - const context = { globals, properties: () => geometry.properties }; - const style = feature.style.applyContext(context); + context.setGeometry(geometry); const lastIndice = geometry.indices.slice(-1)[0]; const end = lastIndice.offset + lastIndice.count; const count = end - start; + const startIn = start * 3; + const endIn = startIn + count * 3; + const id = batchId(geometry.properties, featureId); + + for (let i = startIn, b = start; i < endIn; i += 3, b += 1) { + if (feature.normals) { + normal.fromArray(feature.normals, i).multiply(inverseScale); + } - fillColorArray(colors, count, toColor(style.fill.color), start); + coord.copy(context.setLocalCoordinatesFromArray(feature.vertices, i)); + const style = feature.style.applyContext(context); + const { base_altitude, color } = style.fill; + coord.z = 0; + + // populate geometry buffers + base.copy(normal).multiplyScalar(base_altitude).add(coord).toArray(vertices, i); + batchIds[b] = id; + toColor(color).multiplyScalar(255).toArray(colors, i); + } + + featureId++; const geomVertices = vertices.slice(start * 3, end * 3); const holesOffsets = geometry.indices.map(i => i.offset - start).slice(1); @@ -378,18 +394,12 @@ function featureToPolygon(feature, options) { for (let i = 0; i < triangles.length; i++) { indices[startIndice + i] = triangles[i] + start; } - - if (batchIds) { - const id = options.batchId(geometry.properties, featureId); - fillBatchIdArray(id, batchIds, start, end); - featureId++; - } } const geom = new THREE.BufferGeometry(); geom.setAttribute('position', new THREE.BufferAttribute(vertices, 3)); geom.setAttribute('color', new THREE.BufferAttribute(colors, 3, true)); - if (batchIds) { geom.setAttribute('batchId', new THREE.BufferAttribute(batchIds, 1)); } + geom.setAttribute('batchId', new THREE.BufferAttribute(batchIds, 1)); geom.setIndex(new THREE.BufferAttribute(getIntArrayFromSize(indices, vertices.length / 3), 1)); @@ -408,48 +418,67 @@ function area(contour, offset, count) { return a * 0.5; } -const bottomColor = new THREE.Color(); function featureToExtrudedPolygon(feature, options) { const ptsIn = feature.vertices; - const normals = feature.normals; - - const z = options.GlobalZTrans - feature.altitude.min; const vertices = new Float32Array(ptsIn.length * 2); const totalVertices = ptsIn.length / 3; const colors = new Uint8Array(ptsIn.length * 2); + const indices = []; - const batchIds = options.batchId ? new Uint32Array(vertices.length / 3) : undefined; + const batchIds = new Uint32Array(vertices.length / 3); + const batchId = options.batchId || ((p, id) => id); + let featureId = 0; - const globals = { fill: true }; + context.globals = { fill: true }; + inverseScale.setFromMatrixScale(context.collection.matrixWorldInverse); + normal.set(0, 0, 1).multiply(inverseScale); + coord.setCrs(context.collection.crs); for (const geometry of feature.geometries) { - const start = geometry.indices[0].offset; - - const context = { globals, properties: () => geometry.properties }; - const style = feature.style.applyContext(context); - + context.setGeometry(geometry); + const start = geometry.indices[0].offset; const lastIndice = geometry.indices.slice(-1)[0]; const end = lastIndice.offset + lastIndice.count; const count = end - start; const isClockWise = geometry.indices[0].ccw ?? (area(ptsIn, start, count) < 0); - // topColor is assigned to the top of extruded polygon - const topColor = toColor(style.fill.color); - // bottomColor is assigned to the bottom of extruded polygon - bottomColor.copy(topColor); - bottomColor.multiplyScalar(0.5); + const startIn = start * 3; + const startTop = start + totalVertices; + const endIn = startIn + count * 3; + const id = batchId(geometry.properties, featureId); + + for (let i = startIn, t = startIn + ptsIn.length, b = start; i < endIn; i += 3, t += 3, b += 1) { + if (feature.normals) { + normal.fromArray(feature.normals, i).multiply(inverseScale); + } - coordinatesToVertices(ptsIn, normals, vertices, z, start, count); - fillColorArray(colors, count, bottomColor, start); + coord.copy(context.setLocalCoordinatesFromArray(ptsIn, i)); + + const style = feature.style.applyContext(context); + const { base_altitude, extrusion_height, color } = style.fill; + coord.z = 0; + + // populate base geometry buffers + base.copy(normal).multiplyScalar(base_altitude).add(coord).toArray(vertices, i); + batchIds[b] = id; + + // populate top geometry buffers + extrusion.copy(normal).multiplyScalar(extrusion_height).add(base).toArray(vertices, t); + batchIds[b + totalVertices] = id; + + // coloring base and top mesh + const meshColor = toColor(color).multiplyScalar(255); + meshColor.toArray(colors, t); // top + meshColor.multiplyScalar(0.5).toArray(colors, i); // base is half dark + } + + featureId++; - const startTop = start + totalVertices; const endTop = end + totalVertices; - coordinatesToVertices(ptsIn, normals, vertices, z + style.fill.extrusion_height, startTop, count, start); - fillColorArray(colors, count, topColor, startTop); const geomVertices = vertices.slice(startTop * 3, endTop * 3); const holesOffsets = geometry.indices.map(i => i.offset - start).slice(1); @@ -480,19 +509,12 @@ function featureToExtrudedPolygon(feature, options) { indice.count, !(indice.ccw ?? isClockWise)); } - - if (batchIds) { - const id = options.batchId(geometry.properties, featureId); - fillBatchIdArray(id, batchIds, start, end); - fillBatchIdArray(id, batchIds, startTop, endTop); - featureId++; - } } const geom = new THREE.BufferGeometry(); geom.setAttribute('position', new THREE.BufferAttribute(vertices, 3)); geom.setAttribute('color', new THREE.BufferAttribute(colors, 3, true)); - if (batchIds) { geom.setAttribute('batchId', new THREE.BufferAttribute(batchIds, 1)); } + geom.setAttribute('batchId', new THREE.BufferAttribute(batchIds, 1)); geom.setIndex(new THREE.BufferAttribute(getIntArrayFromSize(indices, vertices.length / 3), 1)); @@ -590,7 +612,6 @@ function featureToMesh(feature, options) { mesh.material.color = new THREE.Color(0xffffff); } mesh.feature = feature; - mesh.position.z = feature.altitude.min - options.GlobalZTrans; if (options.layer) { mesh.layer = options.layer; @@ -650,12 +671,12 @@ export default { options.layer = this; } + context.setCollection(collection); + const features = collection.features; if (!features || features.length == 0) { return; } - options.GlobalZTrans = collection.center.z; - const meshes = features.map(feature => featureToMesh(feature, options)); const featureNode = new FeatureMesh(meshes, collection); diff --git a/src/Core/Feature.js b/src/Core/Feature.js index 968e2dfff7..26bf4a69cd 100644 --- a/src/Core/Feature.js +++ b/src/Core/Feature.js @@ -14,17 +14,12 @@ function _extendBuffer(feature, size) { feature.normals.length = feature.vertices.length; } } -function _setGeometryValues(geom, feature, coord) { +function _setGeometryValues(feature, coord) { if (feature.normals) { coord.geodesicNormal.toArray(feature.normals, feature._pos); } feature._pushValues(coord.x, coord.y, coord.z); - - if (geom.size == 3) { - geom.altitude.min = Math.min(geom.altitude.min, coord.z); - geom.altitude.max = Math.max(geom.altitude.max, coord.z); - } } const coordOut = new Coordinates('EPSG:4326', 0, 0, 0); @@ -35,8 +30,6 @@ export const FEATURE_TYPES = { POLYGON: 2, }; -const typeToStyleProperty = ['point', 'stroke', 'fill']; - /** * @property {string} crs - The CRS to convert the input coordinates to. * @property {string} [structure='2d'] - data structure type : 2d or 3d. @@ -80,10 +73,6 @@ export class FeatureGeometry { this.extent = defaultExtent(feature.extent.crs); this.#currentExtent = defaultExtent(feature.extent.crs); } - this.altitude = { - min: Infinity, - max: -Infinity, - }; } /** * Add a new marker to indicate the starting of sub geometry and extends the vertices buffer. @@ -127,11 +116,6 @@ export class FeatureGeometry { return this.indices[last]; } - baseAltitude(feature, coordinates) { - const base_altitude = feature.style[typeToStyleProperty[feature.type]].base_altitude || 0; - return isNaN(base_altitude) ? base_altitude(this.properties, coordinates) : base_altitude; - } - /** * Push new coordinates in vertices buffer. * @param {Feature} feature - the feature containing the geometry @@ -143,12 +127,11 @@ export class FeatureGeometry { this.pushCoordinates(coordIn, feature); return; } - coordIn.z = this.baseAltitude(feature, coordIn); coordIn.as(feature.crs, coordOut); feature.transformToLocalSystem(coordOut); - _setGeometryValues(this, feature, coordOut); + _setGeometryValues(feature, coordOut); // expand extent if present if (this.#currentExtent) { @@ -174,9 +157,8 @@ export class FeatureGeometry { this.pushCoordinatesValues(feature, { x: coordIn, y: coordProj, normal: args[0] }, args[1]); return; } - coordIn.z = this.baseAltitude(feature, coordProj); - _setGeometryValues(this, feature, coordIn); + _setGeometryValues(feature, coordIn); // expand extent if present if (this.#currentExtent) { @@ -270,11 +252,6 @@ class Feature { this._pos = 0; this._pushValues = (this.size === 3 ? push3DValues : push2DValues).bind(this); this.style = new Style({}, collection.style); - - this.altitude = { - min: Infinity, - max: -Infinity, - }; } /** * Instance a new {@link FeatureGeometry} and push in {@link Feature}. @@ -293,11 +270,6 @@ class Feature { if (this.extent) { this.extent.union(geometry.extent); } - - if (this.size == 3) { - this.altitude.min = Math.min(this.altitude.min, geometry.altitude.min); - this.altitude.max = Math.max(this.altitude.max, geometry.altitude.max); - } } /** @@ -420,11 +392,6 @@ export class FeatureCollection extends THREE.Object3D { }; this.#transformToLocalSystem = transformToLocalSystem3D; } - - this.altitude = { - min: Infinity, - max: -Infinity, - }; } /** @@ -452,14 +419,6 @@ export class FeatureCollection extends THREE.Object3D { this.extent.union(ext); } } - if (this.size == 3) { - for (const feature of this.features) { - this.altitude.min = Math.min(this.altitude.min, feature.altitude.min); - this.altitude.max = Math.max(this.altitude.max, feature.altitude.max); - } - } - this.altitude.min = this.altitude.min == Infinity ? 0 : this.altitude.min; - this.altitude.max = this.altitude.max == -Infinity ? 0 : this.altitude.max; } /** @@ -535,8 +494,6 @@ export class FeatureCollection extends THREE.Object3D { ref.normals = feature.normals; ref.size = feature.size; ref.vertices = feature.vertices; - ref.altitude.min = feature.altitude.min; - ref.altitude.max = feature.altitude.max; ref._pos = feature._pos; this.features.push(ref); return ref; diff --git a/src/Core/Style.js b/src/Core/Style.js index 97f46b092c..e1e8da9040 100644 --- a/src/Core/Style.js +++ b/src/Core/Style.js @@ -16,8 +16,8 @@ const inv255 = 1 / 255; const canvas = (typeof document !== 'undefined') ? document.createElement('canvas') : {}; const style_properties = {}; -function base_altitudeDefault(properties, coordinates = { z: 0 }) { - return coordinates.z; +function base_altitudeDefault(properties, ctx) { + return ctx?.coordinates?.z || ctx?.collection?.center?.z || 0; } function mapPropertiesFromContext(mainKey, from, to, context) { @@ -44,7 +44,10 @@ export function readExpression(property, ctx) { } return property.stops[0][1]; } else if (property instanceof Function) { - return property(ctx.properties()); + // TOBREAK: Pass the current `context` as a unique parameter. + // In this proposal, metadata will be accessed in the callee by the + // `context.properties` property. + return property(ctx.properties, ctx); } else { return property; } diff --git a/test/unit/feature2mesh.js b/test/unit/feature2mesh.js index 6643d1cab0..60867dce2b 100644 --- a/test/unit/feature2mesh.js +++ b/test/unit/feature2mesh.js @@ -88,7 +88,7 @@ describe('Feature2Mesh', function () { parsed2.then((collection) => { const mesh = Feature2Mesh.convert()(collection).meshes; assert.equal(mesh.children[0].type, 'Points'); - assert.equal(mesh.children[1].type, 'Line'); + assert.equal(mesh.children[1].type, 'LineSegments'); assert.equal(mesh.children[2].type, 'Mesh'); done(); }).catch(done); diff --git a/test/unit/source.js b/test/unit/source.js index 0bf85dd561..ffd2fba1cb 100644 --- a/test/unit/source.js +++ b/test/unit/source.js @@ -303,6 +303,7 @@ describe('Sources', function () { describe('C3DTilesSource', function () { const params3DTiles = { url: 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/3DTiles/lyon_1_4978/tileset.json', + networkOptions: process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {}, }; it('should throw an error for having no required parameters', function () {