From 34bfbb4f4f2fb799dda1a6519172ff3310567ce2 Mon Sep 17 00:00:00 2001 From: Roberto Raggi Date: Mon, 1 Feb 2021 14:26:59 +0100 Subject: [PATCH] HARP-13254: Enfore the winding convention expected by the vt decoder. This change ensures that the winding of the polygons decoded by the tiled GeoJson adapter follow the convention expected by the rendering techniques. Signed-off-by: Roberto Raggi --- .../adapters/geojson/GeoJsonDataAdapter.ts | 8 +- test/rendering/GeoJsonFeaturesRendering.ts | 178 ++++++++++++++---- 2 files changed, 141 insertions(+), 45 deletions(-) diff --git a/@here/harp-vectortile-datasource/lib/adapters/geojson/GeoJsonDataAdapter.ts b/@here/harp-vectortile-datasource/lib/adapters/geojson/GeoJsonDataAdapter.ts index 8bf1a3743d..a91e8bf719 100644 --- a/@here/harp-vectortile-datasource/lib/adapters/geojson/GeoJsonDataAdapter.ts +++ b/@here/harp-vectortile-datasource/lib/adapters/geojson/GeoJsonDataAdapter.ts @@ -145,13 +145,11 @@ function signedPolygonArea(contour: GeoPointLike[]): number { } function convertRings(coordinates: GeoPointLike[][], decodeInfo: DecodeInfo): IPolygonGeometry { - let outerWinding: boolean | undefined; const rings = coordinates.map((ring, i) => { + const isOuterRing = i === 0; + const isClockWise = signedPolygonArea(ring) < 0; const { positions } = convertLineStringGeometry(ring, decodeInfo); - const winding = signedPolygonArea(ring) < 0; - if (i === 0) { - outerWinding = winding; - } else if (winding === outerWinding) { + if ((isOuterRing && !isClockWise) || (!isOuterRing && isClockWise)) { positions.reverse(); } return positions; diff --git a/test/rendering/GeoJsonFeaturesRendering.ts b/test/rendering/GeoJsonFeaturesRendering.ts index 99ed37349f..8d76a128f3 100644 --- a/test/rendering/GeoJsonFeaturesRendering.ts +++ b/test/rendering/GeoJsonFeaturesRendering.ts @@ -8,12 +8,16 @@ import { FeatureCollection, + FlatTheme, LineCaps, SolidLineTechniqueParams, Style } from "@here/harp-datasource-protocol"; import { GeoPointLike } from "@here/harp-geoutils"; +import { LookAtParams } from "@here/harp-mapview"; import * as turf from "@turf/turf"; +import { assert } from "chai"; +import * as THREE from "three"; import { GeoJsonStore } from "./utils/GeoJsonStore"; import { GeoJsonTest } from "./utils/GeoJsonTest"; @@ -158,57 +162,151 @@ describe("GeoJson features", function () { }); describe("extruded-polygon technique", async function () { - it("Polygon touching tile border", async function () { - const polygon: turf.Feature = { - type: "Feature", - properties: {}, - geometry: { - type: "Polygon", - coordinates: [ - [ - [0.00671649362467299, 51.49353450058163, 0], - [0.006598810705050831, 51.48737650885326, 0], - [0.015169103358567556, 51.48731300784492, 0], - [0.015286786278189713, 51.49347100814989, 0], - [0.006716493624672999, 51.49353450058163, 0], - [0.00671649362467299, 51.49353450058163, 0] - ] - ] + // Helper function to test the winding of the rings. + + const isClockWise = (ring: turf.Position[]) => + THREE.ShapeUtils.isClockWise(ring.map(p => new THREE.Vector2().fromArray(p))); + + // the theme used by this test suite. + const theme: FlatTheme = { + lights, + styles: [ + { + styleSet: "geojson", + technique: "extruded-polygon", + color: "#0335a2", + lineWidth: 1, + lineColor: "red", + constantHeight: true, + height: 300 } - }; + ] + }; + + // the polygon feature + const polygon = turf.polygon([ + [ + [0.00671649362467299, 51.49353450058163, 0], + [0.006598810705050831, 51.48737650885326, 0], + [0.015169103358567556, 51.48731300784492, 0], + [0.015286786278189713, 51.49347100814989, 0], + [0.006716493624672999, 51.49353450058163, 0], + [0.00671649362467299, 51.49353450058163, 0] + ] + ]); + + // the center geo location of the polygon + const [longitude, latitude] = turf.center(polygon).geometry!.coordinates; + // the default camera setup used by this test suite. + const lookAt: Partial = { + target: [longitude, latitude], + zoomLevel: 15, + tilt: 40, + heading: 45 + }; + + it("Clockwise polygon touching tile border", async function () { const dataProvider = new GeoJsonStore(); dataProvider.features.insert(polygon); - const geoJson = turf.featureCollection([polygon]); + await geoJsonTest.run({ + mochaTest: this, + dataProvider, + testImageName: `geojson-extruded-polygon-touching-tile-bounds`, + lookAt, + theme + }); + }); + + it("Clockwise polygon with a hole", async function () { + const dataProvider = new GeoJsonStore(); + + const innerPolygon = turf.transformScale(polygon, 0.8, { origin: "center" }); - const [longitude, latitude] = turf.center(geoJson).geometry!.coordinates; + const polygonWithHole = turf.difference(polygon, innerPolygon)!; + + dataProvider.features.insert(polygonWithHole); + + assert.strictEqual(polygonWithHole?.geometry?.coordinates.length, 2); + + const outerRing = polygonWithHole.geometry!.coordinates[0] as turf.Position[]; + const innerRing = polygonWithHole.geometry!.coordinates[1] as turf.Position[]; + + // test that the winding of the inner ring is opposite to the winding + // of the outer ring. + assert.notStrictEqual(isClockWise(outerRing), isClockWise(innerRing)); await geoJsonTest.run({ mochaTest: this, dataProvider, - testImageName: `geojson-extruded-polygon-touching-tile-bounds`, - lookAt: { - target: [longitude, latitude], - zoomLevel: 15, - tilt: 55, - heading: 45 - }, - theme: { - lights, - styles: [ - { - styleSet: "geojson", - technique: "extruded-polygon", - color: "#0335a2", - lineWidth: 1, - lineColor: "red", - constantHeight: true, - height: 300 - } - ] - } + testImageName: `geojson-extruded-polygon-cw-with-hole`, + lookAt, + theme + }); + }); + + it("Clockwise polygon with a hole wrong winding", async function () { + const dataProvider = new GeoJsonStore(); + + const innerPolygon = turf.transformScale(polygon, 0.8, { origin: "center" }); + + const polygonWithHole = turf.difference(polygon, innerPolygon)!; + + dataProvider.features.insert(polygonWithHole); + + assert.strictEqual(polygonWithHole?.geometry?.coordinates.length, 2); + + const outerRing = polygonWithHole.geometry!.coordinates[0] as turf.Position[]; + const innerRing = polygonWithHole.geometry!.coordinates[1] as turf.Position[]; + + // reverse the winding of the inner ring so to have the same winding + // of the outer ring + innerRing.reverse(); + + // verify that the winding of the rings is the same + assert.strictEqual(isClockWise(outerRing), isClockWise(innerRing)); + + await geoJsonTest.run({ + mochaTest: this, + dataProvider, + testImageName: `geojson-extruded-polygon-cw-wrong-winding-of-inner-ring`, + lookAt, + theme + }); + }); + + it("Clockwise multi-polygon with a hole wrong winding", async function () { + const dataProvider = new GeoJsonStore(); + + const scaledPolygon = turf.transformScale(polygon, 0.5, { origin: "center" }); + + const innerPolygon = turf.transformScale(scaledPolygon, 0.8, { origin: "center" }); + + const polygonWithHole = turf.difference(scaledPolygon, innerPolygon)!; + + const otherPolygonWithHole = turf.clone(polygonWithHole)!; + + turf.transformTranslate(polygonWithHole, 200, 90, { units: "meters", mutate: true }); + + turf.transformTranslate(otherPolygonWithHole, 200, -90, { + units: "meters", + mutate: true + }); + + const multi = turf.union(polygonWithHole as any, otherPolygonWithHole as any); + + assert.strictEqual(multi.geometry?.type, "MultiPolygon"); + + dataProvider.features.insert(multi); + + await geoJsonTest.run({ + mochaTest: this, + dataProvider, + testImageName: `geojson-extruded-multi-polygon-cw-wrong-winding-of-inner-ring`, + lookAt, + theme }); }); });