diff --git a/src/components/map/map.tsx b/src/components/map/map.tsx index afb75c1..37296f9 100644 --- a/src/components/map/map.tsx +++ b/src/components/map/map.tsx @@ -20,6 +20,7 @@ const Map: react.FC<{}> = (props: {}) => { useEffect(() => { window.addEventListener('resize', debouncedResizeHandler); +// dispatch(mapActions.shift({ x: -50, y: 0 })); }, []); return ( diff --git a/src/setupTests.ts b/src/setupTests.ts new file mode 100644 index 0000000..87988d6 --- /dev/null +++ b/src/setupTests.ts @@ -0,0 +1,14 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom/extend-expect'; + +// Mock matchmedia +window.matchMedia = window.matchMedia || function() { + return { + matches: false, + addListener: function() {}, + removeListener: function() {} + }; +}; diff --git a/src/store/map.test.ts b/src/store/map.test.ts new file mode 100644 index 0000000..e99c0f0 --- /dev/null +++ b/src/store/map.test.ts @@ -0,0 +1,69 @@ +import _ from 'lodash'; +import { initialMapState, reevaluateState } from './map'; + +//interface CustomMatchers { +// isDeepEqual(received: any, target: any): R; +//} + +//declare global { +// namespace jest { +// interface Expect extends CustomMatchers {} +// interface Matchers extends CustomMatchers {} +// interface InverseAsymmetricMatchers extends CustomMatchers {} +// } +//} + +expect.extend({ + isDeepEqual: (received: any, target: any) => { + const pass = _.isEqual(received, target); + if (pass) { + return { message: () => '', pass: true }; + } + return { + message: () => + `${JSON.stringify(received)} instead of\n ${JSON.stringify(target)}`, + pass: false, + }; + }, + isAlmostDeepEqual: (received: any, target: any, delta: number) => { + const pass = _.isEqualWith(received, target, (r: any, t: any) => { + if (typeof r !== 'number' || typeof t !== 'number') { + return undefined; + } + console.log(`r: ${r}, t:${t}, ${Math.abs(r - t) <= delta}`); + return Math.abs(r - t) <= delta; + }); + if (pass) { + return { message: () => '', pass: true }; + } + return { + message: () => + `${JSON.stringify(received)} instead of\n ${JSON.stringify(target)}`, + pass: false, + }; + }, +}); + +describe('Our isAlmostDeepEqual matcher', () => { + test('compares correctly two numbers that are almost equals', () => { + expect({ x: 1.001 }).isAlmostDeepEqual({ x: 1 }, 1e-2); + }); +}); + +describe('Map store methods', () => { + test('initialize its state', () => { + const state = _.cloneDeep(initialMapState); + expect(state.tiles.nb.x).not.toBe(0); + }); + test('reevaluateState keeps the same values', () => { + const state = _.cloneDeep(initialMapState); + reevaluateState(state); + expect(state).isAlmostDeepEqual(initialMapState, 1e-7); + }); + test('reevaluateState computes the right longitude after a shift 50 pixels left', () => { + const state = _.cloneDeep(initialMapState); + state.slippy.translation.x = state.slippy.translation.x - 50; + reevaluateState(state); + expect(state.scope.center.lon).not.toBe(77.5539501); + }); +}); diff --git a/src/store/map.ts b/src/store/map.ts index e622d10..4b7b428 100644 --- a/src/store/map.ts +++ b/src/store/map.ts @@ -1,7 +1,14 @@ import { createSlice } from '@reduxjs/toolkit'; import _ from 'lodash'; import { tileSize } from '../components/map/tiled-map'; -import { geoPoint, Point, lon2tile, lat2tile } from '../lib/geo'; +import { + geoPoint, + Point, + lon2tile, + lat2tile, + tile2lat, + tile2long, +} from '../lib/geo'; // Top level properties (the other properties can be derived from them) @@ -12,7 +19,7 @@ export interface MapScope { } const initialMapScope: MapScope = { center: { lat: -37.8403508, lon: 77.5539501 }, - zoom: 13.49, + zoom: 13, }; // Derived properties @@ -60,7 +67,7 @@ const nbTilesY = _.ceil(mapState.viewport.height / tileSize) + 4; */ -let initialMapState: MapState = { +export var initialMapState: MapState = { scope: initialMapScope, slippy: { scale: 1, @@ -82,7 +89,8 @@ let initialMapState: MapState = { }, }; -const evaluateStateFromScope = (state: MapState) => { +export const evaluateStateFromScope = (state: MapState) => { + console.log('<<<<<<<<<<<< evaluateStateFromScope'); state.tiles.zoom = _.round(state.scope.zoom); const softZoom = state.scope.zoom - state.tiles.zoom; state.slippy.scale = 2 ** softZoom; @@ -115,9 +123,37 @@ const evaluateStateFromScope = (state: MapState) => { evaluateStateFromScope(initialMapState); -const reevaluateState = (state: MapState) => {}; - - +export const reevaluateState = (state: MapState) => { + // Update the scope (center and zoom level) + const centerPX = { + x: window.innerWidth / 2, + y: window.innerHeight / 2, + }; + const visibleTileSize = tileSize / state.slippy.scale; + const centerTiles = { + x: + state.tiles.first.x + + (centerPX.x - state.slippy.translation.x) / visibleTileSize, + y: + state.tiles.first.y + + (centerPX.y - state.slippy.translation.y) / visibleTileSize, + }; + state.scope.center = { + lat: tile2lat(centerTiles.y, state.tiles.zoom), + lon: tile2long(centerTiles.x, state.tiles.zoom), + }; + // TODO: zoom level + if ( + -state.slippy.translation.x < visibleTileSize || + -state.slippy.translation.y < visibleTileSize || + -state.slippy.translation.x > + (state.tiles.nb.x - 1) * visibleTileSize - window.innerWidth || + -state.slippy.translation.y > + (state.tiles.nb.y - 1) * visibleTileSize - window.innerHeight + ) { + evaluateStateFromScope(state); + } +}; const mapSlice = createSlice({ name: 'map', @@ -131,6 +167,7 @@ const mapSlice = createSlice({ x: state.slippy.translation.x + action.payload.x, y: state.slippy.translation.y + action.payload.y, }; + reevaluateState(state); }, scale: (state, action) => { state.slippy.scale = state.slippy.scale * action.payload.factor; @@ -144,6 +181,7 @@ const mapSlice = createSlice({ (state.slippy.translation.y - action.payload.center.y) * (action.payload.factor - 1), }; + reevaluateState(state); }, }, });