UNPKG

@here/harp-mapview

Version:

Functionality needed to render a map.

338 lines 12.2 kB
"use strict"; /* * Copyright (C) 2019-2021 HERE Europe B.V. * Licensed under Apache 2.0, see full license in LICENSE * SPDX-License-Identifier: Apache-2.0 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ScreenCollisionsDebug = exports.ScreenCollisions = exports.isLineWithBound = exports.DetailedCollisionBox = exports.CollisionBox = void 0; const harp_utils_1 = require("@here/harp-utils"); const THREE = require("three"); const DebugContext_1 = require("./DebugContext"); const RBush = require("rbush"); const logger = harp_utils_1.LoggerManager.instance.create("ScreenCollissions"); class CollisionBox extends harp_utils_1.Math2D.Box { constructor(box) { super(); if (box !== undefined) { this.copy(box); } } copy(box) { if (box instanceof harp_utils_1.Math2D.Box) { this.set(box.x, box.y, box.w, box.h); } else if (box instanceof THREE.Box2) { this.set(box.min.x, box.min.y, box.max.x - box.min.x, box.max.y - box.min.y); } else { this.set(box.minX, box.minY, box.maxX - box.minX, box.maxY - box.minY); } return this; } get minX() { return this.x; } set minX(minX) { this.x = minX; } get maxX() { return this.x + this.w; } set maxX(maxX) { this.w = maxX - this.x; } get minY() { return this.y; } set minY(minY) { this.y = minY; } get maxY() { return this.y + this.h; } set maxY(maxY) { this.h = maxY - this.y; } } exports.CollisionBox = CollisionBox; /** * Collision box with additional boxes defining tighter bounds for the enclosed feature * (e.g.glyph bounds for text). */ class DetailedCollisionBox extends CollisionBox { constructor(box, detailBoxes) { super(box); this.detailBoxes = detailBoxes; } } exports.DetailedCollisionBox = DetailedCollisionBox; function isLineWithBound(box) { return box.line !== undefined; } exports.isLineWithBound = isLineWithBound; const tmpCollisionBox = new CollisionBox(); class ScreenCollisions { /** * Constructs a new ScreenCollisions object. */ constructor() { /** The screen bounding box. */ this.screenBounds = new harp_utils_1.Math2D.Box(); /** Tree of allocated bounds. */ this.rtree = new RBush(); // } /** * Resets the list of allocated screen bounds. */ reset() { this.rtree.clear(); } /** * Updates the screen bounds that are used to check if bounding boxes are visible. * * @param width - The width of the container. * @param height - The height of the container. */ update(width, height) { this.screenBounds.set(width / -2, height / -2, width, height); this.reset(); } /** * Marks the region of the screen intersecting with the given bounding box as allocated. * * @param bounds - The bounding box in NDC scaled coordinates (i.e. top left is -width/2, * -height/2) */ allocate(bounds) { const bbox = !(bounds instanceof CollisionBox) ? new CollisionBox(bounds) : bounds; this.rtree.insert(bbox); } /** * Inserts the given bounds into the rtree. * * @param bounds - The bounding boxes (the bounding boxes must be in the space returned from the * ScreenProjector.project method). */ allocateIBoxes(bounds) { this.rtree.load(bounds); } /** * Search for all bounds in the tree intersecting with the given box. * @param box - The box used for the search. * @returns An array of all IBoxes intersecting with the given box. */ search(box) { return this.rtree.search(box); } /** * Checks if the given bounding box is already allocated. * * @param bounds - The bounding box in world coordinates. */ isAllocated(bounds) { const collisionBox = bounds instanceof CollisionBox ? bounds : tmpCollisionBox.copy(bounds); const results = this.search(collisionBox); return this.intersectsDetails(collisionBox, results); } /** * Checks if the given screen bounds intersects with the frustum of the active camera. * * @param bounds - The bounding box in world coordinates. */ isVisible(bounds) { return this.screenBounds.intersects(bounds); } /** * Checks if the given screen bounds is contained within the frustum of the active camera. * * @param bounds - The bounding box in world coordinates. */ isFullyVisible(bounds) { return this.screenBounds.containsBox(bounds); } /** * Test whether a given [[CollisionBox]] intersects with any of the details in the specified * [[IBox]]es. * * @param testBox - The box to test for intersection. * @param boxes - The candidate boxes the test box may intersect with. It's assumed that the * global bounds of these boxes intersect with the given test box. * @returns `true` if any intersection found. */ intersectsDetails(testBox, boxes) { for (const box of boxes) { if (box instanceof DetailedCollisionBox) { for (const detailBox of box.detailBoxes) { if (detailBox.intersects(testBox)) { return true; } } } else if (isLineWithBound(box)) { const boundedLine = box; if (this.intersectsLine(testBox, boundedLine)) { return true; } } else { return true; } } return false; } /** * Computes the intersection between the supplied CollisionBox and the LineWithBound. * @note The [[CollisionBox]] is in Screen Bounds space, whereas the line must be * in Screen Coordinate space */ intersectsLine(bbox, boundedLine) { const line = boundedLine.line; // Note, these aren't normalized, but it doesn't matter, we are just interested // in the sign. const lineXDiffTransformed = line.end.x - line.start.x; // Sign of bottom left, bottom right, top left and top right corners. let signBL; let signBR; let signTL; let signTR; if (lineXDiffTransformed !== 0) { const lineYDiffTransformed = line.end.y - line.start.y; const normalX = lineYDiffTransformed; const normalY = -lineXDiffTransformed; const D = line.start.y - (lineYDiffTransformed / lineXDiffTransformed) * line.start.x; signBL = Math.sign(bbox.minX * normalX + (bbox.minY - D) * normalY); signBR = Math.sign(bbox.maxX * normalX + (bbox.minY - D) * normalY); signTL = Math.sign(bbox.minX * normalX + (bbox.maxY - D) * normalY); signTR = Math.sign(bbox.maxX * normalX + (bbox.maxY - D) * normalY); } else { signBL = Math.sign(bbox.minX - line.start.x); signBR = Math.sign(bbox.maxX - line.start.x); signTL = Math.sign(bbox.minX - line.start.x); signTR = Math.sign(bbox.maxX - line.start.x); } return signBL !== signBR || signBL !== signTL || signBL !== signTR; } } exports.ScreenCollisions = ScreenCollisions; /** * @hidden * * Shows requests for screen space during labelling in an HTML canvas, which should be sized like * the actual map canvas. It can be placed on top of the map canvas to show exactly which requests * for screen space were done. * * Also logs statistics. */ class ScreenCollisionsDebug extends ScreenCollisions { /** * Constructs a new ScreenCollisions object which renders its state to a 2D canvas. */ constructor(debugCanvas) { super(); /** 2D rendering context. */ this.m_renderContext = null; this.m_renderingEnabled = false; this.m_numAllocations = 0; this.m_numSuccessfulTests = 0; this.m_numFailedTests = 0; this.m_numSuccessfulVisibilityTests = 0; this.m_numFailedVisibilityTests = 0; if (debugCanvas !== undefined && debugCanvas !== null) { this.m_renderContext = debugCanvas.getContext("2d"); } } /** * Resets the list of allocated bounds and clears the debug canvas. * @override */ reset() { super.reset(); this.m_numAllocations = 0; this.m_numSuccessfulTests = 0; this.m_numFailedTests = 0; this.m_numSuccessfulVisibilityTests = 0; this.m_numFailedVisibilityTests = 0; } /** * Updates the screen bounds used to check if bounding boxes are visible. * * @param width - The width of the container. * @param height - The height of the container. * @override */ update(width, height) { if (this.m_renderingEnabled) { logger.log(`Allocations: ${this.m_numAllocations} Successful Tests: ${this.m_numSuccessfulTests} Failed Tests: ${this.m_numFailedTests} Successful Visibility Tests: ${this.m_numSuccessfulVisibilityTests} Failed Visibility Tests: ${this.m_numFailedVisibilityTests} `); } super.update(width, height); if (this.m_renderContext !== null) { this.m_renderContext.canvas.width = width; this.m_renderContext.canvas.height = height; } // activate in the browser with: // window.__debugContext.setValue("DEBUG_SCREEN_COLLISIONS", true) this.m_renderingEnabled = DebugContext_1.debugContext.getValue("DEBUG_SCREEN_COLLISIONS"); } /** * Marks the region of the screen intersecting with the given bounding box as allocated. * * @param bounds - the bounding box in world coordinates. * @override */ allocate(bounds) { super.allocate(bounds); this.m_numAllocations++; if (this.m_renderingEnabled && this.m_renderContext !== null) { this.m_renderContext.strokeStyle = "#6666ff"; this.m_renderContext.strokeRect(bounds.x - this.screenBounds.x, this.screenBounds.y + this.screenBounds.h - bounds.y, bounds.w, -bounds.h); } } /** @override */ allocateIBoxes(boundsArray) { for (const bounds of boundsArray) { this.m_numAllocations++; if (this.m_renderingEnabled && this.m_renderContext !== null) { this.m_renderContext.strokeStyle = "#aa2222"; this.m_renderContext.strokeRect(bounds.minX - this.screenBounds.x, this.screenBounds.y + this.screenBounds.h - bounds.minY, bounds.maxX - bounds.minX, -(bounds.maxY - bounds.minY)); } } super.allocateIBoxes(boundsArray); } /** @override */ intersectsDetails(testBox, boxes) { const collisionFound = super.intersectsDetails(testBox, boxes); if (this.m_renderingEnabled && this.m_renderContext !== null) { const padding = collisionFound ? 2 : 1; this.m_renderContext.strokeStyle = collisionFound ? "#FF0000" : "#00ff00"; this.m_renderContext.strokeRect(testBox.x - this.screenBounds.x - padding, this.screenBounds.y + this.screenBounds.h - testBox.y + padding, testBox.w + 2 * padding, -testBox.h - 2 * padding); } if (collisionFound) { this.m_numFailedTests++; } else { this.m_numSuccessfulTests++; } return collisionFound; } /** * Checks if the given screen bounds intersects with the frustum of the active camera. * * @param bounds - The bounding box in world coordinates. * @override */ isVisible(bounds) { const visible = super.isVisible(bounds); if (visible) { this.m_numSuccessfulVisibilityTests++; } else { this.m_numFailedVisibilityTests++; } return visible; } } exports.ScreenCollisionsDebug = ScreenCollisionsDebug; //# sourceMappingURL=ScreenCollisions.js.map