import { CoordinateSystem } from "./CoordinateSystem.js"; /** * GeoreferenceManager for working with geographic coordinates in OpenLIME * Handles conversions between geographic coordinates (EPSG:4326/WGS84) and * Web Mercator (EPSG:3857) and managing map navigation. */ class GeoreferenceManager { /** * Creates a new GeoreferenceManager * @param {Object} viewer - OpenLIME Viewer instance * @param {Object} layer - Layer containing the geographic image */ constructor(viewer, layer) { if (!viewer || !layer) { throw new Error('Viewer and layer are required'); } this.viewer = viewer; this.camera = viewer.camera; this.layer = layer; this.earthRadius = 6378137; // Earth radius in meters (WGS84) this.imageSize = Math.max(this.layer.width, this.layer.height); // Define zoom constraints this.minZoom = 0; // Minimum zoom level this.maxZoom = 19; // Maximum zoom level (from OSM) // Set up camera position methods this.setupViewer(); } /** * Configures the viewer with geographic navigation methods * @private */ setupViewer() { // Add method to navigate to geographic coordinates this.camera.setGeoPosition = (lat, lon, zoom) => { const sceneCoord = this.geoToScene(lat, lon); // Constrain zoom to valid range const constrainedZoom = Math.min(this.maxZoom, Math.max(this.minZoom, zoom)); const z = constrainedZoom !== undefined ? 1 / Math.pow(2, constrainedZoom) : this.camera.getCurrentTransform(performance.now()).z; // Notice we need to negate the coordinates and scale by z this.camera.setPosition(250, -sceneCoord.x * z, -sceneCoord.y * z, z, 0); }; // Add method to get current geographic position this.camera.getGeoPosition = () => { const transform = this.camera.getCurrentTransform(performance.now()); // Need to negate and unscale coordinates before converting to geo const geo = this.sceneToGeo(-transform.x / transform.z, -transform.y / transform.z); // Calculate zoom level and ensure it's within valid range const rawZoom = Math.log2(1 / transform.z); const constrainedZoom = Math.min(this.maxZoom, Math.max(this.minZoom, rawZoom)); return { lat: geo.lat, lon: geo.lon, zoom: constrainedZoom }; }; } /** * Converts WGS84 (EPSG:4326) coordinates to Web Mercator (EPSG:3857) * @param {number} lat - Latitude in degrees * @param {number} lon - Longitude in degrees * @returns {Object} Point in Web Mercator coordinates {x, y} */ geoToWebMercator(lat, lon) { // Clamp latitude to avoid singularity at poles lat = Math.max(Math.min(lat, 85.051129), -85.051129); // Convert latitude and longitude to radians const latRad = lat * Math.PI / 180; const lonRad = lon * Math.PI / 180; // Calculate Web Mercator coordinates const x = this.earthRadius * lonRad; const y = this.earthRadius * Math.log(Math.tan(Math.PI / 4 + latRad / 2)); return { x, y }; } /** * Converts Web Mercator (EPSG:3857) coordinates to WGS84 (EPSG:4326) * @param {number} x - X coordinate in Web Mercator * @param {number} y - Y coordinate in Web Mercator * @returns {Object} Geographic coordinates {lat, lon} in degrees */ webMercatorToGeo(x, y) { // Convert Web Mercator coordinates to latitude and longitude const lonRad = x / this.earthRadius; const latRad = 2 * Math.atan(Math.exp(y / this.earthRadius)) - Math.PI / 2; // Convert radians to degrees const lon = lonRad * 180 / Math.PI; const lat = latRad * 180 / Math.PI; return { lat, lon }; } /** * Converts Web Mercator coordinates to Scene coordinates * @param {number} x - X coordinate in Web Mercator * @param {number} y - Y coordinate in Web Mercator * @returns {Object} Scene coordinates {x, y} */ webMercatorToScene(x, y) { // Scale from Web Mercator to the scene coordinate system // Map is centered at 0,0 in scene coordinates const maxMercator = Math.PI * this.earthRadius; const scaleFactor = this.layer.width / (2 * maxMercator); return { x: x * scaleFactor, y: y * scaleFactor }; } /** * Converts scene coordinates to Web Mercator * @param {number} x - X coordinate in scene space * @param {number} y - Y coordinate in scene space * @returns {Object} Web Mercator coordinates {x, y} */ sceneToWebMercator(x, y) { // Scale from scene coordinate system to Web Mercator const maxMercator = Math.PI * this.earthRadius; const scaleFactor = (2 * maxMercator) / this.layer.width; return { x: x * scaleFactor, y: y * scaleFactor }; } /** * Converts WGS84 coordinates to scene coordinates * @param {number} lat - Latitude in degrees * @param {number} lon - Longitude in degrees * @returns {Object} Scene coordinates {x, y} */ geoToScene(lat, lon) { // Convert from WGS84 to Web Mercator const mercator = this.geoToWebMercator(lat, lon); // Convert from Web Mercator to scene coordinates return this.webMercatorToScene(mercator.x, mercator.y); } /** * Converts scene coordinates to WGS84 (EPSG:4326) coordinates * @param {number} x - X coordinate in scene space * @param {number} y - Y coordinate in scene space * @returns {Object} Geographic coordinates {lat, lon} in degrees */ sceneToGeo(x, y) { // Convert from scene coordinates to Web Mercator const mercator = this.sceneToWebMercator(x, y); // Convert from Web Mercator to WGS84 return this.webMercatorToGeo(mercator.x, mercator.y); } /** * Converts canvas HTML coordinates to WGS84 coordinates * @param {number} x - X coordinate in canvas * @param {number} y - Y coordinate in canvas * @returns {Object} Geographic coordinates {lat, lon} in degrees */ canvasToGeo(x, y) { // Convert canvas coordinates to scene coordinates const sceneCoord = CoordinateSystem.fromCanvasHtmlToScene( { x, y }, this.camera, true ); // Convert scene coordinates to geographic coordinates return this.sceneToGeo(sceneCoord.x, sceneCoord.y); } /** * Navigate to a geographic position with animation * @param {number} lat - Latitude in degrees * @param {number} lon - Longitude in degrees * @param {number} [zoom] - Zoom level (optional) * @param {number} [duration=250] - Animation duration in ms * @param {string} [easing='linear'] - Easing function */ flyTo(lat, lon, zoom, duration = 500, easing = 'linear') { if (!this.viewer || !this.camera) { throw new Error('Viewer not initialized'); } const sceneCoord = this.geoToScene(lat, lon); // Constrain zoom to valid range const constrainedZoom = Math.min(this.maxZoom, Math.max(this.minZoom, zoom)); const z = 1.0 / Math.pow(2, constrainedZoom); // Note that we use negative coordinates because the camera transform works that way this.camera.setPosition(duration, -sceneCoord.x * z, -sceneCoord.y * z, z, 0, easing); } /** * Gets the current geographic position and zoom * @returns {Object} Current position {lat, lon, zoom} */ getCurrentPosition() { const transform = this.camera.getCurrentTransform(performance.now()); const geo = this.sceneToGeo(-transform.x / transform.z, -transform.y / transform.z); // Calculate zoom level and ensure it's within valid range const rawZoom = Math.log2(1 / transform.z); const constrainedZoom = Math.min(this.maxZoom, Math.max(this.minZoom, rawZoom)); return { lat: geo.lat, lon: geo.lon, zoom: constrainedZoom }; } } export { GeoreferenceManager }