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 }); }); });