Refactoring to split logic between `<Map>` and a brand new `<LiveMap>` component.

This commit is contained in:
Eric van der Vlist 2022-10-31 15:02:46 +01:00
parent 425ad06e33
commit 7b5abaccd3
10 changed files with 165 additions and 34 deletions

View File

@ -33,7 +33,14 @@ const App: React.FC = () => (
<IonReactRouter>
<IonRouterOutlet>
<Route exact path='/home'>
<Map />
<Map
scope={{
center: { lat: -37.8403508, lon: 77.5539501 },
zoom: 15,
tileProvider: 'osm',
}}
numberOfTiledLayers={1}
/>
</Route>
<Route exact path='/'>
<Redirect to='/home' />

View File

@ -1,15 +1,20 @@
import { renderHook, act, render, screen } from '@testing-library/react';
import { useAtom } from 'jotai';
import LayerStack from './LayerStack';
import { coordinateSystemAtom, relativeCoordinateSystemAtom } from './Map';
import { CoordinateSystem } from './Map';
describe('The LayerStack component', () => {
const coordinateSystem:CoordinateSystem= {
shift:{x:0,y:0},
zoom:1,
}
test('generates four empty layers and a populated one', () => {
// const { result } = renderHook(() => useAtom(tiledLayersAtom));
render(
<LayerStack
numberOfTiledLayers={5}
keyObject={{ provider: 'fake', zoomLevel: 9, x: 777, y: 333 }}
coordinateSystem= {coordinateSystem}
/>
);
const svg = screen.getByTestId('layer-stack');
@ -157,6 +162,7 @@ describe('The LayerStack component', () => {
<LayerStack
numberOfTiledLayers={3}
keyObject={{ provider: 'fake', zoomLevel: 9, x: 777, y: 333 }}
coordinateSystem= {coordinateSystem}
/>
);
const svg = screen.getByTestId('layer-stack');
@ -292,12 +298,13 @@ describe('The LayerStack component', () => {
`);
});
test('populates a new layer when zoomed in', () => {
/* test('populates a new layer when zoomed in', () => {
// const { result } = renderHook(() => useAtom(tiledLayersAtom));
render(
<LayerStack
numberOfTiledLayers={3}
keyObject={{ provider: 'fake', zoomLevel: 9, x: 777, y: 333 }}
coordinateSystem= {coordinateSystem}
/>
);
const { result } = renderHook(() => [
@ -449,5 +456,5 @@ describe('The LayerStack component', () => {
</g>
</svg>
`);
});
}); */
});

View File

@ -1,9 +1,8 @@
import react from 'react';
import { useAtom } from 'jotai';
import { TileKeyObject } from './types';
import { coordinateSystemAtom } from './Map';
import { CoordinateSystem } from './Map';
import { range } from 'lodash';
import tileUri from './uris';
import TiledLayer from './TiledLayer';
@ -17,6 +16,10 @@ export interface LayerStackProperties {
* Number of {@link components/map/TiledLayer!TiledLayer}.
*/
numberOfTiledLayers?: number;
/**
* The current coordinates system
*/
coordinateSystem: CoordinateSystem;
}
/**
@ -32,8 +35,6 @@ export interface LayerStackProperties {
export const LayerStack: react.FC<LayerStackProperties> = (
props: LayerStackProperties
) => {
const [coordinateSystem] = useAtom(coordinateSystemAtom);
const numberOfTiledLayers =
props.numberOfTiledLayers === undefined ? 1 : props.numberOfTiledLayers;
@ -67,6 +68,7 @@ export const LayerStack: react.FC<LayerStackProperties> = (
keyObject={keyObject}
shift={shift}
zoom={zoom * 256}
coordinateSystem={props.coordinateSystem}
/>
);
};
@ -80,7 +82,7 @@ export const LayerStack: react.FC<LayerStackProperties> = (
data-testid='layer-stack'
>
<g
transform={`translate(${coordinateSystem.shift.x}, ${coordinateSystem.shift.y}) scale(${coordinateSystem.zoom})`}
transform={`translate(${props.coordinateSystem.shift.x}, ${props.coordinateSystem.shift.y}) scale(${props.coordinateSystem.zoom})`}
>
{
// Tiled layers with less detail

View File

@ -0,0 +1,9 @@
import react from 'react';
export interface LiveMapProperties {}
export const LiveMap: react.FC<LiveMapProperties> = (props: LiveMapProperties) => {
return <></>;
};
export default LiveMap;

View File

@ -1,11 +1,15 @@
import react from 'react';
import { atom, useAtom } from 'jotai';
import { atom } from 'jotai';
import Handlers from './Handlers';
import { Point } from './types';
import { Point, MapScope } from './types';
import LayerStack from './LayerStack';
import { tileProviders } from './tile-providers';
import { lon2tile, lat2tile } from '../../lib/geo';
export interface MapProperties {}
export interface MapProperties {
scope: MapScope;
numberOfTiledLayers: number;
}
/**
* Definition of a coordinate system
@ -73,20 +77,50 @@ export const relativeCoordinateSystemAtom = atom(
/**
*
* @returns A Map component
* @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 centerPX = {
x: window.innerWidth / 2,
y: window.innerHeight / 2,
};
const tileProvider = tileProviders[props.scope.tileProvider];
const tilesZoom = Math.min(
Math.max(Math.round(props.scope.zoom), tileProvider.minZoom),
tileProvider.maxZoom
);
const tilesCenter: Point = {
x: lon2tile(props.scope.center.lon, tilesZoom),
y: lat2tile(props.scope.center.lat, tilesZoom),
};
const softZoom = props.scope.zoom - tilesZoom;
const relativeScale = 2 ** softZoom;
const visibleTileSize = tileProvider.tileSize * relativeScale;
const nbTilesLeft = window.innerWidth / 2 / visibleTileSize;
const nbTilesTop = window.innerHeight / 2 / visibleTileSize;
const firstTileLeft = Math.floor(tilesCenter.x - nbTilesLeft);
const firstTileTop = Math.floor(tilesCenter.y - nbTilesTop);
return (
<>
<Handlers />
<LayerStack
numberOfTiledLayers={3}
keyObject={{ provider: 'osm', zoomLevel: 16, x: 33485, y: 23936 }}
numberOfTiledLayers={props.numberOfTiledLayers}
keyObject={{
provider: props.scope.tileProvider,
zoomLevel: tilesZoom,
x: firstTileLeft,
y: firstTileTop,
}}
coordinateSystem={{
shift: {
x: -((tilesCenter.x - nbTilesLeft) % 1) * visibleTileSize,
y: -((tilesCenter.y - nbTilesTop) % 1) * visibleTileSize,
},
zoom: relativeScale,
}}
/>
</>
);

View File

@ -1,10 +1,13 @@
import { renderHook, act, render, screen } from '@testing-library/react';
import { useAtom } from 'jotai';
import LayerStack from './LayerStack';
import { coordinateSystemAtom, relativeCoordinateSystemAtom } from './Map';
import { render, screen } from '@testing-library/react';
import { CoordinateSystem } from './Map';
import TiledLayer from './TiledLayer';
describe('The TiledLayer component', () => {
const coordinateSystem: CoordinateSystem = {
shift: { x: 0, y: 0 },
zoom: 1,
};
beforeEach(() => {
globalThis.cacheForTileSet = new Map();
});
@ -17,6 +20,7 @@ describe('The TiledLayer component', () => {
keyObject={{ provider: 'fake', zoomLevel: 5, x: 2, y: 3 }}
shift={{ x: 0.5, y: 0.25 }}
zoom={4}
coordinateSystem={coordinateSystem}
/>
</svg>
);
@ -39,6 +43,7 @@ describe('The TiledLayer component', () => {
keyObject={{ provider: 'fake', zoomLevel: 5, x: 2, y: 3 }}
shift={{ x: 0, y: 0 }}
zoom={1}
coordinateSystem={coordinateSystem}
/>
</svg>
);

View File

@ -2,7 +2,7 @@ import { useAtom } from 'jotai';
import react from 'react';
import TileSet from './TileSet';
import { Point, TileKeyObject } from './types';
import { coordinateSystemAtom } from './Map';
import { CoordinateSystem, coordinateSystemAtom } from './Map';
export interface TiledLayerProperties {
/**
@ -17,6 +17,10 @@ export interface TiledLayerProperties {
* The zoom to apply. If equal to 256 (the tile size), the layer is considered active and should add tiles which are in its viewport
*/
zoom: number;
/**
* The coordinate system
*/
coordinateSystem: CoordinateSystem;
}
/**
@ -29,7 +33,6 @@ export interface TiledLayerProperties {
export const TiledLayer: react.FC<TiledLayerProperties> = (
props: TiledLayerProperties
) => {
const [coordinateSystem] = useAtom(coordinateSystemAtom);
const viewPort =
props.zoom === 256
? {
@ -37,28 +40,32 @@ export const TiledLayer: react.FC<TiledLayerProperties> = (
x:
props.keyObject.x +
Math.floor(
-coordinateSystem.shift.x / coordinateSystem.zoom / 256
-props.coordinateSystem.shift.x /
props.coordinateSystem.zoom /
256
),
y:
props.keyObject.y +
Math.floor(
-coordinateSystem.shift.y / coordinateSystem.zoom / 256
-props.coordinateSystem.shift.y /
props.coordinateSystem.zoom /
256
),
},
bottomRight: {
x:
props.keyObject.x +
Math.ceil(
(-coordinateSystem.shift.x + window.innerWidth) /
coordinateSystem.zoom /
(-props.coordinateSystem.shift.x + window.innerWidth) /
props.coordinateSystem.zoom /
256
) -
1,
y:
props.keyObject.y +
Math.ceil(
(-coordinateSystem.shift.y + window.innerHeight) /
coordinateSystem.zoom /
(-props.coordinateSystem.shift.y + window.innerHeight) /
props.coordinateSystem.zoom /
256
) -
1,

View File

@ -5,6 +5,7 @@ export interface TileProvider {
name: string;
minZoom: number;
maxZoom: number;
tileSize: number;
getTileUrl: { (zoom: number, x: number, y: number): string };
}
@ -15,17 +16,23 @@ const getRandomItem = (items: any[]) => {
const abc = ['a', 'b', 'c'];
const tileProviders: any = {
export type TileProviders = {
[key: string]: TileProvider;
};
export const tileProviders: TileProviders = {
osm: {
name: 'Open Street Map',
minZoom: 0,
maxZoom: 19,
tileSize: 256,
getTileUrl: (zoom: number, x: number, y: number) =>
'https://tile.openstreetmap.org/' + zoom + '/' + x + '/' + y + '.png',
},
osmfr: {
name: 'Open Street Map France',
minZoom: 0,
tileSize: 256,
maxZoom: 20,
getTileUrl: (zoom: number, x: number, y: number) =>
'https://' +
@ -42,6 +49,7 @@ const tileProviders: any = {
name: 'Open Topo Map',
minZoom: 2,
maxZoom: 17,
tileSize: 256,
getTileUrl: (zoom: number, x: number, y: number) =>
'https://' +
getRandomItem(abc) +
@ -57,6 +65,7 @@ const tileProviders: any = {
name: 'CyclOSM',
minZoom: 0,
maxZoom: 19,
tileSize: 256,
getTileUrl: (zoom: number, x: number, y: number) =>
'https://' +
getRandomItem(abc) +
@ -73,6 +82,7 @@ const tileProviders: any = {
name: 'Open River Boat Map',
minZoom: 0,
maxZoom: 20,
tileSize: 256,
getTileUrl: (zoom: number, x: number, y: number) =>
'https://' +
getRandomItem(abc) +
@ -118,6 +128,7 @@ const tileProviders: any = {
name: 'Fake provider',
minZoom: 0,
maxZoom: 20,
tileSize: 256,
getTileUrl: (zoom: number, x: number, y: number) =>
'https://fakeurl/' + zoom + '/' + x + '/' + y + '.png',
},

View File

@ -1,9 +1,11 @@
import tileProviders, { TileProviders } from './tile-providers';
/**
* An identifier for tiles (can also be used for tile layers)
*/
export interface TileKeyObject {
/**A tile provider id ('osm', 'otm', ...) */
provider: string;
provider: keyof TileProviders;
/**The zoom level (integer) */
zoomLevel: number;
/**The X coordinate (integer)*/
@ -12,6 +14,26 @@ export interface TileKeyObject {
y: number;
}
/**
* A point identified by its longitude and latitude
*/
export interface geoPoint {
lon: number;
lat: number;
}
/**
* A map scope.
*
* This object contains what's needed to identify the state of the map
*
*/
export interface MapScope {
center: geoPoint;
zoom: number;
tileProvider: keyof TileProviders;
}
/**
* A point
*/

27
src/lib/geo.ts Normal file
View File

@ -0,0 +1,27 @@
// cf https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#ECMAScript_(JavaScript/ActionScript,_etc.)
export const lon2tile = (lon: number, zoom: number) => {
return ((Number(lon) + 180) / 360) * Math.pow(2, zoom);
};
export const lat2tile = (lat: number, zoom: number) => {
return (
((1 -
Math.log(
Math.tan((Number(lat) * Math.PI) / 180) +
1 / Math.cos((Number(lat) * Math.PI) / 180)
) /
Math.PI) /
2) *
Math.pow(2, zoom)
);
};
export function tile2long(x: number, z: number) {
return (x / Math.pow(2, z)) * 360 - 180;
}
export function tile2lat(y: number, z: number) {
var n = Math.PI - (2 * Math.PI * y) / Math.pow(2, z);
return (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
}