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

Commit

Permalink
HARP-12403: Add support for clipping lines against the tile bounds.
Browse files Browse the repository at this point in the history
This change introduces the function clipLineString that can be used
to clip lines against the tile bounding box.

Signed-off-by: Roberto Raggi <roberto.raggi@here.com>
  • Loading branch information
robertoraggi committed Nov 2, 2020
1 parent fe17315 commit 52ae1de
Show file tree
Hide file tree
Showing 3 changed files with 327 additions and 9 deletions.
155 changes: 155 additions & 0 deletions @here/harp-geometry/lib/ClipLineString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Copyright (C) 2017-2020 HERE Europe B.V.
* Licensed under Apache 2.0, see full license in LICENSE
* SPDX-License-Identifier: Apache-2.0
*/

import { Math2D } from "@here/harp-utils";
import { Vector2 } from "three";

/**
* A clipping edge.
*
* @remarks
* Clip lines using the Sutherland-Hodgman algorithm.
*
* @internal
*/
class ClipEdge {
readonly p0: Vector2;
readonly p1: Vector2;

/**
* Creates a clipping edge.
*
* @param x1 - The x coordinate of the first point of this ClipEdge.
* @param y1 - The y coordinate of the first point of this ClipEdge.
* @param x2 - The x coordinate of the second point of this ClipEdge.
* @param y2 - The y coordinate of the second point of this ClipEdge.
* @param isInside - The function used to test points against this ClipEdge.
*/
constructor(
x1: number,
y1: number,
x2: number,
y2: number,
private readonly isInside: (p: Vector2) => boolean
) {
this.p0 = new Vector2(x1, y1);
this.p1 = new Vector2(x2, y2);
}

/**
* Tests if the given point is inside this clipping edge.
*/
inside(point: Vector2): boolean {
return this.isInside(point);
}

/**
* Computes the intersection of a line and this clipping edge.
*
* @remarks
* {@link https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
* | line-line intersection}.
*/
computeIntersection(a: Vector2, b: Vector2): Vector2 {
const result = new Vector2();
Math2D.intersectLines(
a.x,
a.y,
b.x,
b.y,
this.p0.x,
this.p0.y,
this.p1.x,
this.p1.y,
result
);
return result;
}

/**
* Clip the input line against this edge.
*/
clipLine(lineString: Vector2[]): Vector2[][] {
const inputList = lineString;

const result: Vector2[][] = [];

lineString = [];
result.push(lineString);

const pushPoint = (point: Vector2) => {
if (lineString.length === 0 || !lineString[lineString.length - 1].equals(point)) {
lineString.push(point);
}
};

for (let i = 0; i < inputList.length; ++i) {
const currentPoint = inputList[i];
const prevPoint = i > 0 ? inputList[i - 1] : undefined;

if (this.inside(currentPoint)) {
if (prevPoint !== undefined && !this.inside(prevPoint)) {
if (lineString.length > 0) {
lineString = [];
result.push(lineString);
}
pushPoint(this.computeIntersection(prevPoint, currentPoint));
}
pushPoint(currentPoint);
} else if (prevPoint !== undefined && this.inside(prevPoint)) {
pushPoint(this.computeIntersection(prevPoint, currentPoint));
}
}

if (result[result.length - 1].length === 0) {
result.length = result.length - 1;
}

return result;
}

/**
* Clip the input lines against this edge.
*/
clipLines(lineStrings: Vector2[][]) {
const reuslt: Vector2[][] = [];
lineStrings.forEach(lineString => {
this.clipLine(lineString).forEach(clippedLine => {
reuslt.push(clippedLine);
});
});
return reuslt;
}
}

/**
* Clip the input line against the given bounds.
*
* @param lineString - The line to clip.
* @param minX - The minimum x coordinate.
* @param minY - The minimum y coordinate.
* @param maxX - The maxumum x coordinate.
* @param maxY - The maxumum y coordinate.
*/
export function clipLineString(
lineString: Vector2[],
minX: number,
minY: number,
maxX: number,
maxY: number
): Vector2[][] {
const clipEdge0 = new ClipEdge(minX, minY, minX, maxY, p => p.x > minX); // left
const clipEdge1 = new ClipEdge(minX, maxY, maxX, maxY, p => p.y < maxY); // bottom
const clipEdge2 = new ClipEdge(maxX, maxY, maxX, minY, p => p.x < maxX); // right
const clipEdge3 = new ClipEdge(maxX, minY, minX, minY, p => p.y > minY); // top

let lines = clipEdge0.clipLine(lineString);
lines = clipEdge1.clipLines(lines);
lines = clipEdge2.clipLines(lines);
lines = clipEdge3.clipLines(lines);

return lines;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import { MapEnv, ValueMap } from "@here/harp-datasource-protocol/lib/Env";
import { clipLineString } from "@here/harp-geometry/lib/ClipLineString";
import { GeoCoordinates, GeoPointLike, webMercatorProjection } from "@here/harp-geoutils";
import { ILogger } from "@here/harp-utils";
import { Vector2, Vector3 } from "three";
Expand Down Expand Up @@ -199,14 +200,36 @@ export class GeoJsonDataAdapter implements DataAdapter {
switch (feature.geometry.type) {
case "LineString":
case "MultiLineString": {
const geometry = convertLineGeometry(feature.geometry, decodeInfo);
this.m_processor.processLineFeature(
$layer,
DEFAULT_EXTENTS,
geometry,
env,
$level
);
let geometry = convertLineGeometry(feature.geometry, decodeInfo);

const clippedGeometries: ILineGeometry[] = [];

const DEFAULT_BORDER = 100;

geometry.forEach(g => {
const clipped = clipLineString(
g.positions,
-DEFAULT_BORDER,
-DEFAULT_BORDER,
DEFAULT_EXTENTS + DEFAULT_BORDER,
DEFAULT_EXTENTS + DEFAULT_BORDER
);
clipped.forEach(positions => {
clippedGeometries.push({ positions });
});
});

geometry = clippedGeometries;

if (geometry.length > 0) {
this.m_processor.processLineFeature(
$layer,
DEFAULT_EXTENTS,
clippedGeometries,
env,
$level
);
}
break;
}
case "Polygon":
Expand Down
142 changes: 141 additions & 1 deletion test/rendering/GeoJsonDataRendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,22 @@ import {
StyleSet,
Theme
} from "@here/harp-datasource-protocol";
import { clipLineString } from "@here/harp-geometry/lib/ClipLineString";
import { wrapPolygon } from "@here/harp-geometry/lib/WrapPolygon";
import { EarthConstants, GeoCoordinates, webMercatorTilingScheme } from "@here/harp-geoutils";
import {
EarthConstants,
GeoBox,
GeoCoordinates,
GeoPointLike,
webMercatorProjection,
webMercatorTilingScheme
} from "@here/harp-geoutils";
import { LookAtParams, MapView, MapViewEventNames } from "@here/harp-mapview";
import { GeoJsonTiler } from "@here/harp-mapview-decoder/index-worker";
import { RenderingTestHelper, waitForEvent } from "@here/harp-test-utils";
import { GeoJsonDataProvider, VectorTileDataSource } from "@here/harp-vectortile-datasource";
import { VectorTileDecoder } from "@here/harp-vectortile-datasource/lib/VectorTileDecoder";
import { Vector2, Vector3 } from "three";

import * as polygon_crossing_antimeridian from "../resources/polygon_crossing_antimeridian.json";

Expand Down Expand Up @@ -738,4 +747,135 @@ describe("MapView + OmvDataSource + GeoJsonDataProvider rendering test", functio
});
}
});
describe("clip lines against bounds", async function() {
it("clip long line string", async function() {
const bounds: GeoPointLike[] = [
[-2.8125, -15.28418511407642],
[76.9921875, -15.28418511407642],
[76.9921875, 54.77534585936447],
[-2.8125, 54.77534585936447],
[-2.8125, -15.28418511407642]
];

const geoBox = new GeoBox(
GeoCoordinates.fromGeoPoint(bounds[0]),
GeoCoordinates.fromGeoPoint(bounds[0])
);

bounds.forEach(geoPoint => {
geoBox.growToContain(GeoCoordinates.fromGeoPoint(geoPoint));
});

const { west, south, east, north } = geoBox;

const { min, max } = webMercatorTilingScheme.projection.projectBox(geoBox);

const inputLineString: GeoPointLike[] = [
[-28.125, 33.43144133557529],
[-24.609375, 49.38237278700955],
[-9.140625, 39.36827914916014],
[21.4453125, 44.08758502824516],
[25.6640625, 61.60639637138628],
[45, 45.82879925192134],
[56.6015625, 62.2679226294176],
[61.87499999999999, 43.32517767999296],
[88.24218749999999, 39.639537564366684],
[84.72656249999999, 14.26438308756265],
[53.78906249999999, 13.923403897723347],
[33.3984375, 27.994401411046148],
[52.734375, 35.17380831799959],
[58.71093750000001, 28.613459424004414],
[61.52343749999999, 35.460669951495305],
[38.67187499999999, 39.639537564366684],
[16.875, 32.24997445586331],
[12.65625, 14.604847155053898],
[-14.0625, -4.214943141390639],
[41.1328125, -26.11598592533351]
];

const points = inputLineString.map(geoPos => {
const { x, y } = webMercatorProjection.projectPoint(
GeoCoordinates.fromGeoPoint(geoPos)
);
return new Vector2(x, y);
});

const lineStrings = clipLineString(points, min.x, min.y, max.x, max.y);

const coordinates = lineStrings.map(lineString =>
lineString.map(({ x, y }) =>
webMercatorProjection.unprojectPoint(new Vector3(x, y, 0)).toGeoPoint()
)
);

const geoJson = {
type: "FeatureCollection",
features: [
{
type: "Feature",
properties: {},
geometry: {
type: "Polygon",
coordinates: [
[
[west, south],
[east, south],
[east, north],
[west, north],
[west, south]
]
]
}
},
{
type: "Feature",
properties: {
color: "#00ff00"
},
geometry: {
type: "LineString",
coordinates: inputLineString
}
},
{
type: "Feature",
properties: {
color: "#ff0000"
},
geometry: {
type: "MultiLineString",
coordinates
}
}
]
};

const ourStyle: StyleSet = [
{
when: ["==", ["geometry-type"], "Polygon"],
technique: "fill",
color: "rgba(100,100,100,0.5)",
lineWidth: 2
},
{
when: ["==", ["geometry-type"], "LineString"],
technique: "solid-line",
metricUnit: "Pixel",
color: ["get", "color"],
lineWidth: 2
}
];

await geoJsonTest({
mochaTest: this,
testImageName: "geojson-clip-line-against-tile-border",
theme: { lights, styles: { geojson: ourStyle } },
geoJson: geoJson as any,
lookAt: {
zoomLevel: 2,
target: geoBox.center
}
});
});
});
});

0 comments on commit 52ae1de

Please sign in to comment.