diff --git a/@here/harp-mapview/lib/MapObjectAdapter.ts b/@here/harp-mapview/lib/MapObjectAdapter.ts index 8136542444..0cb73c6cf8 100644 --- a/@here/harp-mapview/lib/MapObjectAdapter.ts +++ b/@here/harp-mapview/lib/MapObjectAdapter.ts @@ -3,7 +3,7 @@ * Licensed under Apache 2.0, see full license in LICENSE * SPDX-License-Identifier: Apache-2.0 */ -import { GeometryKind, MapEnv, Pickability, Technique } from "@here/harp-datasource-protocol"; +import { GeometryKind, Pickability, Technique } from "@here/harp-datasource-protocol"; import * as THREE from "three"; import { DataSource } from "./DataSource"; @@ -135,9 +135,8 @@ export class MapObjectAdapter { /** * Whether underlying `THREE.Object3D` should be pickable by {@link PickHandler}. - * @param env - Property lookup environment. */ - isPickable(env: MapEnv) { + isPickable() { // An object is pickable only if it's visible and Pickabilty.onlyVisible or // Pickabililty.all set. return ( diff --git a/@here/harp-mapview/lib/MapView.ts b/@here/harp-mapview/lib/MapView.ts index b118d54697..695e2e25f1 100644 --- a/@here/harp-mapview/lib/MapView.ts +++ b/@here/harp-mapview/lib/MapView.ts @@ -64,7 +64,6 @@ import { MapViewFog } from "./MapViewFog"; import { MapViewTaskScheduler } from "./MapViewTaskScheduler"; import { MapViewThemeManager } from "./MapViewThemeManager"; import { PickHandler, PickResult } from "./PickHandler"; -import { PickingRaycaster } from "./PickingRaycaster"; import { PoiManager } from "./poi/PoiManager"; import { PoiTableManager } from "./poi/PoiTableManager"; import { PolarTileDataSource } from "./PolarTileDataSource"; @@ -869,7 +868,7 @@ export class MapView extends EventDispatcher { private readonly m_enablePolarDataSource: boolean = true; // gestures - private readonly m_raycaster: PickingRaycaster; + private readonly m_raycaster = new THREE.Raycaster(); private readonly m_plane = new THREE.Plane(new THREE.Vector3(0, 0, 1)); private readonly m_sphere = new THREE.Sphere(undefined, EarthConstants.EQUATORIAL_RADIUS); @@ -1007,11 +1006,6 @@ export class MapView extends EventDispatcher { this.m_politicalView = this.m_options.politicalView; this.handleRequestAnimationFrame = this.renderLoop.bind(this); - this.m_pickHandler = new PickHandler( - this, - this.m_rteCamera, - this.m_options.enablePickTechnique === true - ); if (this.m_options.tileWrappingEnabled !== undefined) { this.m_tileWrappingEnabled = this.m_options.tileWrappingEnabled; @@ -1087,7 +1081,11 @@ export class MapView extends EventDispatcher { // setup camera with initial position this.setupCamera(); - this.m_raycaster = new PickingRaycaster(width, height, this.m_env); + this.m_pickHandler = new PickHandler( + this, + this.m_rteCamera, + this.m_options.enablePickTechnique === true + ); this.m_movementDetector = new CameraMovementDetector( this.m_options.movementThrottleTimeout, @@ -2569,22 +2567,6 @@ export class MapView extends EventDispatcher { return p; } - /** - * Returns a ray caster using the supplied screen positions. - * - * @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). - * - * @alpha - * - * @return Raycaster with origin at the camera and direction based on the supplied x / y screen - * points. - */ - raycasterFromScreenPoint(x: number, y: number): THREE.Raycaster { - this.m_raycaster.setFromCamera(this.getNormalizedScreenCoordinates(x, y), this.m_rteCamera); - return this.m_raycaster; - } - getWorldPositionAt(x: number, y: number, fallback: true): THREE.Vector3; getWorldPositionAt(x: number, y: number, fallback?: boolean): THREE.Vector3 | null; diff --git a/@here/harp-mapview/lib/MapViewPoints.ts b/@here/harp-mapview/lib/MapViewPoints.ts index 011f7fd2a9..eead49d435 100644 --- a/@here/harp-mapview/lib/MapViewPoints.ts +++ b/@here/harp-mapview/lib/MapViewPoints.ts @@ -68,14 +68,25 @@ export abstract class MapViewPoints extends THREE.Points { const geometry = this.geometry; const matrixWorld = this.matrixWorld; - const screenCoords = raycaster.ray.origin + const ndc = raycaster.ray.origin .clone() .add(raycaster.ray.direction) .project(raycaster.camera); - const mouseCoords = new THREE.Vector2( - Math.ceil(((screenCoords.x + 1) / 2) * raycaster.width), - Math.ceil(((1 - screenCoords.y) / 2) * raycaster.height) - ); + const mouseCoords = ndcToScreen(ndc, raycaster); + + const testPoint = (point: THREE.Vector3, index: number) => { + const pointInfo = getPointInfo(point, matrixWorld, raycaster); + if (pointInfo.pointIsOnScreen) { + this.testPoint( + point, + pointInfo.absoluteScreenPosition!, + mouseCoords, + index, + pointInfo.distance!, + intersects + ); + } + }; if (geometry instanceof THREE.BufferGeometry) { const point = new THREE.Vector3(); @@ -85,56 +96,29 @@ export abstract class MapViewPoints extends THREE.Points { if (index !== null) { const indices = index.array; for (let i = 0, il = indices.length; i < il; i++) { - const a = indices[i]; - point.fromArray(positions as number[], a * 3); - const pointInfo = getPointInfo(point, matrixWorld, raycaster); - if (pointInfo.pointIsOnScreen) { - this.testPoint( - point, - pointInfo.absoluteScreenPosition!, - mouseCoords, - i, - pointInfo.distance!, - intersects - ); - } + testPoint(point.fromArray(positions as number[], indices[i] * 3), i); } } else { for (let i = 0, l = positions.length / 3; i < l; i++) { - point.fromArray(positions as number[], i * 3); - const pointInfo = getPointInfo(point, matrixWorld, raycaster); - if (pointInfo.pointIsOnScreen) { - this.testPoint( - point, - pointInfo.absoluteScreenPosition!, - mouseCoords, - i, - pointInfo.distance!, - intersects - ); - } + testPoint(point.fromArray(positions as number[], i * 3), i); } } } else { const vertices = geometry.vertices; for (let index = 0; index < vertices.length; index++) { - const point = vertices[index]; - const pointInfo = getPointInfo(point, matrixWorld, raycaster); - if (pointInfo.pointIsOnScreen) { - this.testPoint( - point, - pointInfo.absoluteScreenPosition!, - mouseCoords, - index, - pointInfo.distance!, - intersects - ); - } + testPoint(vertices[index], index); } } } } +function ndcToScreen(ndc: THREE.Vector3, raycaster: PickingRaycaster): THREE.Vector2 { + return new THREE.Vector2(ndc.x + 1, 1 - ndc.y) + .divideScalar(2) + .multiply(raycaster.canvasSize) + .ceil(); +} + function getPointInfo( point: THREE.Vector3, matrixWorld: THREE.Matrix4, @@ -144,20 +128,12 @@ function getPointInfo( absoluteScreenPosition?: THREE.Vector2; distance?: number; } { - const worldPosition = point.clone(); - worldPosition.applyMatrix4(matrixWorld); + const worldPosition = point.clone().applyMatrix4(matrixWorld); const distance = worldPosition.distanceTo(raycaster.ray.origin); - worldPosition.project(raycaster.camera); - const relativeScreenPosition = new THREE.Vector2(worldPosition.x, worldPosition.y); - const pointIsOnScreen = - relativeScreenPosition.x < 1 && - relativeScreenPosition.x > -1 && - relativeScreenPosition.y < 1 && - relativeScreenPosition.y > -1; + const ndc = worldPosition.project(raycaster.camera); + const pointIsOnScreen = ndc.x < 1 && ndc.x > -1 && ndc.y < 1 && ndc.y > -1; if (pointIsOnScreen) { - worldPosition.x = ((worldPosition.x + 1) / 2) * raycaster.width; - worldPosition.y = ((1 - worldPosition.y) / 2) * raycaster.height; - const absoluteScreenPosition = new THREE.Vector2(worldPosition.x, worldPosition.y); + const absoluteScreenPosition = ndcToScreen(ndc, raycaster); return { absoluteScreenPosition, pointIsOnScreen, diff --git a/@here/harp-mapview/lib/PickHandler.ts b/@here/harp-mapview/lib/PickHandler.ts index c3f135b000..cdf2d28b03 100644 --- a/@here/harp-mapview/lib/PickHandler.ts +++ b/@here/harp-mapview/lib/PickHandler.ts @@ -11,8 +11,10 @@ import * as THREE from "three"; import { IntersectParams } from "./IntersectParams"; import { MapView } from "./MapView"; import { MapViewPoints } from "./MapViewPoints"; +import { PickingRaycaster } from "./PickingRaycaster"; import { PickListener } from "./PickListener"; import { Tile, TileFeatureData } from "./Tile"; +import { MapViewUtils } from "./Utils"; /** * Describes the general type of a picked object. @@ -108,6 +110,7 @@ export interface PickResult { userData?: any; } +const tmpV3 = new THREE.Vector3(); const tmpOBB = new OrientedBox3(); // Intersects the dependent tile objects using the supplied raycaster. Note, because multiple @@ -143,16 +146,20 @@ function intersectDependentObjects( * @internal */ export class PickHandler { + private readonly m_pickingRaycaster: PickingRaycaster; + constructor( readonly mapView: MapView, readonly camera: THREE.Camera, public enablePickTechnique = false - ) {} + ) { + this.m_pickingRaycaster = new PickingRaycaster( + mapView.renderer.getSize(new THREE.Vector2()) + ); + } /** - * Does a raycast on all objects in the scene; useful for picking. This function is Limited to - * objects that THREE.js can raycast. However, any solid lines that have their geometry in the - * shader cannot be tested for intersection. + * Does a raycast on all objects in the scene; useful for picking. * * @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. @@ -161,15 +168,14 @@ export class PickHandler { * @returns the list of intersection results. */ intersectMapObjects(x: number, y: number, parameters?: IntersectParams): PickResult[] { - const worldPos = this.mapView.getNormalizedScreenCoordinates(x, y); - const rayCaster = this.mapView.raycasterFromScreenPoint(x, y); - + const ndc = this.mapView.getNormalizedScreenCoordinates(x, y); + const rayCaster = this.setupRaycaster(x, y); 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 screenX = ndc.x * clientWidth * 0.5; + const screenY = ndc.y * clientHeight * 0.5; const scenePosition = new THREE.Vector2(screenX, screenY); this.mapView.textElementsRenderer.pickTextElements(scenePosition, pickListener); } @@ -217,6 +223,25 @@ export class PickHandler { return pickListener.results; } + /** + * Returns a ray caster using the supplied screen positions. + * + * @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). + * + * @return Raycaster with origin at the camera and direction based on the supplied x / y screen + * points. + */ + raycasterFromScreenPoint(x: number, y: number): THREE.Raycaster { + this.m_pickingRaycaster.setFromCamera( + this.mapView.getNormalizedScreenCoordinates(x, y), + this.camera + ); + + this.mapView.renderer.getSize(this.m_pickingRaycaster.canvasSize); + return this.m_pickingRaycaster; + } + private createResult(intersection: THREE.Intersection): PickResult { const pickResult: PickResult = { type: PickObjectType.Unspecified, @@ -350,4 +375,24 @@ export class PickHandler { } pickResult.userData = featureData.objInfos[objInfosIndex - 1]; } + + private setupRaycaster(x: number, y: number): THREE.Raycaster { + const camera = this.mapView.camera; + const rayCaster = this.raycasterFromScreenPoint(x, y); + + // A threshold must be set for picking of line and line segments, indicating the maximum + // distance in world units from the ray to a line to consider it as picked. Use the world + // units equivalent to one pixel at the furthest intersection (i.e. intersection with ground + // or far plane). + const furthestIntersection = this.mapView.getWorldPositionAt(x, y, true); + const furthestDistance = + camera.position.distanceTo(furthestIntersection) / + this.mapView.camera.getWorldDirection(tmpV3).dot(rayCaster.ray.direction); + rayCaster.params.Line!.threshold = MapViewUtils.calculateWorldSizeByFocalLength( + this.mapView.focalLength, + furthestDistance, + 1 + ); + return rayCaster; + } } diff --git a/@here/harp-mapview/lib/PickingRaycaster.ts b/@here/harp-mapview/lib/PickingRaycaster.ts index 88c8360bb7..16d2abd162 100644 --- a/@here/harp-mapview/lib/PickingRaycaster.ts +++ b/@here/harp-mapview/lib/PickingRaycaster.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { MapEnv } from "@here/harp-datasource-protocol"; import * as THREE from "three"; import { MapObjectAdapter } from "./MapObjectAdapter"; @@ -12,20 +11,19 @@ import { MapObjectAdapter } from "./MapObjectAdapter"; function intersectObject( object: THREE.Object3D, raycaster: PickingRaycaster, - env: MapEnv, intersects: THREE.Intersection[], recursive?: boolean ) { if (object.layers.test(raycaster.layers) && object.visible) { const mapObjectAdapter = MapObjectAdapter.get(object); - if (!mapObjectAdapter || mapObjectAdapter.isPickable(env)) { + if (!mapObjectAdapter || mapObjectAdapter.isPickable()) { object.raycast(raycaster, intersects); } } if (recursive === true) { for (const child of object.children) { - intersectObject(child, raycaster, env, intersects, true); + intersectObject(child, raycaster, intersects, true); } } } @@ -41,11 +39,9 @@ export class PickingRaycaster extends THREE.Raycaster { /** * Constructor. * - * @param width - the canvas width. - * @param height - the canvas height. - * @param m_env - the view enviroment. + * @param canvasSize - the canvas width and height. */ - constructor(public width: number, public height: number, private readonly m_env: MapEnv) { + constructor(readonly canvasSize: THREE.Vector2) { super(); } @@ -58,7 +54,7 @@ export class PickingRaycaster extends THREE.Raycaster { ): THREE.Intersection[] { const intersects: THREE.Intersection[] = optionalTarget ?? []; - intersectObject(object, this, this.m_env, intersects, recursive); + intersectObject(object, this, intersects, recursive); return intersects; } @@ -73,7 +69,7 @@ export class PickingRaycaster extends THREE.Raycaster { const intersects: THREE.Intersection[] = optionalTarget ?? []; for (const object of objects) { - intersectObject(object, this, this.m_env, intersects, recursive); + intersectObject(object, this, intersects, recursive); } return intersects; diff --git a/@here/harp-mapview/lib/geometry/TileGeometryCreator.ts b/@here/harp-mapview/lib/geometry/TileGeometryCreator.ts index 8ed965c27d..29fc8a3955 100644 --- a/@here/harp-mapview/lib/geometry/TileGeometryCreator.ts +++ b/@here/harp-mapview/lib/geometry/TileGeometryCreator.ts @@ -802,13 +802,16 @@ export class TileGeometryCreator { setDepthPrePassStencil(depthPassMesh, object as THREE.Mesh); } + const techniquePickability = transientToPickability( + getPropertyValue(technique.transient, mapView.env) + ); // register all objects as pickable except solid lines with outlines, in that case // it's enough to make outlines pickable. registerTileObject(tile, object, techniqueKind, { technique, pickability: hasSolidLinesOutlines ? Pickability.transient - : transientToPickability(getPropertyValue(technique.transient, mapView.env)) + : techniquePickability }); objects.push(object); @@ -975,7 +978,7 @@ export class TileGeometryCreator { registerTileObject(tile, outlineObj, techniqueKind, { technique, - pickability: Pickability.transient + pickability: techniquePickability }); MapMaterialAdapter.create(outlineMaterial, { color: fillTechnique.lineColor, diff --git a/@here/harp-mapview/test/AddGroundPlaneTest.ts b/@here/harp-mapview/test/AddGroundPlaneTest.ts index 04ef5d3c18..02f58f9cc6 100644 --- a/@here/harp-mapview/test/AddGroundPlaneTest.ts +++ b/@here/harp-mapview/test/AddGroundPlaneTest.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { MapEnv } from "@here/harp-datasource-protocol"; import { mercatorProjection, sphereProjection, @@ -74,7 +73,7 @@ describe("addGroundPlaneTest", function () { addGroundPlane(tile, 0); const adapter = MapObjectAdapter.get(getPlaneMesh(tile)); expect(adapter).not.equals(undefined); - expect(adapter!.isPickable(new MapEnv({}))).to.equal(false); + expect(adapter!.isPickable()).to.equal(false); }); it("plane mesh properties are correctly set", () => { diff --git a/@here/harp-mapview/test/PickingRaycasterTest.ts b/@here/harp-mapview/test/PickingRaycasterTest.ts index e7f8cf302c..b1ade4e70b 100644 --- a/@here/harp-mapview/test/PickingRaycasterTest.ts +++ b/@here/harp-mapview/test/PickingRaycasterTest.ts @@ -7,7 +7,7 @@ // Mocha discourages using arrow functions, see https://mochajs.org/#arrow-functions // Chai uses properties instead of functions for some expect checks. -import { MapEnv, Pickability } from "@here/harp-datasource-protocol"; +import { Pickability } from "@here/harp-datasource-protocol"; import { expect } from "chai"; import * as sinon from "sinon"; import * as THREE from "three"; @@ -26,7 +26,7 @@ describe("PickingRaycaster", function () { let raycaster: PickingRaycaster; beforeEach(function () { - raycaster = new PickingRaycaster(0, 0, new MapEnv({})); + raycaster = new PickingRaycaster(new THREE.Vector2()); }); describe("intersectObject(s)", function () { diff --git a/@here/harp-mapview/test/TileGeometryCreatorTest.ts b/@here/harp-mapview/test/TileGeometryCreatorTest.ts index a812e9066b..32d363156d 100644 --- a/@here/harp-mapview/test/TileGeometryCreatorTest.ts +++ b/@here/harp-mapview/test/TileGeometryCreatorTest.ts @@ -285,23 +285,23 @@ describe("TileGeometryCreator", () => { newTile.objects.forEach(object => { const adapter = MapObjectAdapter.get(object); expect(adapter).not.equals(undefined); - expect(adapter!.isPickable(new MapEnv({}))).to.equal( + expect(adapter!.isPickable()).to.equal( !isDepthPrePassMesh(object) && !(object as any).isLine ); }); }); - it("fill outline geometry is registered as non-pickable", () => { + it("fill outline geometry is registered as pickable", () => { const decodedTile: DecodedTile = getFillTile(); tgc.createObjects(newTile, decodedTile); assert.equal(newTile.objects.length, 2); const adapter0 = MapObjectAdapter.get(newTile.objects[0]); expect(adapter0).not.equals(undefined); - expect(adapter0!.isPickable(new MapEnv({}))).to.equal(true); + expect(adapter0!.isPickable()).to.equal(true); const adapter1 = MapObjectAdapter.get(newTile.objects[1]); expect(adapter1).not.equals(undefined); - expect(adapter1!.isPickable(new MapEnv({}))).to.equal(false); + expect(adapter1!.isPickable()).to.equal(true); }); it("solid line without outline is registered as pickable", () => { @@ -310,7 +310,7 @@ describe("TileGeometryCreator", () => { assert.equal(newTile.objects.length, 1); const adapter = MapObjectAdapter.get(newTile.objects[0]); expect(adapter).not.equals(undefined); - expect(adapter!.isPickable(new MapEnv({}))).to.equal(true); + expect(adapter!.isPickable()).to.equal(true); }); it("only outline geometry from solid line with outline is registered as pickable", () => { @@ -338,11 +338,11 @@ describe("TileGeometryCreator", () => { assert.equal(newTile.objects.length, 2); const adapter0 = MapObjectAdapter.get(newTile.objects[0]); expect(adapter0).not.equals(undefined); - expect(adapter0!.isPickable(new MapEnv({}))).to.equal(false); + expect(adapter0!.isPickable()).to.equal(false); const adapter1 = MapObjectAdapter.get(newTile.objects[1]); expect(adapter1).not.equals(undefined); - expect(adapter1!.isPickable(new MapEnv({}))).to.equal(true); + expect(adapter1!.isPickable()).to.equal(true); }); }); diff --git a/@here/harp-vectortile-datasource/test/MapViewPickingTest.ts b/@here/harp-vectortile-datasource/test/MapViewPickingTest.ts index d95f93a6d5..e72e0042a0 100644 --- a/@here/harp-vectortile-datasource/test/MapViewPickingTest.ts +++ b/@here/harp-vectortile-datasource/test/MapViewPickingTest.ts @@ -225,7 +225,7 @@ describe("MapView Picking", async function () { assert.isDefined(decodeTile.textGeometries); assert.isDefined(decodeTile.geometries); - assert.equal(decodeTile.geometries.length, 2); + assert.equal(decodeTile.geometries.length, 3); }); it("Decoded tile contains text pick info", async () => { @@ -258,17 +258,18 @@ describe("MapView Picking", async function () { assert.isDefined(polygonGeometry, "polygon geometry missing"); assert.isDefined(polygonGeometry.groups, "polygon geometry groups missing"); - assert.equal(polygonGeometry.groups.length, 1); + assert.equal(polygonGeometry.groups.length, 2); assert.isDefined(polygonGeometry.objInfos, "objInfos missing"); - assert.equal(polygonGeometry.objInfos!.length, 1); + assert.equal(polygonGeometry.objInfos!.length, 2); - const objInfo = polygonGeometry.objInfos![0] as any; - assert.include(objInfo, GEOJSON_DATA.features[0].properties); + assert.include(polygonGeometry.objInfos![0], GEOJSON_DATA.features[0].properties); + assert.include(polygonGeometry.objInfos![1], GEOJSON_DATA.features[3].properties); }); }); describe("Picking tests", async function () { const pickPolygonAt: number[] = [13.084716796874998, 22.61401087437029]; + const pickPolyOutlineAt: number[] = [-1.0, 29.0]; const pickLineAt: number[] = ((GEOJSON_DATA.features[1].geometry as any) .coordinates as number[][])[0]; const pickLabelAt: number[] = (GEOJSON_DATA.features[2].geometry as any).coordinates; @@ -326,6 +327,17 @@ describe("MapView Picking", async function () { elevation, shiftLongitude }, + { + name: `Pick ${elevatedText}polygon outline in ${projName} projection. \ + Shifted: ${shiftLongitude}`, + rayOrigGeo: elevation + ? pickPolyOutlineAt.concat([FAKE_HEIGHT]) + : pickPolyOutlineAt, + featureIdx: 3, + projection, + elevation, + shiftLongitude + }, { name: `Pick ${elevatedText}solid line in ${projName} projection. \ Shifted: ${shiftLongitude}`, diff --git a/@here/harp-vectortile-datasource/test/resources/geoJsonData.ts b/@here/harp-vectortile-datasource/test/resources/geoJsonData.ts index 96455ea9d3..b33a660f1f 100644 --- a/@here/harp-vectortile-datasource/test/resources/geoJsonData.ts +++ b/@here/harp-vectortile-datasource/test/resources/geoJsonData.ts @@ -51,6 +51,24 @@ export const GEOJSON_DATA: FeatureCollection = { type: "Point", coordinates: [-2.900390625, 26.115985925333536] } + }, + { + type: "Feature", + properties: { + name: "Polygon Outline" + }, + geometry: { + type: "Polygon", + coordinates: [ + [ + [-1.0, 14.86], + [0.57, 14.86], + [0.57, 29.46], + [-1.0, 29.46], + [-1.0, 14.86] + ] + ] + } } ] }; @@ -80,6 +98,18 @@ export const THEME: Theme = { }, renderOrder: 10.3 }, + { + styleSet: "geojson", + when: ["==", ["get", "name"], "Polygon Outline"], + technique: "fill", + renderOrder: 10000, + attr: { + color: "yellow", + lineWidth: 2, + lineColor: "#00ff00", + opacity: 0 + } + }, { description: "line", when: "$geometryType == 'line'",