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

Commit

Permalink
HARP-10772: Support pick result count limit.
Browse files Browse the repository at this point in the history
Add a parameters object to intersectMapObjects to allow for picking behaviour
customization. So far, only the pick maximum number of results can be set.
If set, this limit might be taken into account to accelerate picking by skipping
unnecessary intersection tests.

Signed-off-by: Andres Mandado <andres.mandado-almajano@here.com>
  • Loading branch information
atomicsulfate committed Jul 16, 2020
1 parent fbb8ba3 commit 0114c81
Show file tree
Hide file tree
Showing 6 changed files with 539 additions and 100 deletions.
17 changes: 17 additions & 0 deletions @here/harp-mapview/lib/IntersectParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright (C) 2017-2020 HERE Europe B.V.
* Licensed under Apache 2.0, see full license in LICENSE
* SPDX-License-Identifier: Apache-2.0
*/

/**
* Parameters to customize behaviour of {@link (MapView.intersectMapObjects)}.
*/
export interface IntersectParams {
/**
* The maximum number of results to be retrieved from the intersection test. If set, only the
* first maxResultCount results will be returned, following an order by distance first, then
* by reversed render order (topmost/highest render order first).
*/
maxResultCount?: number;
}
7 changes: 5 additions & 2 deletions @here/harp-mapview/lib/MapView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import { FrustumIntersection } from "./FrustumIntersection";
import { overlayOnElevation } from "./geometry/overlayOnElevation";
import { TileGeometryManager } from "./geometry/TileGeometryManager";
import { MapViewImageCache } from "./image/MapViewImageCache";
import { IntersectParams } from "./IntersectParams";
import { MapAnchors } from "./MapAnchors";
import { MapObjectAdapter } from "./MapObjectAdapter";
import { MapViewFog } from "./MapViewFog";
Expand Down Expand Up @@ -2677,10 +2678,12 @@ export class MapView extends THREE.EventDispatcher {
*
* @param x - The X position in css/client coordinates (without applied display ratio).
* @param y - The Y position in css/client coordinates (without applied display ratio).
* @param parameters - The intersection test behaviour may be adjusted by providing an instance
* of {@link IntersectParams}.
* @returns The list of intersection results.
*/
intersectMapObjects(x: number, y: number): PickResult[] {
return this.m_pickHandler.intersectMapObjects(x, y);
intersectMapObjects(x: number, y: number, parameters?: IntersectParams): PickResult[] {
return this.m_pickHandler.intersectMapObjects(x, y, parameters);
}

/**
Expand Down
165 changes: 100 additions & 65 deletions @here/harp-mapview/lib/PickHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import { GeometryType, getFeatureId, Technique } from "@here/harp-datasource-pro
import * as THREE from "three";

import { OrientedBox3 } from "@here/harp-geoutils";
import { IntersectParams } from "./IntersectParams";
import { MapView } from "./MapView";
import { MapViewPoints } from "./MapViewPoints";
import { TileFeatureData } from "./Tile";
import { PickListener } from "./PickListener";
import { Tile, TileFeatureData } from "./Tile";

/**
* Describes the general type of a picked object.
Expand Down Expand Up @@ -73,6 +75,11 @@ export interface PickResult {
*/
distance: number;

/**
* Render order of the intersected object.
*/
renderOrder?: number;

/**
* An optional feature ID of the picked object; typically applies to the Optimized Map
* Vector (OMV) format.
Expand Down Expand Up @@ -121,22 +128,103 @@ export class PickHandler {
*
* @param x - The X position in CSS/client coordinates, without the applied display ratio.
* @param y - The Y position in CSS/client coordinates, without the applied display ratio.
* @param parameters - The intersection test behaviour may be adjusted by providing an instance
* of {@link IntersectParams}.
* @returns the list of intersection results.
*/
intersectMapObjects(x: number, y: number): PickResult[] {
intersectMapObjects(x: number, y: number, parameters?: IntersectParams): PickResult[] {
const worldPos = this.mapView.getNormalizedScreenCoordinates(x, y);
const rayCaster = this.mapView.raycasterFromScreenPoint(x, y);
const pickResults: PickResult[] = [];

const pickListener = new PickListener(parameters);

if (this.mapView.textElementsRenderer !== undefined) {
const { clientWidth, clientHeight } = this.mapView.canvas;
const screenX = worldPos.x * clientWidth * 0.5;
const screenY = worldPos.y * clientHeight * 0.5;
const scenePosition = new THREE.Vector2(screenX, screenY);
this.mapView.textElementsRenderer.pickTextElements(scenePosition, pickResults);
this.mapView.textElementsRenderer.pickTextElements(scenePosition, pickListener);
}

const intersects: THREE.Intersection[] = [];
const intersectedTiles = this.getIntersectedTiles(rayCaster);

for (const { tile, distance } of intersectedTiles) {
if (pickListener.done && pickListener.furthestResult!.distance < distance) {
// Stop when the listener has all results it needs and remaining tiles are further
// away than then furthest pick result found so far.
break;
}
intersects.length = 0;
rayCaster.intersectObjects(tile.objects, true, intersects);
for (const intersect of intersects) {
pickListener.addResult(this.createResult(intersect));
}
}

return pickListener.results;
}

private createResult(intersection: THREE.Intersection): PickResult {
const pickResult: PickResult = {
type: PickObjectType.Unspecified,
point: intersection.point,
distance: intersection.distance,
intersection
};

if (
intersection.object.userData === undefined ||
intersection.object.userData.feature === undefined
) {
return pickResult;
}

if (this.enablePickTechnique) {
pickResult.technique = intersection.object.userData.technique;
}
pickResult.renderOrder = intersection.object?.renderOrder;

const featureData: TileFeatureData = intersection.object.userData.feature;
this.addObjInfo(featureData, intersection, pickResult);
if (pickResult.userData) {
const featureId = getFeatureId(pickResult.userData);
pickResult.featureId = featureId === 0 ? undefined : featureId;
}

let pickObjectType: PickObjectType;

switch (featureData.geometryType) {
case GeometryType.Point:
case GeometryType.Text:
pickObjectType = PickObjectType.Point;
break;
case GeometryType.Line:
case GeometryType.ExtrudedLine:
case GeometryType.SolidLine:
case GeometryType.TextPath:
pickObjectType = PickObjectType.Line;
break;
case GeometryType.Polygon:
case GeometryType.ExtrudedPolygon:
pickObjectType = PickObjectType.Area;
break;
case GeometryType.Object3D:
pickObjectType = PickObjectType.Object3D;
break;
default:
pickObjectType = PickObjectType.Unspecified;
}

pickResult.type = pickObjectType;
return pickResult;
}

private getIntersectedTiles(
rayCaster: THREE.Raycaster
): Array<{ tile: Tile; distance: number }> {
const tiles = new Array<{ tile: Tile; distance: number }>();

const tileList = this.mapView.visibleTileSet.dataSourceTileList;
tileList.forEach(dataSourceTileList => {
dataSourceTileList.renderedTiles.forEach(tile => {
Expand All @@ -146,72 +234,19 @@ export class PickHandler {
// MapView
const worldOffsetX = tile.computeWorldOffsetX();
tmpOBB.position.x += worldOffsetX;

if (tmpOBB.intersectsRay(rayCaster.ray) !== undefined) {
rayCaster.intersectObjects(tile.objects, true, intersects);
const distance = tmpOBB.intersectsRay(rayCaster.ray);
if (distance !== undefined) {
tiles.push({ tile, distance });
}
});
});

for (const intersect of intersects) {
const pickResult: PickResult = {
type: PickObjectType.Unspecified,
point: intersect.point,
distance: intersect.distance,
intersection: intersect
};

if (
intersect.object.userData === undefined ||
intersect.object.userData.feature === undefined
) {
pickResults.push(pickResult);
continue;
tiles.sort(
(lhs: { tile: Tile; distance: number }, rhs: { tile: Tile; distance: number }) => {
return lhs.distance - rhs.distance;
}

const featureData: TileFeatureData = intersect.object.userData.feature;
if (this.enablePickTechnique) {
pickResult.technique = intersect.object.userData.technique;
}

this.addObjInfo(featureData, intersect, pickResult);
if (pickResult.userData) {
pickResult.featureId = getFeatureId(pickResult.userData);
}

let pickObjectType: PickObjectType;

switch (featureData.geometryType) {
case GeometryType.Point:
case GeometryType.Text:
pickObjectType = PickObjectType.Point;
break;
case GeometryType.Line:
case GeometryType.ExtrudedLine:
case GeometryType.SolidLine:
case GeometryType.TextPath:
pickObjectType = PickObjectType.Line;
break;
case GeometryType.Polygon:
case GeometryType.ExtrudedPolygon:
pickObjectType = PickObjectType.Area;
break;
case GeometryType.Object3D:
pickObjectType = PickObjectType.Object3D;
break;
default:
pickObjectType = PickObjectType.Unspecified;
}

pickResult.type = pickObjectType;
pickResults.push(pickResult);
}

pickResults.sort((a: PickResult, b: PickResult) => {
return a.distance - b.distance;
});

return pickResults;
);
return tiles;
}

private addObjInfo(
Expand Down
127 changes: 127 additions & 0 deletions @here/harp-mapview/lib/PickListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* 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 { IntersectParams } from "./IntersectParams";
import { PickResult } from "./PickHandler";

// Default sorting by distance first and then by reversed render order.
function defaultSort(lhs: PickResult, rhs: PickResult) {
const distanceDiff = lhs.distance - rhs.distance;
const haveRenderOrder = lhs.renderOrder !== undefined && rhs.renderOrder !== undefined;
if (distanceDiff !== 0 || !haveRenderOrder) {
return distanceDiff;
}

return rhs.renderOrder! - lhs.renderOrder!;
}

/**
* Collects results from a picking (intersection) test.
*
* @internal
*/
export class PickListener {
private m_results: PickResult[] = [];
private m_sorted: boolean = true;

/**
* Constructs a new `PickListener`.
*
* @param m_parameters - Optional parameters to customize picking behaviour.
*/
constructor(private readonly m_parameters?: IntersectParams) {}

/**
* Adds a pick result.
*
* @param result - The result to be added.
*/
addResult(result: PickResult): void {
// Add the result only if it's a different feature from the ones already collected.
const foundFeatureIdx = this.m_results.findIndex(otherResult => {
const sameType = otherResult.type === result.type;
const dataSource = result.intersection?.object.userData?.dataSource;
const sameDataSource =
dataSource && otherResult.intersection?.object.userData?.dataSource === dataSource;
const sameId =
result.featureId !== undefined && otherResult.featureId === result.featureId;
const noId = result.featureId === undefined && otherResult.featureId === undefined;
const sameUserData = result.userData && otherResult.userData === result.userData;
return sameType && sameDataSource && (sameId || (noId && sameUserData));
});

if (foundFeatureIdx < 0) {
this.m_sorted = false;
this.m_results.push(result);
return;
}

// Replace the result for the same feature if it's sorted after the new result.
const oldResult = this.m_results[foundFeatureIdx];
if (defaultSort(result, oldResult) < 0) {
this.m_results[foundFeatureIdx] = result;
this.m_sorted = false;
}
}

/**
* Indicates whether the listener is satisfied with the results already provided.
* @returns `True` if the listener doesn't expect more results, `False` otherwise.
*/
get done(): boolean {
return this.maxResults ? this.m_results.length >= this.maxResults : false;
}

/**
* Returns the collected results, ordered by distance first, then by reversed render order
* (topmost/highest render order first).
* @returns The pick results.
*/
get results(): PickResult[] {
this.finish();
return this.m_results;
}

/**
* Returns the closest result collected so far, following the order documented in
* {@link PickListener.results}
* @returns The closest pick result, or `undefined` if no result was collected.
*/
get closestResult(): PickResult | undefined {
this.sortResults();
return this.m_results.length > 0 ? this.m_results[0] : undefined;
}

/**
* Returns the furtherst result collected so far, following the order documented in
* {@link PickListener.results}
* @returns The furthest pick result, or `undefined` if no result was collected.
*/
get furthestResult(): PickResult | undefined {
this.sortResults();
return this.m_results.length > 0 ? this.m_results[this.m_results.length - 1] : undefined;
}

private get maxResults(): number | undefined {
const maxCount = this.m_parameters?.maxResultCount ?? 0;
return maxCount > 0 ? maxCount : undefined;
}

private sortResults(): void {
if (!this.m_sorted) {
this.m_results.sort(defaultSort);
this.m_sorted = true;
}
}

private finish(): void {
// Keep only the closest max results.
this.sortResults();
if (this.maxResults && this.m_results.length > this.maxResults) {
this.m_results.length = this.maxResults;
}
}
}
Loading

0 comments on commit 0114c81

Please sign in to comment.