diff --git a/starter.html b/index.html similarity index 99% rename from starter.html rename to index.html index aac4d38..e896545 100644 --- a/starter.html +++ b/index.html @@ -27,6 +27,7 @@ callback: function(viewerWindow, viewerAsync) { console.info("initial callback called:", viewerWindow, viewerAsync); viewerAsync("../assets/", (api) => { + console.info("loaded callback called with API:", api); }); } diff --git a/src/css/main.css b/src/css/main.css index 2e7d34e..4cd5a04 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -25,9 +25,9 @@ body { #map{ position: fixed; bottom: 1%; - right: 3%; + right: 1%; width: 20%; - height:20%; + height: 20%; color: #fff; } @@ -37,9 +37,16 @@ body { .control-OL{ position: fixed; - bottom:20.5%; - right: 3%; + bottom: 22%; + right: 1%; color: #fff; } +#menu { + float: left; + position: absolute; + top: 0 px; + left: 0%; + z-index: 2000; +} diff --git a/src/index.html b/src/index.html index 19c1b9d..b60dbf5 100644 --- a/src/index.html +++ b/src/index.html @@ -60,6 +60,11 @@
+
diff --git a/src/js/viewer/ViewerAPI.js b/src/js/viewer/ViewerAPI.js index fef3186..6827bbf 100644 --- a/src/js/viewer/ViewerAPI.js +++ b/src/js/viewer/ViewerAPI.js @@ -7,6 +7,8 @@ import { ViewerMapAPI } from "./ViewerMapAPI.js" import { ViewerState } from "./ViewerState.js"; import { libraryInfo } from "./LibraryInfo.js"; import { ViewerVersionAPI } from "./ViewerVersionAPI.js"; +import { ViewerContextItem } from "./ViewerContextItem.js"; +import { LON_SCALAR, LAN_SCALAR } from "./ViewerConfig.js"; // API provided by the viewer @@ -27,7 +29,7 @@ export class ViewerAPI { this.baseURL = baseURL; this.listeners = []; - + this.renderer; // Load the metadata only once $.ajax({ @@ -43,6 +45,7 @@ export class ViewerAPI { this.pano = new ViewerPanoAPI(this); this.map = new ViewerMapAPI(this); }).then(() => { + document.addEventListener('mousedown', function (e) { e.preventDefault(); }, false); // the only html element we work with (the pano-viewer div) const panoDiv = document.getElementById('pano-viewer'); @@ -54,7 +57,7 @@ export class ViewerAPI { this.renderer.sortObjects = false; panoDiv.appendChild(this.renderer.domElement); - + // start animation loop this.animate(); }); @@ -73,12 +76,12 @@ export class ViewerAPI { let minDistance = 1000000000; let bestImg; - + this.floor.currentFloor.viewerImages.forEach(element => { const currLocalPos = this.toLocal(element.pos); - const [dx, dz] = [localPos.x - currLocalPos.x, localPos.z - currLocalPos.z]; - const currDistance = Math.sqrt(dx * dx + dz * dz); - + const [dx, dy, dz] = [localPos.x - currLocalPos.x, localPos.y - currLocalPos.y, localPos.z - currLocalPos.z]; + const currDistance = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (currDistance < minDistance) { minDistance = currDistance; bestImg = element; @@ -136,11 +139,10 @@ export class ViewerAPI { // Convert the local metric three.js coordinates used by the viewer to WGS 84 coordinates [longitude, latitude, z]. toGlobal(localCoords) { // localCoords : THREE.Vector3 // Local coordinates used by the viewer - const globalX = this.floor.origin[0] - ((localCoords.x / 1000) / 71.5); - const globalY = this.floor.origin[1] - ((-localCoords.z / 1000) / 111.3); - const globalZ = localCoords.y - this.floor.currentFloor.z; + const globalX = this.floor.origin[0] - ((localCoords.x / 1000) / LON_SCALAR); + const globalY = this.floor.origin[1] - ((localCoords.y / 1000) / LAN_SCALAR); + const globalZ = localCoords.z - this.floor.currentFloor.z; - // the three js scene sees the y axis as the up-down axis so we have to swap with z return [globalX, globalY, globalZ]; // Returns: [Number] : WGS 84 coordinates [longitude, latitude, z] (z value is floorZ + panoZ, where localCoords is just the panoZ) } @@ -150,13 +152,61 @@ export class ViewerAPI { toLocal(globalCoords) { // Distance calculation math taken from here https://www.mkompf.com/gps/distcalc.html // The more accurate calculation breaks the pixel offset on the pre-created maps - const dx = 71.5 * (this.floor.origin[0] - globalCoords[0]); - const dz = 111.3 * (this.floor.origin[1] - globalCoords[1]); + const dx = LON_SCALAR * (this.floor.origin[0] - globalCoords[0]); + const dy = LAN_SCALAR * (this.floor.origin[1] - globalCoords[1]); return new this.THREE.Vector3( dx * 1000, - globalCoords[2] + this.floor.currentFloor.z, - -dz * 1000); + dy * 1000, + globalCoords[2] + this.floor.currentFloor.z); + } + + eventMeshTest(x = 0, y = 0, z = -2) { + // visual test, spawn in white sphere at first image position in scene (offset specified by parameters) + const sphere = new THREE.SphereGeometry(1 / 5, 10, 10); + const testMesh = new THREE.Mesh(sphere, new THREE.MeshBasicMaterial()); + const startPos = this.toLocal(this.image.currentImage.pos); + testMesh.position.set(startPos.x + x, startPos.y + y, startPos.z + z); + + testMesh.vwr_onclick = function (xy, position) { + this.material.color.set(0xff0000); // as a test set color red + console.log("vwr_onclick is triggered."); + console.log("Pointer position: " , xy); + console.log("Local coordinate for pointer position: " , position); + return true; + } + + testMesh.vwr_oncontext = function (xy, position) { + this.material.color.set(0x00ff00); // as a test set color green + console.log("vwr_oncontext is triggered."); + console.log("Pointer position: " , xy); + console.log("Local coordinate for pointer position: " , position); + + //Creating callback function for context menu item: + let callback = function (key, options) { + var msg = 'clicked: ' + key; + (window.console && console.log(msg)) || alert(msg); + }; + + //Creating item objects + let itemEdit = new ViewerContextItem(callback, "edit", null, "Edit"); + let itemCut = new ViewerContextItem(callback, "cut", null, "Cut"); + + //Creating list of item objects. + return [itemEdit, itemCut]; + } + + testMesh.vwr_onpointerenter = function () { + this.material.color.set(0xffff00); // as a test set color yellow + console.log("vwr_onpointerenter is triggered."); + } + + testMesh.vwr_onpointerleave = function () { + this.material.color.set(0x0000ff); // as a test set color blue + console.log("vwr_onpointerleave is triggered."); + } + + this.pano.addLayer(testMesh); } // TODO: swap() and big(wanted) diff --git a/src/js/viewer/ViewerConfig.js b/src/js/viewer/ViewerConfig.js index b75d97e..2e20d45 100644 --- a/src/js/viewer/ViewerConfig.js +++ b/src/js/viewer/ViewerConfig.js @@ -26,3 +26,9 @@ export const SCALING_MAP = 0.2; // Describes the maximum zoom of the map. export const MAP_ZOOM = 4; + +// Scalar for Longitude from degree to km +export const LON_SCALAR = 71.5; + +// Scalar for Langitude from degree to km +export const LAN_SCALAR = 111.3; \ No newline at end of file diff --git a/src/js/viewer/ViewerFloorAPI.js b/src/js/viewer/ViewerFloorAPI.js index 4a261b0..2d2eb7b 100644 --- a/src/js/viewer/ViewerFloorAPI.js +++ b/src/js/viewer/ViewerFloorAPI.js @@ -1,5 +1,8 @@ "use strict"; +import { LON_SCALAR, LAN_SCALAR } from "./ViewerConfig.js"; + + export class ViewerFloorAPI { constructor(data, viewerAPI) { @@ -28,7 +31,7 @@ export class ViewerFloorAPI { if (currentImage.id >= interval[0] && currentImage.id <= interval[1]) { currentImage.floor = key; - const [dx, dy] = [71.5 * (data.lon0 - currentImage.pos[0]), 111.3 * (data.lat0 - currentImage.pos[1])]; + const [dx, dy] = [LON_SCALAR * (data.lon0 - currentImage.pos[0]), LAN_SCALAR * (data.lat0 - currentImage.pos[1])]; const offsetX = currentFloor.mapData.x + currentFloor.mapData.density * (dx * 1000); const offsetY = currentFloor.mapData.y - currentFloor.mapData.density * (dy * 1000); diff --git a/src/js/viewer/ViewerImage.js b/src/js/viewer/ViewerImage.js index a30fe78..1323122 100644 --- a/src/js/viewer/ViewerImage.js +++ b/src/js/viewer/ViewerImage.js @@ -16,12 +16,7 @@ export class ViewerImage { this.pos = [panoLon, panoLat, panoZ]; // : [Number] // WGS 84 coordinates [longitude, latitude, z] of this image - // The quaternion data available in the json is not quite compatible with the translation we need in our scene - const threeX = y; - const threeY = z; - const threeZ = x; - - this.orientation = new THREE.Quaternion(threeX, threeY, threeZ, w); + this.orientation = new THREE.Quaternion(x, y, z, w); this.mapOffset; // : [offsetX, offsetY] // in pixels, offset from map png diff --git a/src/js/viewer/ViewerMapAPI.js b/src/js/viewer/ViewerMapAPI.js index fd15762..b52a428 100644 --- a/src/js/viewer/ViewerMapAPI.js +++ b/src/js/viewer/ViewerMapAPI.js @@ -1,6 +1,6 @@ "use strict"; -import { MAX_FOV, SCALING_MAP, MAP_ZOOM } from "./ViewerConfig.js"; +import { MAX_FOV, SCALING_MAP, MAP_ZOOM, LON_SCALAR, LAN_SCALAR } from "./ViewerConfig.js"; // Map (2D) Viewer API // Specific API for the Map View @@ -29,6 +29,16 @@ export class ViewerMapAPI { this.lastLayerDirection = []; this.redraw(); + + this.viewerAPI = viewerAPI; + let map = document.getElementById('map'); + map.addEventListener('dblclick', (event) => this.onDoubleClick(event)); + + map.addEventListener('fullscreenchange', (event) => { + // If map set to full screen, hide the floor setting buttons + hideButtons( "floorOL"); + }); + this.control_button(); } // Method: Add an event layer to the map (2D) view. @@ -64,15 +74,15 @@ export class ViewerMapAPI { }), controls: ol.control.defaults({ // Hide Map rotation button - rotate: false - }).extend([ - // create fullScreen button - new ol.control.FullScreen(), - ]), + rotate: false, + zoom: false + }), //Disable Zoom Control on MAP - interactions: ol.interaction.defaults({ mouseWheelZoom: false }), + interactions: ol.interaction.defaults({doubleClickZoom :false}), }); + + // create image layers for each floors for (var i = 0; i < this.viewerFloorAPI.floors.length; i++) { let mapData = this.viewerFloorAPI.floors[i].mapData @@ -179,6 +189,9 @@ export class ViewerMapAPI { this.map.addLayer(vectorLayerRed); + // set view to middle + this.setMiddle(this.posLon,this.posLan); + // save last vector layers for deleting next time this.lastVectorLayer = currentVectorLayer; this.lastVectorLayerRed = vectorLayerRed; @@ -193,7 +206,7 @@ export class ViewerMapAPI { var lonov = this.viewerViewState.lonov; // temporary using 170 degree for correcting the starting zero degree of 2D map - var direction = -(lonov + 180) * (Math.PI / 180) % 360; + var direction = lonov * (Math.PI / 180) % 360; // remove prvious vector layers @@ -259,9 +272,70 @@ export class ViewerMapAPI { getLonLanCoordinates(position, mapdata){ // Compute the latitude and longitude in reference to the origin in WGS84 and aff offset of the map - let lon = 87000 * (position[0] - this.viewerFloorAPI.origin[0]) + (mapdata.x / mapdata.density); - let lan = 111000 * (position[1] - this.viewerFloorAPI.origin[1]) + (mapdata.y / mapdata.density); + let lon = LON_SCALAR * 1000 * (position[0] - this.viewerFloorAPI.origin[0]) + (mapdata.x / mapdata.density); + let lan = LAN_SCALAR * 1000 * (position[1] - this.viewerFloorAPI.origin[1]) + (mapdata.y / mapdata.density); return [lon, lan]; } + + onDoubleClick(event) { + + var coord = []; + var mousePosition = []; + var mapdata = this.viewerFloorAPI.floors[this.viewerFloorAPI.currentFloorId].mapData; + var floor = this.viewerFloorAPI; + var z = this.viewerFloorAPI.floors[this.viewerFloorAPI.currentFloorId].z; + var viewerAPI = this.viewerAPI; + + this.map.on('dblclick', function(event){ + + coord = event.coordinate; + mousePosition.push(((coord[0] - (mapdata.x / mapdata.density)) / (LON_SCALAR * 1000) ) + floor.origin[0]); + mousePosition.push(((coord[1] - (mapdata.y / mapdata.density)) / (LAN_SCALAR * 1000) ) + floor.origin[1]); + + // move + viewerAPI.move(mousePosition[0],mousePosition[1],z); + + }) + } + + setMiddle(poslon, poslan){ + this.map.getView().setCenter([poslon,poslan]); + } + + control_button(){ + var zoom_in = document.getElementById('zoom-in'); + var zoom_out = document.getElementById('zoom-out'); + var full_screen = document.getElementById('full-screen'); + var map = this.map; + + zoom_in.addEventListener('click', function () { + var view = map.getView(); + var zoom = view.getZoom(); + view.setZoom(zoom + 1); + }) + + zoom_out.addEventListener('click', function () { + var view = map.getView(); + var zoom = view.getZoom(); + view.setZoom(zoom - 1); + }) + + full_screen.addEventListener('click', function () { + var elem = document.getElementById('map'); + elem.requestFullscreen(); + }) + } } +function hideButtons(divId) { + + //let divId = "floorOL"; + var element = document.getElementById(divId); + + /* Toggle to hide HTML div */ + if (element.style.display === "none") { + element.style.display = "block"; + } else { + element.style.display = "none"; + } + } \ No newline at end of file diff --git a/src/js/viewer/ViewerPanoAPI.js b/src/js/viewer/ViewerPanoAPI.js index 8b304df..10113a0 100644 --- a/src/js/viewer/ViewerPanoAPI.js +++ b/src/js/viewer/ViewerPanoAPI.js @@ -1,52 +1,61 @@ "use strict"; import { ViewerViewState } from "./ViewerViewState.js"; -import { DEFAULT_FOV, MAX_FOV, MIN_FOV, ZOOM_SPEED, PAN_SPEED } from "./ViewerConfig.js"; -import { EventLayer } from "./EventLayer.js"; +import { DEFAULT_FOV, MAX_FOV, MIN_FOV, ZOOM_SPEED, PAN_SPEED} from "./ViewerConfig.js"; import { EventPosition } from "./EventPosition.js"; export class ViewerPanoAPI { constructor(viewerAPI) { - this.scene = new THREE.Scene(); // three.js scene used by the panorama (3D) viewer - this.camera = new THREE.PerspectiveCamera(DEFAULT_FOV, window.innerWidth / window.innerHeight, 1, 1100); - this.viewerImageAPI = viewerAPI.image; this.viewerAPI = viewerAPI; - this.sphereRadius = 5; + this.addedLayers = new Set(); // EventMesh and EventLayer objects added via addLayer(); - this.viewerViewState = new ViewerViewState(DEFAULT_FOV, 0, 0); - this.lastViewState; - this.lastMousePos; + this.scene = new THREE.Scene(); // three.js scene used by the panorama (3D) viewer + this.camera = new THREE.PerspectiveCamera(DEFAULT_FOV, window.innerWidth / window.innerHeight, 1, 1100); + this.camera.up = new THREE.Vector3(0, 0, 1); + this.sphereRadius = 10; - //initialize the eventLayer - this.eventLayer = new EventLayer(); - - // properties needed for display and depthAtPointer method + // property needed for display method this.loadedMesh = null; - this.depthCanvas = document.createElement("canvas"); - // Two new event listeneres are called to handle *how far* the user drags - this.oPM = (event) => this.onPointerMove(event); - this.oPU = () => this.onPointerUp(); + // property needed for depthAtPointer method + this.depthCanvas = document.createElement("canvas"); + // handeling zooming / panning / moving / resizing const panoViewer = document.getElementById('pano-viewer'); + this.viewerViewState = new ViewerViewState(DEFAULT_FOV, 0, 0); + this.lastViewState; + this.lastMousePos; panoViewer.addEventListener('wheel', (event) => this.onDocumentMouseWheel(event)); panoViewer.addEventListener('pointerdown', (event) => this.onPointerDown(event)); panoViewer.addEventListener('dblclick', (event) => this.onDoubleClick(event)); + window.addEventListener("resize", () => this.onWindowResize()); + // Two new event listeneres are called to handle *how far* the user drags + this.oPM = (event) => this.onPointerMove(event); + this.oPU = () => this.onPointerUp(); + + // handeling EventMesh / EventLayer API integration + this.preMeshes = new Set(); // meshes that the mouse pointer is currently over + panoViewer.addEventListener('click', (event) => this.meshCheckClick(event)); + panoViewer.addEventListener('contextmenu', (event) => { + event.preventDefault(); + this.meshCheckRightClick(event); + }); + panoViewer.addEventListener('pointermove', (event) => this.meshCheckMouseOver(event)); - $('#pano-viewer').mousedown((event) => this.onRightClick(event)); - this.display(this.viewerImageAPI.currentImageId); + this.display(this.viewerAPI.image.currentImageId); } // displays the panorama with idx *ImageNum* in the model display(imageNum) { - this.viewerImageAPI.currentImageId = imageNum; + this.viewerAPI.image.currentImageId = imageNum; // create sphere const sphere = new THREE.SphereGeometry(this.sphereRadius, 60, 40); // invert the geometry on the x-axis so that we look out from the middle of the sphere sphere.scale(-1, 1, 1); + sphere.rotateX(Math.PI / 2); // load the 360-panorama image data (highest resolution hardcoded for now) const texturePano = this.viewerAPI.textureLoader.load( @@ -61,9 +70,9 @@ export class ViewerPanoAPI { const image = new Image(); //image.crossOrigin = "use-credentials"; image.src = this.viewerAPI.baseURL + - Math.trunc(this.viewerImageAPI.currentImage.id / 100) + '/' + - this.viewerImageAPI.currentImage.id + 'd.png'; - + Math.trunc(imageNum / 100) + '/' + + imageNum + 'd.png'; + image.addEventListener('load', () => { this.depthCanvas.getContext("2d").drawImage(image, 0, 0); }, false); @@ -71,12 +80,12 @@ export class ViewerPanoAPI { // put the texture on the spehere and add it to the scene const mesh = new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({ map: texturePano })); - + // adjust for orientation offset - mesh.applyQuaternion(this.viewerImageAPI.currentImage.orientation); - + mesh.applyQuaternion(this.viewerAPI.image.currentImage.orientation); + // put in the correct position in the scene - const localCoord = this.viewerAPI.toLocal(this.viewerImageAPI.currentImage.pos); + const localCoord = this.viewerAPI.toLocal(this.viewerAPI.image.currentImage.pos); mesh.position.set(localCoord.x, localCoord.y, localCoord.z); // check if other panorama was previously already loaded @@ -86,7 +95,7 @@ export class ViewerPanoAPI { this.scene.add(mesh); this.loadedMesh = mesh; - + // put camera inside sphere mesh this.camera.position.set(localCoord.x, localCoord.y, localCoord.z); } @@ -100,7 +109,7 @@ export class ViewerPanoAPI { const normalizedViewingDirection = lonLatToLocal(lonov, latov); // adjust looking direction for offset of current mesh in scene - const localCoord = this.viewerAPI.toLocal(this.viewerImageAPI.currentImage.pos); + const localCoord = this.viewerAPI.toLocal(this.viewerAPI.image.currentImage.pos); this.camera.lookAt(localCoord.add(normalizedViewingDirection)); @@ -109,6 +118,23 @@ export class ViewerPanoAPI { this.camera.updateProjectionMatrix(); } + // Add an event layer to the panorama (3D) viewer. + // param: EventLayer (or EventMesh) to add + addLayer(layer) { + if (!layer) return; + if (this.addedLayers.has(layer)) return; + + this.scene.add(layer); + this.addedLayers.add(layer); + } + + removeLayer(layer) { + if (!layer) return; + if (!this.addedLayers.has(layer)) return; + + this.scene.remove(layer); + this.addedLayers.delete(layer); + } // ----- Event handling functions for panning, zooming and moving ----- onPointerDown(event) { @@ -124,7 +150,7 @@ export class ViewerPanoAPI { onPointerMove(event) { const scalingFactor = this.camera.fov / MAX_FOV; - this.viewerViewState.setLonov((this.lastMousePos[0] - event.clientX) * PAN_SPEED * scalingFactor + this.lastViewState[0]); + this.viewerViewState.setLonov((event.clientX - this.lastMousePos[0]) * PAN_SPEED * scalingFactor + this.lastViewState[0]); this.viewerViewState.setLatov((event.clientY - this.lastMousePos[1]) * PAN_SPEED * scalingFactor + this.lastViewState[1]); this.viewerAPI.map.show_direction(); @@ -140,7 +166,7 @@ export class ViewerPanoAPI { onDocumentMouseWheel(event) { this.viewerViewState.fov = this.camera.fov + event.deltaY * ZOOM_SPEED; - + this.view(this.viewerViewState.lonov, this.viewerViewState.latov, this.viewerViewState.fov); this.camera.updateProjectionMatrix(); @@ -149,67 +175,125 @@ export class ViewerPanoAPI { } onDoubleClick(event) { - const [adjustedLonov, adjustedLatov] = this.getAdjustedViewstate(event); - const MEDIAN_WALKING_DISTANCE = 5; // in meter - // distance to be walked along adjustedHorizontalAngle from current location - const distance = MEDIAN_WALKING_DISTANCE + ((adjustedLatov / 85) * MEDIAN_WALKING_DISTANCE); + const currentPos = this.viewerAPI.image.currentImage.pos; + const newLocalPos = this.getCursorLocation(event); + const newPos = this.viewerAPI.toGlobal(newLocalPos); - // convertedAngle converted to represent directions like specified in newLocationFromPointAngle - const convertedAngle = (adjustedLonov < 180) ? -adjustedLonov : 360 - adjustedLonov; - - const currentPos = this.viewerImageAPI.currentImage.pos; - - const newPos = newLocationFromPointAngle(currentPos[0], currentPos[1], THREE.Math.degToRad(convertedAngle), distance); this.viewerAPI.move(newPos[0], newPos[1], currentPos[2]); - this.viewerAPI.propagateEvent("moved", this.viewerImageAPI.currentImage.id, true); + this.viewerAPI.propagateEvent("moved", this.viewerAPI.image.currentImage.id, true); + } + + onWindowResize() { + this.camera.aspect = window.innerWidth / window.innerHeight; + this.camera.updateProjectionMatrix(); + + this.viewerAPI.renderer.setSize(window.innerWidth, window.innerHeight); } - onRightClick(event) { - //if right mouse is clicked: - if (event.which == 3) { + // ---- event handeling functions for EventMesh / EventLayer API interaction ---- + getIntersectingMeshes(event) { + const raycaster = this.getRaycaster(event); + + // calculate objects intersecting the picking ray + const intersects = raycaster.intersectObjects(this.scene.children); + + // include only objects that are added meshes + const meshes = []; + for (const e in intersects) { + if (this.addedLayers.has(intersects[e].object)) { + // check if mesh is within sphere radius to camera + const dist = this.camera.position.distanceTo(intersects[e].object.position); + if (dist < this.sphereRadius) { + meshes.push(intersects[e].object); + } + } + } - //get the current pointer position: - const xy = new EventPosition(event); + return meshes; + } + meshCheckClick(event) { + const meshes = this.getIntersectingMeshes(event); + const xy = new EventPosition(event); + const location = this.getCursorLocation(event); - //get the position of pointer in scene: - const location = this.getCursorLocation(event); + for (let i = 0; i < meshes.length; i++) { + const mesh = meshes[i]; - //Set up the context menu: - $.contextMenu({ - selector: '#pano-viewer', - items: this.eventLayer.vwr_oncontext(xy, location), - }); + if (typeof mesh.vwr_onclick == "function") { + mesh.vwr_onclick(xy, location); + } } } + meshCheckRightClick(event) { + const meshes = this.getIntersectingMeshes(event); + const xy = new EventPosition(event); + const location = this.getCursorLocation(event); + + for (let i = 0; i < meshes.length; i++) { + const mesh = meshes[i]; + + if (typeof mesh.vwr_oncontext == "function") { + const callback = mesh.vwr_oncontext(xy, location); + + $.contextMenu({ + selector: '#pano-viewer', + items: callback, + }); + } + } + } + + meshCheckMouseOver(event) { + const meshes = this.getIntersectingMeshes(event); + + // check for meshes that mouse pointer is no longer over + this.preMeshes.forEach((preMesh) => { + if (!meshes.includes(preMesh)) { + if (typeof preMesh.vwr_onpointerleave == "function") { + // remove the current mesh + this.preMeshes.delete(preMesh); + + preMesh.vwr_onpointerleave(); + } + } + }); + + // check for meshes that mouse pointer is newly over + for (let i = 0; i < meshes.length; i++) { + const mesh = meshes[i]; + + //if the current mesh has not been entered before. + if (!this.preMeshes.has(mesh)) { + if (typeof mesh.vwr_onpointerenter == "function") { + // store the current mesh + this.preMeshes.add(mesh); + + mesh.vwr_onpointerenter(); + } + } + } + } // returns: the depth information (in meter) of the panorama at the current curser position (event.clientX, event.clientY) depthAtPointer(event) { - const [adjustedLonov, adjustedLatov] = this.getAdjustedViewstate(event); - + const raycaster = this.getRaycaster(event); // because depth map is not rotated by quaternion like panorama mesh, the quaternion adjustment need to happen first - const localPos = lonLatToLocal(adjustedLonov, adjustedLatov); - const adjustedQuaternion = localPos.applyQuaternion(this.viewerImageAPI.currentImage.orientation); - const [realLonov, realLatov] = localToLonLat(adjustedQuaternion); + const mappedCursorDirection = raycaster.ray.direction.applyQuaternion(this.viewerAPI.image.currentImage.orientation); + const [cursorLon, cursorLat] = localToLonLat(mappedCursorDirection); - // pixel offsets in depth map at current curser position - const pixelX = Math.trunc((realLonov / 360) * this.depthCanvas.width); - const pixelY = Math.trunc((realLatov + 90) / 180 * this.depthCanvas.height); + // adjust to calculate pixel offset on image, values in [0;360, -90;90] + const [adjustedLonov, adjustedLatov] = [((180 - cursorLon) + 360) % 360, cursorLat]; - const offsetX = (pixelX >= 2) ? pixelX - 2 : 0; - const offsetY = (pixelY >= 2) ? pixelY - 2 : 0; + // pixel offsets in depth map at current curser position + const pixelX = Math.trunc((adjustedLonov / 360) * this.depthCanvas.width); + const pixelY = Math.trunc((adjustedLatov + 90) / 180 * this.depthCanvas.height); // convert pixel value to depth information - const use5pixelAvg = false; - let imgData; - if (use5pixelAvg) { - imgData = this.depthCanvas.getContext("2d").getImageData(offsetX, offsetY, 5, 5); - } else { - imgData = this.depthCanvas.getContext("2d").getImageData(pixelX, pixelY, 1, 1); - } - const [red, green, blue, alpha] = averagePixelValues(imgData.data); + const imgData = this.depthCanvas.getContext("2d").getImageData(pixelX, pixelY, 1, 1); + const [red, green, blue, alpha] = imgData.data; // LSB red -> green -> blue MSB (ignore alpha) const distanceMM = red | (green << 8) | (blue << 16); @@ -220,121 +304,49 @@ export class ViewerPanoAPI { // returns the current location of the cursor in the three js scene (Vector3) getCursorLocation(event) { - // param: event.x event.y current cursor position on screen - const [adjustedLonov, adjustedLatov] = this.getAdjustedViewstate(event); - const normalizedLocalViewingDir = lonLatToLocal(adjustedLonov, adjustedLatov); - - // adjust looking direction for offset of current mesh in scene - const localCoord = this.viewerAPI.toLocal(this.viewerImageAPI.currentImage.pos); - - // get distance und extend viewing direction vector by distance - const dist = this.depthAtPointer(event); - - localCoord.addScaledVector(normalizedLocalViewingDir, dist) - - return localCoord; - } - - // returns [lonov, latov] at the current cursor position - getAdjustedViewstate(event) { - // find correct pixel position on equilateral projected depth map - const halfWidth = window.innerWidth / 2; - const halfHeight = window.innerHeight / 2; - - // horizontal (lonov) : image left -> 0, image right -> 360 - // vertical (latov) : image top -> 85, image bottom -> -85 - const horizontalOffset = (event.clientX - halfWidth) / halfWidth; // scaled between [-1,1] depending how left-right the mouse click is on the screen - const verticalOffset = (halfHeight - event.clientY) / halfHeight; // scaled between [-1,1] depending how up-down the mouse click is on the screen - - const adjustedLonov = ((this.viewerViewState.lonov + (horizontalOffset * this.viewerViewState.fov / 2)) + 360) % 360; - const adjustedLatov = Math.max(-85, Math.min(85, this.viewerViewState.latov + (verticalOffset * this.viewerViewState.fov / 2))); + const raycaster = this.getRaycaster(event); + // formula for position is currentLoc + direction*distance (where the direction is normalized) + const distance = this.depthAtPointer(event); + const cursorLocation = raycaster.ray.origin.addScaledVector(raycaster.ray.direction, distance); - return [adjustedLonov, adjustedLatov]; + return cursorLocation; } - visualTest(event) { - console.info(this.viewerViewState); - console.info(this.camera.getWorldDirection()); - console.info("current img original global ", this.viewerImageAPI.currentImage.pos) - const loc = this.viewerAPI.toLocal(this.viewerImageAPI.currentImage.pos); - console.info("current img pos loc ", loc); - const b = this.viewerAPI.toGlobal(loc); - console.info("back to global ", b); - const bb = this.viewerAPI.toLocal(b); - console.info("and back to loc one more time ", bb); - - // visual test, spawn in white sphere at cursor position in scene - const direction = this.getCursorLocation(event); - const sphere = new THREE.SphereGeometry(1 / this.depthAtPointer(event), 10, 10); - const mesh = new THREE.Mesh(sphere, new THREE.MeshBasicMaterial()); - mesh.position.set(direction.x, direction.y, direction.z); - - if (this.testMesh != null) { - this.scene.remove(this.testMesh); - } + getRaycaster(event) { + // calculate mouse position in normalized device coordinates + // (-1 to +1) for both components + const mouse = new THREE.Vector2(); + const raycaster = new THREE.Raycaster(); - this.scene.add(mesh); + mouse.x = (event.clientX / window.innerWidth) * 2 - 1; + mouse.y = - (event.clientY / window.innerHeight) * 2 + 1; - this.testMesh = mesh; - - console.info("current sphere pos ", direction); - } -} - - -// takes in a location (in lot/lat), a direction (as a *angle*[rad, in birds eye view), and a distance (in meters) to move in the direction -const newLocationFromPointAngle = (lon1, lat1, angle, distance) => { - // angle: +-0 -> west, +pi/2 -> south, +-pi -> east, -pi/2 -> north - let lon2, lat2; - - const dx = (distance / 1000) * Math.cos(angle); - const dy = (distance / 1000) * Math.sin(angle); - - lon2 = lon1 - (dx / 71.5); - lat2 = lat1 - (dy / 111.3); - - return [lon2, lat2]; -} - -const averagePixelValues = (data) => { - const pixels = data.length / 4; - let [red, green, blue, alpha] = [0, 0, 0, 0]; // sum of all pixel values - - for (let i = 0; i < data.length; i = i + 4) { - red = red + data[i]; - green = green + data[i + 1]; - blue = blue + data[i + 2]; - alpha = alpha + data[i + 3]; + raycaster.setFromCamera(mouse, this.camera); + + return raycaster; } - - // get average by dividing - red = red / pixels; - green = green / pixels; - blue = blue / pixels; - alpha = alpha / pixels; - - return [red, green, blue, alpha]; + } // returns a normalized Vector3 pointing in the direction specified by lonov latov const lonLatToLocal = (lonov, latov) => { const phi = THREE.MathUtils.degToRad(90 - latov); const theta = THREE.MathUtils.degToRad(lonov); - + const x = Math.sin(phi) * Math.cos(theta); - const y = Math.cos(phi); - const z = Math.sin(phi) * Math.sin(theta); + const y = Math.sin(phi) * Math.sin(theta); + const z = Math.cos(phi); - return new THREE.Vector3(x, y, z); + return new THREE.Vector3(-x, -y, z); } // inverse operation to above const localToLonLat = (vec) => { - const phi = Math.acos(vec.y); - const theta = Math.atan2(vec.z, vec.x); + const phi = Math.acos(vec.z); + const theta = Math.atan2(-vec.y, -vec.x); - let latov = THREE.MathUtils.radToDeg(phi); + const latov = THREE.MathUtils.radToDeg(phi); const lonov = (THREE.MathUtils.radToDeg(theta) + 360) % 360; return [lonov, 90 - latov]; -} +} \ No newline at end of file