Starting to implement a LayerStack component
This commit is contained in:
parent
4238ce0938
commit
3dee9e90d7
|
@ -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(
|
||||
<LayerStack
|
||||
numberOfTiledLayers={5}
|
||||
keyObject={{ provider: 'xxx', zoomLevel: 9, x: 777, y: 333 }}
|
||||
/>
|
||||
);
|
||||
const svg = screen.getByTestId('layer-stack');
|
||||
expect(svg).toMatchInlineSnapshot(`
|
||||
<svg
|
||||
data-testid="layer-stack"
|
||||
height="768"
|
||||
width="1024"
|
||||
>
|
||||
<g
|
||||
transform="translate(0, 0) scale(1)"
|
||||
>
|
||||
<g
|
||||
transform="scale(64) translate(0.25, 0.25)"
|
||||
/>
|
||||
<g
|
||||
transform="scale(128) translate(0.5, 0.5)"
|
||||
/>
|
||||
<g
|
||||
transform="scale(2048) translate(0, 0)"
|
||||
/>
|
||||
<g
|
||||
transform="scale(1024) translate(0, 0)"
|
||||
/>
|
||||
<g
|
||||
transform="scale(256) translate(0, 0)"
|
||||
>
|
||||
<g
|
||||
transform="translate(0, 0)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/777/333.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(1, 0)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/778/333.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(2, 0)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/779/333.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(3, 0)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/780/333.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(4, 0)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/781/333.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(0, 1)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/777/334.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(1, 1)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/778/334.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(2, 1)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/779/334.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(3, 1)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/780/334.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(4, 1)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/781/334.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(0, 2)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/777/335.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(1, 2)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/778/335.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(2, 2)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/779/335.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(3, 2)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/780/335.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(4, 2)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/781/335.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(0, 3)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/777/336.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(1, 3)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/778/336.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(2, 3)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/779/336.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(3, 3)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/780/336.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(4, 3)"
|
||||
>
|
||||
<g>
|
||||
<image
|
||||
height="1"
|
||||
href="https://tile.openstreetmap.org/9/781/336.png"
|
||||
width="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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<LayerStackProperties> = (
|
||||
props: LayerStackProperties
|
||||
) => {
|
||||
const [coordinateSystem] = useAtom(coordinateSystemAtom);
|
||||
|
||||
const simpleTileFactory: TileFactory = useCallback(
|
||||
(keyObject) => (
|
||||
<Tile
|
||||
href={`https://tile.openstreetmap.org/${keyObject.zoomLevel}/${keyObject.x}/${keyObject.y}.png`}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
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 (
|
||||
<g
|
||||
transform={`scale(${256 * zoom}) translate(${shift.x}, ${shift.y})`}
|
||||
key={tileUri({
|
||||
provider: keyObject.provider,
|
||||
zoomLevel: keyObject.zoomLevel,
|
||||
})}
|
||||
>
|
||||
<TiledLayer
|
||||
keyObject={keyObject}
|
||||
viewPort={i === activeTiledLayer ? viewPort : undefined}
|
||||
tileFactory={simpleTileFactory}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const tileLayers = range(0, numberOfTiledLayers).map((i) => {});
|
||||
|
||||
// console.log(`tiledLayers: ${JSON.stringify(tiledLayers)}`);
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={window.innerWidth}
|
||||
height={window.innerHeight}
|
||||
data-testid='layer-stack'
|
||||
>
|
||||
<g
|
||||
transform={`translate(${coordinateSystem.shift.x}, ${coordinateSystem.shift.y}) scale(${coordinateSystem.zoom})`}
|
||||
>
|
||||
{
|
||||
// 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)
|
||||
}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayerStack;
|
|
@ -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 `<LayerStack>` ?
|
||||
*/
|
||||
export const Map: react.FC<MapProperties> = (props: MapProperties) => {
|
||||
const [coordinateSystem, setCoordinateSystem] = useAtom(coordinateSystemAtom);
|
||||
|
||||
const simpleTileFactory: TileFactory = useCallback(
|
||||
(keyObject) => (
|
||||
<Tile
|
||||
href={`https://tile.openstreetmap.org/${keyObject.zoomLevel}/${keyObject.x}/${keyObject.y}.png`}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Handlers />
|
||||
<svg width={window.innerWidth} height={window.innerHeight}>
|
||||
<g
|
||||
transform={`translate(${coordinateSystem.shift.x}, ${coordinateSystem.shift.y}) scale(${coordinateSystem.zoom})`}
|
||||
>
|
||||
<g transform='scale(256)'>
|
||||
<TiledLayer
|
||||
<LayerStack
|
||||
numberOfTiledLayers={3}
|
||||
keyObject={{ provider: 'osm', zoomLevel: 16, x: 33485, y: 23936 }}
|
||||
viewPort={viewPort}
|
||||
tileFactory={simpleTileFactory}
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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<TiledLayerProperties> = memo(
|
||||
|
@ -32,10 +34,13 @@ export const TiledLayer: react.FC<TiledLayerProperties> = memo(
|
|||
console.log(`Rendering TiledLayer: ${JSON.stringify(props)}`);
|
||||
const tiles = useRef<any>(new Map());
|
||||
|
||||
range(props.viewPort.topLeft.y, props.viewPort.bottomRight.y + 1).map(
|
||||
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).map(
|
||||
(col) => {
|
||||
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<TiledLayerProperties> = memo(
|
|||
</g>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return <>{Array.from(tiles.current.values())}</>;
|
||||
},
|
||||
isEqual
|
||||
(prevProps, nextProps) =>
|
||||
isEqual(
|
||||
[prevProps.keyObject, prevProps.viewPort],
|
||||
[nextProps.keyObject, nextProps.viewPort]
|
||||
)
|
||||
);
|
||||
|
||||
export default TiledLayer;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,9 +6,14 @@ import { route } from 'docuri';
|
|||
* 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 {
|
||||
return r.x === undefined
|
||||
? {
|
||||
provider: r.provider,
|
||||
zoomLevel: parseInt(r.zoomLevel),
|
||||
}
|
||||
: {
|
||||
provider: r.provider,
|
||||
zoomLevel: parseInt(r.zoomLevel),
|
||||
x: parseInt(r.x),
|
||||
|
|
Loading…
Reference in New Issue