Trying to simplify.

This commit is contained in:
Eric van der Vlist 2022-10-30 20:18:21 +01:00
parent 7f9400b395
commit edca3f0d1b
9 changed files with 545 additions and 430 deletions

View File

@ -9,7 +9,7 @@ describe('The LayerStack component', () => {
render( render(
<LayerStack <LayerStack
numberOfTiledLayers={5} numberOfTiledLayers={5}
keyObject={{ provider: 'xxx', zoomLevel: 9, x: 777, y: 333 }} keyObject={{ provider: 'fake', zoomLevel: 9, x: 777, y: 333 }}
/> />
); );
const svg = screen.getByTestId('layer-stack'); const svg = screen.getByTestId('layer-stack');
@ -273,7 +273,7 @@ describe('The LayerStack component', () => {
render( render(
<LayerStack <LayerStack
numberOfTiledLayers={3} numberOfTiledLayers={3}
keyObject={{ provider: 'xxx', zoomLevel: 9, x: 777, y: 333 }} keyObject={{ provider: 'fake', zoomLevel: 9, x: 777, y: 333 }}
/> />
); );
const svg = screen.getByTestId('layer-stack'); const svg = screen.getByTestId('layer-stack');
@ -529,7 +529,7 @@ describe('The LayerStack component', () => {
render( render(
<LayerStack <LayerStack
numberOfTiledLayers={3} numberOfTiledLayers={3}
keyObject={{ provider: 'xxx', zoomLevel: 9, x: 777, y: 333 }} keyObject={{ provider: 'fake', zoomLevel: 9, x: 777, y: 333 }}
/> />
); );
const { result } = renderHook(() => [ const { result } = renderHook(() => [

View File

@ -1,11 +1,10 @@
import react, { useCallback, useEffect, useRef, useState } from 'react'; import react from 'react';
import { atom, useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { TileFactory, TileKeyObject } from './types'; import { TileKeyObject } from './types';
import { coordinateSystemAtom } from './Map'; import { coordinateSystemAtom } from './Map';
import TiledLayer from './TiledLayer'; import TileSet from './TileSet';
import Tile from './Tile';
import _, { floor, range } from 'lodash'; import _, { floor, range } from 'lodash';
import tileUri from './uris'; import tileUri from './uris';
@ -49,24 +48,34 @@ export const LayerStack: react.FC<LayerStackProperties> = (
const viewPort = { const viewPort = {
topLeft: { topLeft: {
x: Math.floor(-coordinateSystem.shift.x / coordinateSystem.zoom / 256), x:
y: Math.floor(-coordinateSystem.shift.y / coordinateSystem.zoom / 256), props.keyObject.x +
Math.floor(-coordinateSystem.shift.x / coordinateSystem.zoom / 256),
y:
props.keyObject.y +
Math.floor(-coordinateSystem.shift.y / coordinateSystem.zoom / 256),
}, },
bottomRight: { bottomRight: {
x: Math.ceil( x:
(-coordinateSystem.shift.x + window.innerWidth) / props.keyObject.x +
coordinateSystem.zoom / Math.ceil(
256 (-coordinateSystem.shift.x + window.innerWidth) /
), coordinateSystem.zoom /
y: Math.ceil( 256
(-coordinateSystem.shift.y + window.innerHeight) / ) -
coordinateSystem.zoom / 1,
256 y:
), props.keyObject.y +
Math.ceil(
(-coordinateSystem.shift.y + window.innerHeight) /
coordinateSystem.zoom /
256
) -
1,
}, },
}; };
const getTiledLayer = (i: number) => { const getTileSet = (i: number) => {
const relativeZoomLevel = i - Math.floor(numberOfTiledLayers / 2); const relativeZoomLevel = i - Math.floor(numberOfTiledLayers / 2);
const zoom = 2 ** relativeZoomLevel; const zoom = 2 ** relativeZoomLevel;
const origin = { const origin = {
@ -90,11 +99,13 @@ export const LayerStack: react.FC<LayerStackProperties> = (
return ( return (
<g <g
transform={`scale(${256 / zoom}) translate(${shift.x}, ${shift.y})`} transform={`scale(${256 / zoom}) translate(${shift.x - keyObject.x}, ${
shift.y - keyObject.y
})`}
key={key} key={key}
data-testid={key} data-testid={key}
> >
<TiledLayer <TileSet
key={key} key={key}
keyObject={keyObject} keyObject={keyObject}
viewPort={i === activeTiledLayer ? viewPort : undefined} viewPort={i === activeTiledLayer ? viewPort : undefined}
@ -116,17 +127,15 @@ export const LayerStack: react.FC<LayerStackProperties> = (
> >
{ {
// Tiled layers with less detail // Tiled layers with less detail
range(0, activeTiledLayer).map(getTiledLayer) range(0, activeTiledLayer).map(getTileSet)
} }
{ {
// Tiled layers with more details // Tiled layers with more details
range(numberOfTiledLayers - 1, activeTiledLayer, -1).map( range(numberOfTiledLayers - 1, activeTiledLayer, -1).map(getTileSet)
getTiledLayer
)
} }
{ {
// And the active one // And the active one
getTiledLayer(activeTiledLayer) getTileSet(activeTiledLayer)
} }
</g> </g>
</svg> </svg>

View File

@ -1,11 +1,18 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import Tile from './Tile'; import Tile from './Tile';
import { TileKeyObject } from './types';
describe('The Tile component ', () => { describe('The Tile component ', () => {
const keyObject: TileKeyObject = {
provider: 'fake',
zoomLevel: 10,
x: 1,
y: 2,
};
test('Is initially empty', () => { test('Is initially empty', () => {
const { baseElement } = render( const { baseElement } = render(
<svg> <svg>
<Tile href='http://fakeurl' delay={10000} /> <Tile keyObject={keyObject} delay={10000} />
</svg> </svg>
); );
// screen.debug(); // screen.debug();
@ -13,7 +20,9 @@ describe('The Tile component ', () => {
<body> <body>
<div> <div>
<svg> <svg>
<g /> <g
transform="translate(1, 2)"
/>
</svg> </svg>
</div> </div>
</body> </body>
@ -22,7 +31,7 @@ describe('The Tile component ', () => {
test('Gets its image immediately with a fake URL', () => { test('Gets its image immediately with a fake URL', () => {
const { baseElement } = render( const { baseElement } = render(
<svg> <svg>
<Tile href='http://fakeurl' /> <Tile keyObject={keyObject} />
</svg> </svg>
); );
// screen.debug(); // screen.debug();
@ -30,10 +39,12 @@ describe('The Tile component ', () => {
<body> <body>
<div> <div>
<svg> <svg>
<g> <g
transform="translate(1, 2)"
>
<image <image
height="1" height="1"
href="http://fakeurl" href="https://fakeurl/10/1/2.png"
width="1" width="1"
/> />
</g> </g>

View File

@ -1,9 +1,11 @@
import react, { memo, useEffect, useRef } from 'react'; import react, { memo, useEffect, useRef } from 'react';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { TileKeyObject } from './types';
import { getTileUrl } from './tile-providers';
export interface TileProperties { export interface TileProperties {
/** The image's source URL */ /** The image's source URL */
href: string; keyObject: TileKeyObject;
/** A delay to add (for test/debug purposes) */ /** A delay to add (for test/debug purposes) */
delay?: number; delay?: number;
} }
@ -27,14 +29,15 @@ export const Tile: react.FC<TileProperties> = memo((props: TileProperties) => {
const timeout = (ms: number) => { const timeout = (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
}; };
// console.log(`Rendering tile: ${props.href}`); // console.log(`Rendering tile: ${JSON.stringify(props)}`);
useEffect(() => { useEffect(() => {
const loadImage = async () => { const loadImage = async () => {
// console.log(`Pre loading: ${props.href}`); // console.log(`Pre loading: ${props.href}`);
const href = getTileUrl(props.keyObject);
const image = new Image(1, 1); const image = new Image(1, 1);
image.loading = 'eager'; image.loading = 'eager';
// @ts-ignore // @ts-ignore
image.setAttribute('href', props.href); image.setAttribute('href', href);
if (!image.complete) { if (!image.complete) {
await image.decode(); await image.decode();
} }
@ -48,13 +51,18 @@ export const Tile: react.FC<TileProperties> = memo((props: TileProperties) => {
svgImage.setAttribute('width', '1'); svgImage.setAttribute('width', '1');
svgImage.setAttribute('height', '1'); svgImage.setAttribute('height', '1');
// @ts-ignore // @ts-ignore
svgImage.setAttribute('href', props.href); svgImage.setAttribute('href', href);
g.current?.replaceChildren(svgImage); g.current?.replaceChildren(svgImage);
}; };
loadImage(); loadImage();
}, [props.href]); }, [props.keyObject]);
return <g ref={g} />; return (
<g
ref={g}
transform={`translate(${props.keyObject.x}, ${props.keyObject.y})`}
/>
);
}, isEqual); }, isEqual);
export default Tile; export default Tile;

View File

@ -0,0 +1,309 @@
import { render } from '@testing-library/react';
import TileSet from './TileSet';
describe('The TiledLayer component ', () => {
beforeEach(() => {
globalThis.cacheForTileSet = new Map();
});
test('exposes the tiles needed per its viewport', () => {
const { baseElement } = render(
<svg>
<TileSet
keyObject={{ provider: 'fake', zoomLevel: 10, x: 5, y: 8 }}
viewPort={{ topLeft: { x: 1, y: 2 }, bottomRight: { x: 3, y: 2 } }}
/>
</svg>
);
// screen.debug();
expect(baseElement).toMatchInlineSnapshot(`
<body>
<div>
<svg>
<g
transform="translate(1, 2)"
>
<image
height="1"
href="https://fakeurl/10/1/2.png"
width="1"
/>
</g>
<g
transform="translate(2, 2)"
>
<image
height="1"
href="https://fakeurl/10/2/2.png"
width="1"
/>
</g>
<g
transform="translate(3, 2)"
>
<image
height="1"
href="https://fakeurl/10/3/2.png"
width="1"
/>
</g>
</svg>
</div>
</body>
`);
});
test('adds more tiles when its viewport is updated without removing the previous ones', () => {
const { baseElement, rerender } = render(
<svg>
<TileSet
keyObject={{ provider: 'fake', zoomLevel: 10, x: 5, y: 8 }}
viewPort={{ topLeft: { x: 1, y: 2 }, bottomRight: { x: 3, y: 2 } }}
/>
</svg>
);
rerender(
<svg>
<TileSet
keyObject={{ provider: 'fake', zoomLevel: 10, x: 5, y: 8 }}
viewPort={{ topLeft: { x: 5, y: 0 }, bottomRight: { x: 5, y: 2 } }}
/>
</svg>
);
// screen.debug();
expect(baseElement).toMatchInlineSnapshot(`
<body>
<div>
<svg>
<g
transform="translate(1, 2)"
>
<image
height="1"
href="https://fakeurl/10/1/2.png"
width="1"
/>
</g>
<g
transform="translate(2, 2)"
>
<image
height="1"
href="https://fakeurl/10/2/2.png"
width="1"
/>
</g>
<g
transform="translate(3, 2)"
>
<image
height="1"
href="https://fakeurl/10/3/2.png"
width="1"
/>
</g>
<g
transform="translate(5, 0)"
>
<image
height="1"
href="https://fakeurl/10/5/0.png"
width="1"
/>
</g>
<g
transform="translate(5, 1)"
>
<image
height="1"
href="https://fakeurl/10/5/1.png"
width="1"
/>
</g>
<g
transform="translate(5, 2)"
>
<image
height="1"
href="https://fakeurl/10/5/2.png"
width="1"
/>
</g>
</svg>
</div>
</body>
`);
});
test('is not reinitialized if its key isObject updated', () => {
const { baseElement, rerender } = render(
<svg>
<TileSet
keyObject={{ provider: 'fake', zoomLevel: 10, x: 5, y: 8 }}
viewPort={{ topLeft: { x: 1, y: 2 }, bottomRight: { x: 3, y: 2 } }}
/>
</svg>
);
rerender(
<svg>
<TileSet
keyObject={{ provider: 'fake', zoomLevel: 10, x: 4, y: 10 }}
viewPort={{ topLeft: { x: 5, y: 0 }, bottomRight: { x: 5, y: 2 } }}
/>
</svg>
);
// screen.debug();
expect(baseElement).toMatchInlineSnapshot(`
<body>
<div>
<svg>
<g
transform="translate(1, 2)"
>
<image
height="1"
href="https://fakeurl/10/1/2.png"
width="1"
/>
</g>
<g
transform="translate(2, 2)"
>
<image
height="1"
href="https://fakeurl/10/2/2.png"
width="1"
/>
</g>
<g
transform="translate(3, 2)"
>
<image
height="1"
href="https://fakeurl/10/3/2.png"
width="1"
/>
</g>
<g
transform="translate(5, 0)"
>
<image
height="1"
href="https://fakeurl/10/5/0.png"
width="1"
/>
</g>
<g
transform="translate(5, 1)"
>
<image
height="1"
href="https://fakeurl/10/5/1.png"
width="1"
/>
</g>
<g
transform="translate(5, 2)"
>
<image
height="1"
href="https://fakeurl/10/5/2.png"
width="1"
/>
</g>
</svg>
</div>
</body>
`);
});
test('Also works with negative coordinates', () => {
const { baseElement } = render(
<svg>
<TileSet
keyObject={{ provider: 'fake', zoomLevel: 10, x: 5, y: 8 }}
viewPort={{ topLeft: { x: -3, y: -1 }, bottomRight: { x: -2, y: 2 } }}
/>
</svg>
);
// screen.debug();
expect(baseElement).toMatchInlineSnapshot(`
<body>
<div>
<svg>
<g
transform="translate(-3, -1)"
>
<image
height="1"
href="https://fakeurl/10/1021/1023.png"
width="1"
/>
</g>
<g
transform="translate(-2, -1)"
>
<image
height="1"
href="https://fakeurl/10/1022/1023.png"
width="1"
/>
</g>
<g
transform="translate(-3, 0)"
>
<image
height="1"
href="https://fakeurl/10/1021/0.png"
width="1"
/>
</g>
<g
transform="translate(-2, 0)"
>
<image
height="1"
href="https://fakeurl/10/1022/0.png"
width="1"
/>
</g>
<g
transform="translate(-3, 1)"
>
<image
height="1"
href="https://fakeurl/10/1021/1.png"
width="1"
/>
</g>
<g
transform="translate(-2, 1)"
>
<image
height="1"
href="https://fakeurl/10/1022/1.png"
width="1"
/>
</g>
<g
transform="translate(-3, 2)"
>
<image
height="1"
href="https://fakeurl/10/1021/2.png"
width="1"
/>
</g>
<g
transform="translate(-2, 2)"
>
<image
height="1"
href="https://fakeurl/10/1022/2.png"
width="1"
/>
</g>
</svg>
</div>
</body>
`);
});
});

View File

@ -1,9 +1,9 @@
import react, { memo, useRef } from 'react'; import react, { } from 'react';
import { range, isEqual } from 'lodash'; import { range } from 'lodash';
import { Rectangle, TileFactory, TileKeyObject } from './types'; import { Rectangle, TileKeyObject } from './types';
import tileUri from './uris'; import tileUri from './uris';
import tileFactory from './tile-factory'; import Tile from './Tile';
/** /**
* @hidden * @hidden
@ -15,22 +15,22 @@ export const thisIsAModule = true;
*/ */
declare global { declare global {
var cacheForTiledLayer: any; var cacheForTileSet: any;
} }
//export {}; //export {};
globalThis.cacheForTiledLayer = new Map(); globalThis.cacheForTileSet = new Map();
export interface TiledLayerProperties { export interface TileSetProperties {
/** The key of the first (ie top/left) tile */ /** A partial Tile key object specifying the provider and zoom level */
keyObject: TileKeyObject; keyObject: TileKeyObject;
/** The current viewport expressed in tiles coordinates */ /** The current viewport expressed in tiles coordinates */
viewPort?: Rectangle; viewPort?: Rectangle;
} }
/** /**
* A lazily loaded layer of tiles. * A lazily loaded set of tiles.
* *
* This component is rather dumb and is mainly a sparse array of tiles. * This component is rather dumb and is mainly a sparse array of tiles.
* *
@ -49,13 +49,10 @@ export interface TiledLayerProperties {
* *
* TODO: cache housekeeping * TODO: cache housekeeping
* *
* TODO: test tiles'X and Y boundaries.
*
* TODO: remove tileFactory to facilitate memoisation.
* *
*/ */
export const TiledLayer: react.FC<TiledLayerProperties> = ( export const TileSet: react.FC<TileSetProperties> = (
props: TiledLayerProperties props: TileSetProperties
) => { ) => {
// console.log(`Rendering TiledLayer: ${JSON.stringify(props)}`); // console.log(`Rendering TiledLayer: ${JSON.stringify(props)}`);
@ -64,7 +61,7 @@ export const TiledLayer: react.FC<TiledLayerProperties> = (
zoomLevel: props.keyObject.zoomLevel, zoomLevel: props.keyObject.zoomLevel,
}); });
const tiles: any = globalThis.cacheForTiledLayer.get(key) ?? new Map(); const tiles: any = globalThis.cacheForTileSet.get(key) ?? new Map();
if (props.viewPort !== undefined) { if (props.viewPort !== undefined) {
range(props.viewPort.topLeft.y, props.viewPort.bottomRight.y + 1).forEach( range(props.viewPort.topLeft.y, props.viewPort.bottomRight.y + 1).forEach(
(row) => { (row) => {
@ -82,18 +79,24 @@ export const TiledLayer: react.FC<TiledLayerProperties> = (
if (!tiles.has(key)) { if (!tiles.has(key)) {
tiles.set( tiles.set(
key, key,
<g key={key} transform={`translate(${col}, ${row})`}> <Tile
{tileFactory(keyObject)} key={key}
</g> keyObject={{
provider: props.keyObject.provider,
zoomLevel: props.keyObject.zoomLevel,
x: col,
y: row,
}}
/>
); );
} }
}); });
} }
); );
globalThis.cacheForTiledLayer.set(key, tiles); globalThis.cacheForTileSet.set(key, tiles);
} }
return <>{Array.from(tiles.values())}</>; return <>{Array.from(tiles.values())}</>;
}; };
export default TiledLayer; export default TileSet;

View File

@ -1,356 +0,0 @@
import { render } from '@testing-library/react';
import TiledLayer from './TiledLayer';
import { TileFactory } from './types';
describe('The TiledLayer component ', () => {
beforeEach(() => {
globalThis.cacheForTiledLayer = new Map();
});
test('exposes the tiles needed per its viewport', () => {
const { baseElement } = render(
<svg>
<TiledLayer
keyObject={{ provider: 'osm', zoomLevel: 10, x: 5, y: 8 }}
viewPort={{ topLeft: { x: 1, y: 2 }, bottomRight: { x: 3, y: 2 } }}
/>
</svg>
);
// screen.debug();
expect(baseElement).toMatchInlineSnapshot(`
<body>
<div>
<svg>
<g
transform="translate(1, 2)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/6/10.png"
width="1"
/>
</g>
</g>
<g
transform="translate(2, 2)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/7/10.png"
width="1"
/>
</g>
</g>
<g
transform="translate(3, 2)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/8/10.png"
width="1"
/>
</g>
</g>
</svg>
</div>
</body>
`);
});
test('adds more tiles when its viewport is updated without removing the previous ones', () => {
const { baseElement, rerender } = render(
<svg>
<TiledLayer
keyObject={{ provider: 'osm', zoomLevel: 10, x: 5, y: 8 }}
viewPort={{ topLeft: { x: 1, y: 2 }, bottomRight: { x: 3, y: 2 } }}
/>
</svg>
);
rerender(
<svg>
<TiledLayer
keyObject={{ provider: 'osm', zoomLevel: 10, x: 5, y: 8 }}
viewPort={{ topLeft: { x: 5, y: 0 }, bottomRight: { x: 5, y: 2 } }}
/>
</svg>
);
// screen.debug();
expect(baseElement).toMatchInlineSnapshot(`
<body>
<div>
<svg>
<g
transform="translate(1, 2)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/6/10.png"
width="1"
/>
</g>
</g>
<g
transform="translate(2, 2)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/7/10.png"
width="1"
/>
</g>
</g>
<g
transform="translate(3, 2)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/8/10.png"
width="1"
/>
</g>
</g>
<g
transform="translate(5, 0)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/10/8.png"
width="1"
/>
</g>
</g>
<g
transform="translate(5, 1)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/10/9.png"
width="1"
/>
</g>
</g>
<g
transform="translate(5, 2)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/10/10.png"
width="1"
/>
</g>
</g>
</svg>
</div>
</body>
`);
});
test('is not reinitialized if its key isObject updated', () => {
const { baseElement, rerender } = render(
<svg>
<TiledLayer
keyObject={{ provider: 'osm', zoomLevel: 10, x: 5, y: 8 }}
viewPort={{ topLeft: { x: 1, y: 2 }, bottomRight: { x: 3, y: 2 } }}
/>
</svg>
);
rerender(
<svg>
<TiledLayer
keyObject={{ provider: 'osm', zoomLevel: 10, x: 4, y: 10 }}
viewPort={{ topLeft: { x: 5, y: 0 }, bottomRight: { x: 5, y: 2 } }}
/>
</svg>
);
// screen.debug();
expect(baseElement).toMatchInlineSnapshot(`
<body>
<div>
<svg>
<g
transform="translate(1, 2)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/6/10.png"
width="1"
/>
</g>
</g>
<g
transform="translate(2, 2)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/7/10.png"
width="1"
/>
</g>
</g>
<g
transform="translate(3, 2)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/8/10.png"
width="1"
/>
</g>
</g>
<g
transform="translate(5, 0)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/9/10.png"
width="1"
/>
</g>
</g>
<g
transform="translate(5, 1)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/9/11.png"
width="1"
/>
</g>
</g>
<g
transform="translate(5, 2)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/9/12.png"
width="1"
/>
</g>
</g>
</svg>
</div>
</body>
`);
});
test('Also works with negative coordinates', () => {
const { baseElement } = render(
<svg>
<TiledLayer
keyObject={{ provider: 'osm', zoomLevel: 10, x: 5, y: 8 }}
viewPort={{ topLeft: { x: -3, y: -1 }, bottomRight: { x: -2, y: 2 } }}
/>
</svg>
);
// screen.debug();
expect(baseElement).toMatchInlineSnapshot(`
<body>
<div>
<svg>
<g
transform="translate(-3, -1)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/2/7.png"
width="1"
/>
</g>
</g>
<g
transform="translate(-2, -1)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/3/7.png"
width="1"
/>
</g>
</g>
<g
transform="translate(-3, 0)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/2/8.png"
width="1"
/>
</g>
</g>
<g
transform="translate(-2, 0)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/3/8.png"
width="1"
/>
</g>
</g>
<g
transform="translate(-3, 1)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/2/9.png"
width="1"
/>
</g>
</g>
<g
transform="translate(-2, 1)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/3/9.png"
width="1"
/>
</g>
</g>
<g
transform="translate(-3, 2)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/2/10.png"
width="1"
/>
</g>
</g>
<g
transform="translate(-2, 2)"
>
<g>
<image
height="1"
href="https://tile.openstreetmap.org/10/3/10.png"
width="1"
/>
</g>
</g>
</svg>
</div>
</body>
`);
});
});

View File

@ -1,15 +0,0 @@
import Tile from './Tile';
import { TileFactory, TileKeyObject } from './types';
/**
*
* @param keyObject The tile identifier
* @returns The `<Tile>` component
*/
export const tileFactory: TileFactory = (keyObject:TileKeyObject) => (
<Tile
href={`https://tile.openstreetmap.org/${keyObject.zoomLevel}/${keyObject.x}/${keyObject.y}.png`}
/>
);
export default tileFactory;

View File

@ -0,0 +1,146 @@
import Tile from './Tile';
import { TileFactory, TileKeyObject } from './types';
export interface TileProvider {
name: string;
minZoom: number;
maxZoom: number;
getTileUrl: { (zoom: number, x: number, y: number): string };
}
const getRandomItem = (items: any[]) => {
const idx = Math.floor(Math.random() * items.length);
return items[idx];
};
const abc = ['a', 'b', 'c'];
const tileProviders: any = {
osm: {
name: 'Open Street Map',
minZoom: 0,
maxZoom: 19,
getTileUrl: (zoom: number, x: number, y: number) =>
'https://tile.openstreetmap.org/' + zoom + '/' + x + '/' + y + '.png',
},
osmfr: {
name: 'Open Street Map France',
minZoom: 0,
maxZoom: 20,
getTileUrl: (zoom: number, x: number, y: number) =>
'https://' +
getRandomItem(abc) +
'.tile.openstreetmap.fr/osmfr/' +
zoom +
'/' +
x +
'/' +
y +
'.png',
},
otm: {
name: 'Open Topo Map',
minZoom: 2,
maxZoom: 17,
getTileUrl: (zoom: number, x: number, y: number) =>
'https://' +
getRandomItem(abc) +
'.tile.opentopomap.org/' +
zoom +
'/' +
x +
'/' +
y +
'.png',
},
cyclosm: {
name: 'CyclOSM',
minZoom: 0,
maxZoom: 19,
getTileUrl: (zoom: number, x: number, y: number) =>
'https://' +
getRandomItem(abc) +
'.tile-cyclosm.openstreetmap.fr/cyclosm/' +
zoom +
'/' +
x +
'/' +
y +
'.png',
},
//https://b.tile.openstreetmap.fr/openriverboatmap/20/535762/382966.png
openriverboatmap: {
name: 'Open River Boat Map',
minZoom: 0,
maxZoom: 20,
getTileUrl: (zoom: number, x: number, y: number) =>
'https://' +
getRandomItem(abc) +
'.tile.openstreetmap.fr/openriverboatmap/' +
zoom +
'/' +
x +
'/' +
y +
'.png',
},
// cyclosmlite: {
// name: 'CyclOSM lite',
// minZoom: 0,
// maxZoom: 19,
// getTileUrl: (zoom: number, x: number, y: number) =>
// 'https://' +
// getRandomItem(abc) +
// '.tile-cyclosm.openstreetmap.fr/cyclosm-lite/' +
// zoom +
// '/' +
// x +
// '/' +
// y +
// '.png',
// },
// esrisat: {
// name: 'ESRI Satellite',
// minZoom: 0,
// maxZoom: 19,
// getTileUrl: (zoom: number, x: number, y: number) =>
// 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/' +
// zoom +
// '/' +
// x +
// '/' +
// y +
// '.jpg',
// },
fake: {
name: 'Fake provider',
minZoom: 0,
maxZoom: 20,
getTileUrl: (zoom: number, x: number, y: number) =>
'https://fakeurl/' + zoom + '/' + x + '/' + y + '.png',
},
};
const mod = (n: number, m: number) => {
const jsMod = n % m;
return jsMod >= 0 ? jsMod : jsMod + m;
};
/**
*
* @param keyObject The tile identifier
* @returns The `<Tile>`'s URL 'as a string)
*/
export const getTileUrl = (keyObject: TileKeyObject) => {
const nbTiles = 2 ** keyObject.zoomLevel;
const x = mod(keyObject.x, nbTiles);
const y = mod(keyObject.y, nbTiles);
return tileProviders[keyObject.provider].getTileUrl(
keyObject.zoomLevel,
x,
y
);
};
export default getTileUrl;