Skip to content
This repository has been archived by the owner on Mar 8, 2023. It is now read-only.

Commit

Permalink
HARP-13251: Add support for wrapping lines crossing the antimeridian.
Browse files Browse the repository at this point in the history
This change adds the function `wrapLineString` that can be used
to wrap line features crossing the antimeridian.

Signed-off-by: Roberto Raggi <roberto.raggi@here.com>
  • Loading branch information
robertoraggi committed Dec 1, 2020
1 parent 0f31957 commit b72683d
Show file tree
Hide file tree
Showing 3 changed files with 265 additions and 1 deletion.
101 changes: 100 additions & 1 deletion @here/harp-geometry/lib/ClipLineString.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { EarthConstants, GeoCoordinates, webMercatorProjection } from "@here/harp-geoutils";
import { Math2D } from "@here/harp-utils";
import { Vector2 } from "three";
import { Vector2, Vector3 } from "three";

/**
* A clipping edge.
Expand Down Expand Up @@ -153,3 +154,101 @@ export function clipLineString(

return lines;
}

/**
* The result of wrapping a line string.
*/
interface WrappedLineString {
left: GeoCoordinates[][];
middle: GeoCoordinates[][];
right: GeoCoordinates[][];
}

/**
* Helper function to wrap a line string projected in web mercator.
*
* @param multiLineString The input to wrap
* @param edges The clipping edges used to wrap the input.
* @param offset The x-offset used to displace the result
*
* @internal
*/
function wrapMultiLineStringHelper(
multiLineString: Vector2[][],
edges: ClipEdge[],
offset: number
): GeoCoordinates[][] | undefined {
for (const clip of edges) {
multiLineString = clip.clipLines(multiLineString);
}

const worldP = new Vector3();

const coordinates: GeoCoordinates[][] = [];

multiLineString.forEach(lineString => {
if (lineString.length === 0) {
return;
}

const coords = lineString.map(({ x, y }) => {
worldP.set(x, y, 0);
const geoPoint = webMercatorProjection.unprojectPoint(worldP);
geoPoint.longitude += offset;
return geoPoint;
});

coordinates.push(coords);
});

return coordinates.length > 0 ? coordinates : undefined;
}

/**
* Wrap the given line string.
*
* @remarks
* This function splits this input line string in three parts.
*
* The `left` member of the result contains the part of the line string with longitude less than `-180`.
*
* The `middle` member contains the part of the line string with longitude in the range `[-180, 180]`.
*
* The `right` member contains the part of the line string with longitude greater than `180`.
*
* @param coordinates The coordinates of the line string to wrap.
*/
export function wrapLineString(coordinates: GeoCoordinates[]): Partial<WrappedLineString> {
const worldP = new Vector3();

const lineString = coordinates.map(g => {
const { x, y } = webMercatorProjection.projectPoint(g, worldP);
return new Vector2(x, y);
});

const multiLineString = [lineString];

return {
left: wrapMultiLineStringHelper(multiLineString, WRAP_LEFT_CLIP_EDGES, 360),
middle: wrapMultiLineStringHelper(multiLineString, WRAP_MIDDLE_CLIP_EDGES, 0),
right: wrapMultiLineStringHelper(multiLineString, WRAP_RIGHT_CLIP_EDGES, -360)
};
}

const ec = EarthConstants.EQUATORIAL_CIRCUMFERENCE;
const border = 0;

const WRAP_MIDDLE_CLIP_EDGES = [
new ClipEdge(0 - border, ec, 0 - border, 0, p => p.x > 0 - border),
new ClipEdge(ec + border, 0, ec + border, ec, p => p.x < ec + border)
];

const WRAP_LEFT_CLIP_EDGES = [
new ClipEdge(-ec - border, ec, -ec - border, 0, p => p.x > -ec - border),
new ClipEdge(0 + border, 0, 0 + border, ec, p => p.x < 0 + border)
];

const WRAP_RIGHT_CLIP_EDGES = [
new ClipEdge(ec - border, ec, ec - border, 0, p => p.x > ec - border),
new ClipEdge(ec * 2 + border, 0, ec * 2 + border, ec, p => p.x < ec * 2 + border)
];
7 changes: 7 additions & 0 deletions @here/harp-geometry/lib/WrapLineString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright (C) 2017-2020 HERE Europe B.V.
* Licensed under Apache 2.0, see full license in LICENSE
* SPDX-License-Identifier: Apache-2.0
*/

export { wrapLineString } from "./ClipLineString";
158 changes: 158 additions & 0 deletions test/rendering/GeoJsonDataRendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { Feature, FeatureCollection, StyleSet } from "@here/harp-datasource-protocol";
import { clipLineString } from "@here/harp-geometry/lib/ClipLineString";
import { wrapLineString } from "@here/harp-geometry/lib/WrapLineString";
import { wrapPolygon } from "@here/harp-geometry/lib/WrapPolygon";
import {
EarthConstants,
Expand Down Expand Up @@ -818,6 +819,163 @@ describe("MapView + OmvDataSource + GeoJsonDataProvider rendering test", functio
});
}
});

describe("wrap linestring crossing antimeridian", async function() {
const ourStyle: StyleSet = [
{
when: ["all", ["==", ["geometry-type"], "Polygon"], ["!has", "fragment"]],
technique: "solid-line",
color: "rgba(255,0,0,1)",
lineWidth: "2px"
},
{
when: [
"all",
["==", ["geometry-type"], "LineString"],
["==", ["get", "fragment"], "left"]
],
technique: "solid-line",
color: "rgba(255,0,0,1)",
lineWidth: "2px",
caps: "Round",
renderOrder: 1
},
{
when: [
"all",
["==", ["geometry-type"], "LineString"],
["==", ["get", "fragment"], "middle"]
],
technique: "solid-line",
color: "rgba(0,255,0,1)",
lineWidth: "2px",
renderOrder: 0
},
{
when: [
"all",
["==", ["geometry-type"], "LineString"],
["==", ["get", "fragment"], "right"]
],
technique: "solid-line",
color: "rgba(0,0,255,1)",
lineWidth: "2px",
renderOrder: 1
},
{
when: ["all", ["==", ["geometry-type"], "LineString"], ["!has", "fragment"]],
technique: "solid-line",
lineWidth: "2px",
color: "rgb(0,0,0)"
}
];

// Convert the coordinates of the polygon from GeoJson to GeoCoordinates.
const coordinates = polygon_crossing_antimeridian.features[0].geometry.coordinates[0].map(
([lng, lat]) => GeoCoordinates.fromGeoPoint([lng, lat])
);

// Split the polygon in 3 parts.
const wrappedLineString = wrapLineString(coordinates);

// Extracts a part (left, middle or right) from the wrapped polygon
// and convert it to a GeoJson feature.
const toGeoJsonMultiLineString = (part: string): Feature => {
const coordinates: GeoCoordinates[][] | undefined = (wrappedLineString as any)[part];

// Converts an Array of GeoCoordinates to GeoJSON coordinates.
const toGeoJsonCoordinates = (coordinates: GeoCoordinates[][] | undefined) => {
coordinates = coordinates ?? [];
return coordinates.map(line => {
return line.map(({ lat, lng }) => [lng, lat]);
});
};

return {
type: "Feature",
properties: {
fragment: part
},
geometry: {
type: "MultiLineString",
coordinates: toGeoJsonCoordinates(coordinates)
}
};
};

const separator: Feature = {
type: "Feature",
properties: {},
geometry: {
type: "LineString",
coordinates: [
[180, 90],
[180, -90]
]
}
};

const lookAt: Partial<LookAtParams> = {
target: [-160, 40],
distance: EarthConstants.EQUATORIAL_CIRCUMFERENCE * 2.5
};

it(`linestring to wrap`, async function() {
this.timeout(5000);

await geoJsonTest.run({
lookAt,
mochaTest: this,
testImageName: `geojson-wrap-linestring-crossing-antimeridian`,
theme: { lights, styles: { geojson: ourStyle } },
geoJson: polygon_crossing_antimeridian as any
});
});

it(`wrapped linestring - merged`, async function() {
this.timeout(5000);

await geoJsonTest.run({
lookAt,
mochaTest: this,
testImageName: `geojson-wrap-linestring-crossing-antimeridian-merged`,
theme: { lights, styles: { geojson: ourStyle } },
geoJson: {
type: "FeatureCollection",
features: [
toGeoJsonMultiLineString("left"),
toGeoJsonMultiLineString("middle"),
toGeoJsonMultiLineString("right"),
separator
]
}
});
});

for (const part in wrappedLineString) {
if (!wrappedLineString.hasOwnProperty(part)) {
continue;
}

const feature = toGeoJsonMultiLineString(part);

it(`wrapped linestring - ${part}`, async function() {
this.timeout(5000);

await geoJsonTest.run({
lookAt,
mochaTest: this,
testImageName: `geojson-wrap-linestring-crossing-antimeridian-${part}`,
theme: { lights, styles: { geojson: ourStyle } },
geoJson: {
type: "FeatureCollection",
features: [feature, separator]
}
});
});
}
});

describe("clip lines against bounds", async function() {
it("clip long line string", async function() {
const bounds: GeoPointLike[] = [
Expand Down

0 comments on commit b72683d

Please sign in to comment.