import react, { useEffect, useState } from 'react'; import { MapScope, Point } from './types'; import Map from './Map'; import Handlers from './Handlers'; import { tileProviders } from './tile-providers'; import { lon2tile, lat2tile, tile2lat, tile2long } from '../../lib/geo'; /** * Definition of a coordinate system * * The coordinate system is shifted and zoomed (from the viewport origin) * */ export interface CoordinateSystem { /** Zoom relative to the origin */ zoom: number; /** Origin's shift (in pixels) */ shift: Point; } /** * Description of coordinates system transformation */ export interface Transformation { /** New translation to apply */ deltaShift: Point | null; /** Zoom factor to apply */ deltaZoom: number | null; /** Center of the new zoom to apply */ zoomCenter: Point | null; } export interface LiveMapProperties { /** The initial map's scope */ scope: MapScope; /** The number of tiled layers (default to 1) */ numberOfTiledLayers?: number; /** If provided, a function to call when the scope is updated. */ setScope?: (scope: MapScope) => void; } /** * * @param props * @returns A `` component. * * A `` is a wrapper around a {@link components/map/Map!Map} component which updates the ``'s scope according to user's mouse, wheel and touch events. * * To do so, `` embeds a `` component together with a {@link components/map/Handlers!Handlers} component which listens to user's event. * * The main task of `` components is thus to translate {@link Transformation}s delivered by `` in pixels into geographical coordinates. */ export const LiveMap: react.FC = ( props: LiveMapProperties ) => { const [scope, setScope] = useState(props.scope); useEffect(() => { setScope(props.scope); }, [props.scope]); console.log(`LiveMap rendering: ${JSON.stringify(scope)}`); const transform = (t: Transformation) => { const deltaZoom = t.deltaZoom === null ? 1 : t.deltaZoom; const deltaZoomLevel = Math.log2(deltaZoom); const tileProvider = tileProviders[scope.tileProvider]; const tilesZoom = Math.min( Math.max(Math.round(scope.zoom), tileProvider.minZoom), tileProvider.maxZoom ); const softZoom = scope.zoom - tilesZoom; const relativeScale = 2 ** softZoom; const visibleTileSize = tileProvider.tileSize * relativeScale; // Values in pixels const actualDeltaShift = t.deltaShift === null ? { x: 0, y: 0 } : t.deltaShift; const actualZoomCenter = t.zoomCenter === null ? { x: 0, y: 0 } : t.zoomCenter; // Values in tiles (for the current zoom level) const actualDeltaShiftTiles = { x: actualDeltaShift.x / visibleTileSize, y: actualDeltaShift.y / visibleTileSize, }; const actualZoomCenterTiles = { x: actualZoomCenter.x / visibleTileSize, y: actualZoomCenter.y / visibleTileSize, }; const tilesCenter: Point = { x: lon2tile(scope.center.lon, tilesZoom), y: lat2tile(scope.center.lat, tilesZoom), }; const newTilesCenter = { x: tilesCenter.x - actualDeltaShiftTiles.x - (window.innerWidth / 2 / visibleTileSize - actualZoomCenterTiles.x) * (deltaZoom - 1), y: tilesCenter.y - actualDeltaShiftTiles.y - (window.innerHeight / 2 / visibleTileSize - actualZoomCenterTiles.y) * (deltaZoom - 1), }; const newScope: MapScope = { center: { lat: tile2lat(newTilesCenter.y, tilesZoom), lon: tile2long(newTilesCenter.x, tilesZoom), }, zoom: deltaZoomLevel + scope.zoom, tileProvider: scope.tileProvider, }; // console.log( // `LiveMap transform: ${JSON.stringify(t)}, ${JSON.stringify( // scope // )} -> ${JSON.stringify(newScope)}, delta lat: ${ // newScope.center.lat - scope.center.lat // }, delta lon: ${newScope.center.lon - scope.center.lon}` // ); setScope(newScope); if (props.setScope !== undefined) { props.setScope(newScope); } }; return ( <> ); }; export default LiveMap;