From 3dee9e90d721b316defc36251ba8cdaa15bbc1a1 Mon Sep 17 00:00:00 2001 From: evlist Date: Wed, 19 Oct 2022 18:35:20 +0200 Subject: [PATCH] Starting to implement a LayerStack component --- src/components/map/LayerStack.test.tsx | 264 +++++++++++++++++++++++++ src/components/map/LayerStack.tsx | 139 +++++++++++++ src/components/map/Map.tsx | 55 +----- src/components/map/TiledLayer.tsx | 31 +-- src/components/map/uris.test.ts | 9 + src/components/map/uris.ts | 21 +- 6 files changed, 455 insertions(+), 64 deletions(-) create mode 100644 src/components/map/LayerStack.test.tsx create mode 100644 src/components/map/LayerStack.tsx diff --git a/src/components/map/LayerStack.test.tsx b/src/components/map/LayerStack.test.tsx new file mode 100644 index 0000000..4f6b477 --- /dev/null +++ b/src/components/map/LayerStack.test.tsx @@ -0,0 +1,264 @@ +import { renderHook, act, render, screen } from '@testing-library/react'; +import { useAtom } from 'jotai'; +import LayerStack from './LayerStack'; + +describe('The LayerStack component', () => { + test('generates something', () => { + // const { result } = renderHook(() => useAtom(tiledLayersAtom)); + render( + + ); + const svg = screen.getByTestId('layer-stack'); + expect(svg).toMatchInlineSnapshot(` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`); + }); +}); diff --git a/src/components/map/LayerStack.tsx b/src/components/map/LayerStack.tsx new file mode 100644 index 0000000..037e72c --- /dev/null +++ b/src/components/map/LayerStack.tsx @@ -0,0 +1,139 @@ +import react, { useCallback, useEffect, useRef, useState } from 'react'; +import { atom, useAtom } from 'jotai'; + +import { TileFactory, TileKeyObject } from './types'; + +import { coordinateSystemAtom } from './Map'; +import TiledLayer from './TiledLayer'; +import Tile from './Tile'; +import _, { floor, range } from 'lodash'; +import tileUri from './uris'; + +export interface LayerStackProperties { + /** + * A key identifying the initial top left tile + */ + keyObject: TileKeyObject; + /** + * Number of {@link components/map/TiledLayer!TiledLayer}. + */ + numberOfTiledLayers?: number; +} + +/** + * + * @param props + * @returns A stack of layers embedded in an SVG element + * + * This component does the conversion between the {@link components/map/Map!CoordinateSystem} stored + * in the {@link components/map/Map!coordinateSystemAtom} atom and the {@link components/map/TiledLayer!TiledLayer} + * components which units are in tiles. + * + */ +export const LayerStack: react.FC = ( + props: LayerStackProperties +) => { + const [coordinateSystem] = useAtom(coordinateSystemAtom); + + const simpleTileFactory: TileFactory = useCallback( + (keyObject) => ( + + ), + [] + ); + + const numberOfTiledLayers = + props.numberOfTiledLayers === undefined ? 1 : props.numberOfTiledLayers; + + const [activeTiledLayer, setActiveTiledLayer] = useState( + Math.floor(numberOfTiledLayers / 2) + ); + + const viewPort = { + topLeft: { + x: Math.floor(-coordinateSystem.shift.x / coordinateSystem.zoom / 256), + y: Math.floor(-coordinateSystem.shift.y / coordinateSystem.zoom / 256), + }, + bottomRight: { + x: Math.ceil( + (-coordinateSystem.shift.x + window.innerWidth) / + coordinateSystem.zoom / + 256 + ), + y: Math.ceil( + (-coordinateSystem.shift.y + window.innerHeight) / + coordinateSystem.zoom / + 256 + ), + }, + }; + + const getTiledLayer = (i: number) => { + const relativeZoomLevel = i - activeTiledLayer; + const zoom = 2 ** relativeZoomLevel; + const origin = { + x: props.keyObject.x * zoom, + y: props.keyObject.y * zoom, + }; + const keyObject = { + provider: props.keyObject.provider, + zoomLevel: props.keyObject.zoomLevel + relativeZoomLevel, + x: Math.floor(origin.x), + y: Math.floor(origin.y), + }; + const shift = { + x: origin.x - floor(origin.x), + y: origin.y - floor(origin.y), + }; + return ( + + + + ); + }; + + const tileLayers = range(0, numberOfTiledLayers).map((i) => {}); + + // console.log(`tiledLayers: ${JSON.stringify(tiledLayers)}`); + + return ( + + + { + // Tiled layers with less detail + range(0, activeTiledLayer).map(getTiledLayer) + } + { + // Tiled layers with more details + range(numberOfTiledLayers, activeTiledLayer + 1, -1).map( + getTiledLayer + ) + } + { + // And the active one + getTiledLayer(activeTiledLayer) + } + + + ); +}; + +export default LayerStack; diff --git a/src/components/map/Map.tsx b/src/components/map/Map.tsx index ab0f240..d885fcc 100644 --- a/src/components/map/Map.tsx +++ b/src/components/map/Map.tsx @@ -1,10 +1,9 @@ -import react, { useCallback } from 'react'; +import react from 'react'; import { atom, useAtom } from 'jotai'; import Handlers from './Handlers'; -import Tile from './Tile'; -import TiledLayer from './TiledLayer'; -import { Point, TileFactory } from './types'; +import { Point } from './types'; +import LayerStack from './LayerStack'; export interface MapProperties {} @@ -75,54 +74,20 @@ export const relativeCoordinateSystemAtom = atom( /** * * @returns A Map component + * + * TODO: Is this component really useful ? + * TODO: does the coordinate system belong to this component or to `` ? */ export const Map: react.FC = (props: MapProperties) => { const [coordinateSystem, setCoordinateSystem] = useAtom(coordinateSystemAtom); - const simpleTileFactory: TileFactory = useCallback( - (keyObject) => ( - - ), - [] - ); - - const viewPort = { - topLeft: { - x: Math.floor(-coordinateSystem.shift.x / coordinateSystem.zoom / 256), - y: Math.floor(-coordinateSystem.shift.y / coordinateSystem.zoom / 256), - }, - bottomRight: { - x: Math.ceil( - (-coordinateSystem.shift.x + window.innerWidth) / - coordinateSystem.zoom / - 256 - ), - y: Math.ceil( - (-coordinateSystem.shift.y + window.innerHeight) / - coordinateSystem.zoom / - 256 - ), - }, - }; - return ( <> - - - - - - - + ); }; diff --git a/src/components/map/TiledLayer.tsx b/src/components/map/TiledLayer.tsx index 57f4021..985638c 100644 --- a/src/components/map/TiledLayer.tsx +++ b/src/components/map/TiledLayer.tsx @@ -8,7 +8,7 @@ export interface TiledLayerProperties { /** The key of the first (ie top/left) tile */ keyObject: TileKeyObject; /** The current viewport expressed in tiles coordinates */ - viewPort: Rectangle; + viewPort?: Rectangle; /** The factory to create tiles */ tileFactory: TileFactory; } @@ -19,12 +19,14 @@ export interface TiledLayerProperties { * This component is rather dumb and is mainly a sparse array of tiles. * * New tiles are added to the array when the viewport is updated and they stay in the array until - * the component is destroyed or its number of tiles is updated. + * the component is destroyed. * * This component has no need to know the number nor the size of its tiles: tiles can be added when needed and * its unit is the tile size (the parent component needs to transform its enclosing SVG group to adapt its units) * * TODO: test tiles'X and Y boundaries. + * TODO: housekeeping + * TODO: remove tileFactory to facilitate memoisation. * */ export const TiledLayer: react.FC = memo( @@ -32,10 +34,13 @@ export const TiledLayer: react.FC = memo( console.log(`Rendering TiledLayer: ${JSON.stringify(props)}`); const tiles = useRef(new Map()); - range(props.viewPort.topLeft.y, props.viewPort.bottomRight.y + 1).map( - (row) => { - range(props.viewPort.topLeft.x, props.viewPort.bottomRight.x + 1).map( - (col) => { + if (props.viewPort !== undefined) { + range(props.viewPort.topLeft.y, props.viewPort.bottomRight.y + 1).forEach( + (row) => { + range( + props.viewPort!.topLeft.x, + props.viewPort!.bottomRight.x + 1 + ).forEach((col) => { const keyObject = { provider: props.keyObject.provider, zoomLevel: props.keyObject.zoomLevel, @@ -51,14 +56,18 @@ export const TiledLayer: react.FC = memo( ); } - } - ); - } - ); + }); + } + ); + } return <>{Array.from(tiles.current.values())}; }, - isEqual + (prevProps, nextProps) => + isEqual( + [prevProps.keyObject, prevProps.viewPort], + [nextProps.keyObject, nextProps.viewPort] + ) ); export default TiledLayer; diff --git a/src/components/map/uris.test.ts b/src/components/map/uris.test.ts index 1dab357..2137260 100644 --- a/src/components/map/uris.test.ts +++ b/src/components/map/uris.test.ts @@ -13,4 +13,13 @@ describe('Test that', () => { y: 3, }); }); + test('x and y are optional', () => { + expect(tileUri({ provider: 'osm', zoomLevel: 16 })).toEqual('tile/osm/16//'); + }); + test('uri parsing works', () => { + expect(tileUri('tile/otm/5')).toEqual({ + provider: 'otm', + zoomLevel: 5, + }); + }); }); diff --git a/src/components/map/uris.ts b/src/components/map/uris.ts index 940c119..9a094b5 100644 --- a/src/components/map/uris.ts +++ b/src/components/map/uris.ts @@ -2,18 +2,23 @@ import { route } from 'docuri'; /** * A [docuri](https://github.com/jo/docuri) route for {@link components/map/types!TileKeyObject} - * + * * TODO: update docuri (or write a wrapper) to support datatyping (and formats). */ export const tileUri = (rte: any) => { - const r = route('tile/:provider/:zoomLevel/:x/:y')(rte); + const r = route('tile/:provider/:zoomLevel(/:x/:y)')(rte); if (typeof r === 'object') { - return { - provider: r.provider, - zoomLevel: parseInt(r.zoomLevel), - x: parseInt(r.x), - y: parseInt(r.y), - }; + return r.x === undefined + ? { + provider: r.provider, + zoomLevel: parseInt(r.zoomLevel), + } + : { + provider: r.provider, + zoomLevel: parseInt(r.zoomLevel), + x: parseInt(r.x), + y: parseInt(r.y), + }; } return r; };