- {_.range(nbTilesY).map((iy) => (
-
- {_.range(nbTilesX).map((ix) => (
-
- ))}
+
+
+
+
+
+
+
);
};
diff --git a/src/components/mouse-handler.tsx b/src/components/mouse-handler.tsx
new file mode 100644
index 0000000..b63d2f5
--- /dev/null
+++ b/src/components/mouse-handler.tsx
@@ -0,0 +1,107 @@
+import react, { useCallback, useState } from 'react';
+
+import _ from 'lodash';
+
+import { Transformation } from './viewport';
+
+interface MouseHandlerProps {
+ applyTransformations: (transformations: Transformation[]) => void;
+ children: any;
+}
+
+const MouseHandler: react.FC
= (
+ props: MouseHandlerProps
+) => {
+ const initialMouseState = {
+ down: false,
+ starting: { x: -1, y: -1 },
+ };
+
+ const [mouseState, setMouseState] = useState(initialMouseState);
+
+ console.log('MouseHandler, mouseState: ' + JSON.stringify(mouseState));
+
+ const genericHandler = (event: any) => {
+ console.log('Log - Event: ' + event.type);
+ if (event.pageX !== undefined) {
+ console.log(`Mouse : ${event.pageX}, ${event.pageY}`);
+ console.log('mouseState: ' + JSON.stringify(mouseState));
+ return;
+ }
+ };
+
+ const mouseLeaveHandler = (event: any) => {
+ genericHandler(event);
+ throtteledMouseMoveHandler.cancel();
+ setMouseState(initialMouseState);
+ };
+
+ const mouseDownHandler = (event: any) => {
+ event.preventDefault();
+ genericHandler(event);
+ setMouseState({
+ down: true,
+ starting: { x: event.pageX, y: event.pageY },
+ });
+ };
+
+ const mouseUpHandler = (event: any) => {
+ genericHandler(event);
+ event.preventDefault();
+ setMouseState(initialMouseState);
+ };
+
+ const mouseMoveHandler = (event: any) => {
+ event.preventDefault();
+ if (mouseState.down) {
+ genericHandler(event);
+ props.applyTransformations([
+ {
+ translate: {
+ x: event.pageX - mouseState.starting.x,
+ y: event.pageY - mouseState.starting.y,
+ },
+ },
+ ]);
+ setMouseState({
+ down: true,
+ starting: {
+ x: event.pageX,
+ y: event.pageY,
+ },
+ });
+ }
+ };
+
+ const throtteledMouseMoveHandler = useCallback(
+ _.throttle(mouseMoveHandler, 50),
+ [mouseState.down]
+ );
+
+ const doubleClickHandler = (event: any) => {
+ genericHandler(event);
+ props.applyTransformations([
+ {
+ scale: {
+ factor: 2,
+ center: { x: event.pageX, y: event.pageY },
+ },
+ },
+ ]);
+ };
+
+ return (
+
+ {props.children}
+
+ );
+};
+
+export default MouseHandler;
diff --git a/src/components/single-touch-handler.tsx b/src/components/single-touch-handler.tsx
new file mode 100644
index 0000000..f7cc068
--- /dev/null
+++ b/src/components/single-touch-handler.tsx
@@ -0,0 +1,103 @@
+import react, { useCallback, useState } from 'react';
+
+import _ from 'lodash';
+
+import { Transformation } from './viewport';
+
+interface SingleTouchHandlerProps {
+ applyTransformations: (transformations: Transformation[]) => void;
+ children: any;
+}
+
+const SingleTouchHandler: react.FC = (
+ props: SingleTouchHandlerProps
+) => {
+ const initialTouchState = {
+ state: 'up',
+ touch: { x: -1, y: -1 },
+ };
+
+ const [touchState, setTouchState] = useState(initialTouchState);
+
+ console.log('SingleTouchHandler, touchState: ' + JSON.stringify(touchState));
+
+ const genericHandler = (event: any) => {
+ console.log('Log - Event: ' + event.type);
+ if (event.type.startsWith('touch')) {
+ if (event.touches.length > 0) {
+ console.log(
+ `Touch1 : (${event.touches[0].pageX}, ${event.touches[0].pageY})`
+ );
+ }
+ console.log('touchState: ' + JSON.stringify(touchState));
+ return;
+ }
+ };
+
+ const touchCancelHandler = (event: any) => {
+ genericHandler(event);
+ throtteledTouchMoveHandler.cancel();
+ setTouchState(initialTouchState);
+ };
+
+ const touchStartHandler = (event: any) => {
+ genericHandler(event);
+ // event.preventDefault();
+ if (event.touches.length === 1) {
+ setTouchState({
+ state: 'pointer',
+ touch: { x: event.touches[0].pageX, y: event.touches[0].pageY },
+ });
+ }
+ };
+
+ const touchEndHandler = (event: any) => {
+ genericHandler(event);
+ // event.preventDefault();
+ setTouchState(initialTouchState);
+ throtteledTouchMoveHandler.cancel();
+ };
+
+ const touchMoveHandler = (event: any) => {
+ // event.preventDefault();
+ if (touchState.state === 'pointer') {
+ if (event.touches.length === 1) {
+ genericHandler(event);
+ props.applyTransformations([
+ {
+ translate: {
+ x: event.touches[0].pageX - touchState.touch.x,
+ y: event.touches[0].pageY - touchState.touch.y,
+ },
+ },
+ ]);
+ setTouchState({
+ state: 'pointer',
+ touch: {
+ x: event.touches[0].pageX,
+ y: event.touches[0].pageY,
+ },
+ });
+ }
+ }
+ };
+
+ const throtteledTouchMoveHandler = useCallback(
+ _.throttle(touchMoveHandler, 100),
+ [touchState.state]
+ );
+
+ return (
+
+ {props.children}
+
+ );
+};
+
+export default SingleTouchHandler;
diff --git a/src/components/tile.tsx b/src/components/tile.tsx
deleted file mode 100644
index 19d7cfc..0000000
--- a/src/components/tile.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import React from 'react';
-import { tileSize } from './map';
-
-const tileProvider = (zoom: number, x: number, y: number) =>
- 'https://tile.openstreetmap.org/' + zoom + '/' + x + '/' + y + '.png';
-
-const Tile: React.FC<{
- ix: number;
- iy: number;
- x: number;
- y: number;
- delta: any;
- zoom: number;
-}> = (props: {
- ix: number;
- iy: number;
- x: number;
- y: number;
- delta: any;
- zoom: number;
-}) => {
- const style = {
- width: tileSize + 'px',
- height: tileSize + 'px',
- transform:
- 'translate3d(' +
- (props.ix * tileSize - props.delta.X) +
- 'px, ' +
- (props.iy * tileSize - props.delta.Y) +
- 'px, 0px)',
- };
- return (
-
- );
-};
-
-export default Tile;
diff --git a/src/components/viewport.tsx b/src/components/viewport.tsx
new file mode 100644
index 0000000..456db18
--- /dev/null
+++ b/src/components/viewport.tsx
@@ -0,0 +1,100 @@
+import react, { useCallback, useState } from 'react';
+
+import _, { constant } from 'lodash';
+
+import MouseHandler from './mouse-handler';
+import Layer from './layer';
+
+import '../theme/viewport.css';
+import SingleTouchHandler from './single-touch-handler';
+import DoubleTouchHandler from './double-touch-handler';
+
+export interface Point {
+ x: number;
+ y: number;
+}
+
+export interface Translation {
+ translate: Point;
+}
+
+export interface Scale {
+ scale: {
+ center: Point;
+ factor: number;
+ };
+}
+
+export type Transformation = Translation | Scale;
+
+// const transform1: Transformation = { translate: { x: 0, y: 1 } };
+// const transform2: Transformation = {
+// scale: { center: { x: 10, y: 20 }, factor: 2 },
+// };
+
+interface ViewportProps {
+ children: any;
+}
+
+export interface ViewportState {
+ scale: number;
+ translation: Point;
+}
+
+const Viewport: react.FC = (props: ViewportProps) => {
+ //console.log(`--- Rendering viewport, props: ${JSON.stringify(props)} ---`);
+
+ const initialState: ViewportState = { scale: 1, translation: { x: 0, y: 0 } };
+
+ const [state, setState] = useState(initialState);
+
+ const genericHandler = (event: any) => {
+ console.log('Log - Event: ' + event.type);
+ return;
+ };
+
+ const applyTransformations = (transformations: Transformation[]) => {
+ const newState = transformations.reduce(
+ (previousState: ViewportState, transformation): ViewportState => {
+ if ('scale' in transformation) {
+ return {
+ scale: previousState.scale * transformation.scale.factor,
+ translation: {
+ x:
+ previousState.translation.x +
+ (previousState.translation.x - transformation.scale.center.x) *
+ (transformation.scale.factor - 1),
+ y:
+ previousState.translation.y +
+ (previousState.translation.y - transformation.scale.center.y) *
+ (transformation.scale.factor - 1),
+ },
+ };
+ }
+ return {
+ scale: previousState.scale,
+ translation: {
+ x: previousState.translation.x + transformation.translate.x,
+ y: previousState.translation.y + transformation.translate.y,
+ },
+ };
+ },
+ state
+ );
+ setState(newState);
+ };
+
+ return (
+
+
+
+
+ {props.children}
+
+
+
+
+ );
+};
+
+export default Viewport;
diff --git a/src/declarations/gpx-parser-builder.d.ts b/src/declarations/gpx-parser-builder.d.ts
deleted file mode 100644
index 8e9dbe3..0000000
--- a/src/declarations/gpx-parser-builder.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-declare module 'gpx-parser-builder'
\ No newline at end of file
diff --git a/src/index.tsx b/src/index.tsx
index 35a930f..dcd0a01 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,9 +1,13 @@
import React from 'react';
-
-import App from './App';
-
import { createRoot } from 'react-dom/client';
+import App from './App';
const container = document.getElementById('root');
const root = createRoot(container!);
-root.render();
+root.render(
+
+
+
+);
+
+
diff --git a/src/lib/gpx.ts b/src/lib/gpx.ts
deleted file mode 100644
index d380aea..0000000
--- a/src/lib/gpx.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import _ from 'lodash';
-
-const initialState: any = {
- current: {
- $: {},
- trk: [
- {
- trkseg: [{ trkpt: [] }],
- },
- ],
- },
-};
-
-export const appendTrkpt = (gpx:any , trkpt:any) => {
- var updatedGpx = _.cloneDeep(gpx);
- updatedGpx.trk[0].trkseg[0].trkpt.push(trkpt);
- return updatedGpx;
-};
-
-export const clearTrkpt = (gpx:any) => {
- var updatedGpx = _.cloneDeep(gpx);
- updatedGpx.trk[0]=[];
- return updatedGpx;
-};
-
-
diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts
new file mode 100644
index 0000000..6431bc5
--- /dev/null
+++ b/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/src/service-worker.ts b/src/service-worker.ts
new file mode 100644
index 0000000..652a8a4
--- /dev/null
+++ b/src/service-worker.ts
@@ -0,0 +1,80 @@
+///
+/* eslint-disable no-restricted-globals */
+
+// This service worker can be customized!
+// See https://developers.google.com/web/tools/workbox/modules
+// for the list of available Workbox modules, or add any other
+// code you'd like.
+// You can also remove this file if you'd prefer not to use a
+// service worker, and the Workbox build step will be skipped.
+
+import { clientsClaim } from 'workbox-core';
+import { ExpirationPlugin } from 'workbox-expiration';
+import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
+import { registerRoute } from 'workbox-routing';
+import { StaleWhileRevalidate } from 'workbox-strategies';
+
+declare const self: ServiceWorkerGlobalScope;
+
+clientsClaim();
+
+// Precache all of the assets generated by your build process.
+// Their URLs are injected into the manifest variable below.
+// This variable must be present somewhere in your service worker file,
+// even if you decide not to use precaching. See https://cra.link/PWA
+precacheAndRoute(self.__WB_MANIFEST);
+
+// Set up App Shell-style routing, so that all navigation requests
+// are fulfilled with your index.html shell. Learn more at
+// https://developers.google.com/web/fundamentals/architecture/app-shell
+const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
+registerRoute(
+ // Return false to exempt requests from being fulfilled by index.html.
+ ({ request, url }: { request: Request; url: URL }) => {
+ // If this isn't a navigation, skip.
+ if (request.mode !== 'navigate') {
+ return false;
+ }
+
+ // If this is a URL that starts with /_, skip.
+ if (url.pathname.startsWith('/_')) {
+ return false;
+ }
+
+ // If this looks like a URL for a resource, because it contains
+ // a file extension, skip.
+ if (url.pathname.match(fileExtensionRegexp)) {
+ return false;
+ }
+
+ // Return true to signal that we want to use the handler.
+ return true;
+ },
+ createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
+);
+
+// An example runtime caching route for requests that aren't handled by the
+// precache, in this case same-origin .png requests like those from in public/
+registerRoute(
+ // Add in any other file extensions or routing criteria as needed.
+ ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'),
+ // Customize this strategy as needed, e.g., by changing to CacheFirst.
+ new StaleWhileRevalidate({
+ cacheName: 'images',
+ plugins: [
+ // Ensure that once this runtime cache reaches a maximum size the
+ // least-recently used images are removed.
+ new ExpirationPlugin({ maxEntries: 50 }),
+ ],
+ })
+);
+
+// This allows the web app to trigger skipWaiting via
+// registration.waiting.postMessage({type: 'SKIP_WAITING'})
+self.addEventListener('message', (event) => {
+ if (event.data && event.data.type === 'SKIP_WAITING') {
+ self.skipWaiting();
+ }
+});
+
+// Any other custom service worker logic can go here.
diff --git a/src/serviceWorkerRegistration.ts b/src/serviceWorkerRegistration.ts
new file mode 100644
index 0000000..efbf2ac
--- /dev/null
+++ b/src/serviceWorkerRegistration.ts
@@ -0,0 +1,142 @@
+// This optional code is used to register a service worker.
+// register() is not called by default.
+
+// This lets the app load faster on subsequent visits in production, and gives
+// it offline capabilities. However, it also means that developers (and users)
+// will only see deployed updates on subsequent visits to a page, after all the
+// existing tabs open on the page have been closed, since previously cached
+// resources are updated in the background.
+
+// To learn more about the benefits of this model and instructions on how to
+// opt-in, read https://cra.link/PWA
+
+const isLocalhost = Boolean(
+ window.location.hostname === 'localhost' ||
+ // [::1] is the IPv6 localhost address.
+ window.location.hostname === '[::1]' ||
+ // 127.0.0.0/8 are considered localhost for IPv4.
+ window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
+);
+
+type Config = {
+ onSuccess?: (registration: ServiceWorkerRegistration) => void;
+ onUpdate?: (registration: ServiceWorkerRegistration) => void;
+};
+
+export function register(config?: Config) {
+ if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
+ // The URL constructor is available in all browsers that support SW.
+ const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
+ if (publicUrl.origin !== window.location.origin) {
+ // Our service worker won't work if PUBLIC_URL is on a different origin
+ // from what our page is served on. This might happen if a CDN is used to
+ // serve assets; see https://github.com/facebook/create-react-app/issues/2374
+ return;
+ }
+
+ window.addEventListener('load', () => {
+ const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
+
+ if (isLocalhost) {
+ // This is running on localhost. Let's check if a service worker still exists or not.
+ checkValidServiceWorker(swUrl, config);
+
+ // Add some additional logging to localhost, pointing developers to the
+ // service worker/PWA documentation.
+ navigator.serviceWorker.ready.then(() => {
+ console.log(
+ 'This web app is being served cache-first by a service ' +
+ 'worker. To learn more, visit https://cra.link/PWA'
+ );
+ });
+ } else {
+ // Is not localhost. Just register service worker
+ registerValidSW(swUrl, config);
+ }
+ });
+ }
+}
+
+function registerValidSW(swUrl: string, config?: Config) {
+ navigator.serviceWorker
+ .register(swUrl)
+ .then((registration) => {
+ registration.onupdatefound = () => {
+ const installingWorker = registration.installing;
+ if (installingWorker == null) {
+ return;
+ }
+ installingWorker.onstatechange = () => {
+ if (installingWorker.state === 'installed') {
+ if (navigator.serviceWorker.controller) {
+ // At this point, the updated precached content has been fetched,
+ // but the previous service worker will still serve the older
+ // content until all client tabs are closed.
+ console.log(
+ 'New content is available and will be used when all ' +
+ 'tabs for this page are closed. See https://cra.link/PWA.'
+ );
+
+ // Execute callback
+ if (config && config.onUpdate) {
+ config.onUpdate(registration);
+ }
+ } else {
+ // At this point, everything has been precached.
+ // It's the perfect time to display a
+ // "Content is cached for offline use." message.
+ console.log('Content is cached for offline use.');
+
+ // Execute callback
+ if (config && config.onSuccess) {
+ config.onSuccess(registration);
+ }
+ }
+ }
+ };
+ };
+ })
+ .catch((error) => {
+ console.error('Error during service worker registration:', error);
+ });
+}
+
+function checkValidServiceWorker(swUrl: string, config?: Config) {
+ // Check if the service worker can be found. If it can't reload the page.
+ fetch(swUrl, {
+ headers: { 'Service-Worker': 'script' },
+ })
+ .then((response) => {
+ // Ensure service worker exists, and that we really are getting a JS file.
+ const contentType = response.headers.get('content-type');
+ if (
+ response.status === 404 ||
+ (contentType != null && contentType.indexOf('javascript') === -1)
+ ) {
+ // No service worker found. Probably a different app. Reload the page.
+ navigator.serviceWorker.ready.then((registration) => {
+ registration.unregister().then(() => {
+ window.location.reload();
+ });
+ });
+ } else {
+ // Service worker found. Proceed as normal.
+ registerValidSW(swUrl, config);
+ }
+ })
+ .catch(() => {
+ console.log('No internet connection found. App is running in offline mode.');
+ });
+}
+
+export function unregister() {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.ready
+ .then((registration) => {
+ registration.unregister();
+ })
+ .catch((error) => {
+ console.error(error.message);
+ });
+ }
+}
diff --git a/src/theme/layer.css b/src/theme/layer.css
new file mode 100644
index 0000000..e3cf5f5
--- /dev/null
+++ b/src/theme/layer.css
@@ -0,0 +1,8 @@
+.background {
+ position: fixed;
+ width: 4032px;
+ height: 2268px;
+ z-index: -1;
+ transform-origin: top left;
+}
+
diff --git a/src/theme/map.css b/src/theme/map.css
deleted file mode 100644
index db47bb0..0000000
--- a/src/theme/map.css
+++ /dev/null
@@ -1,17 +0,0 @@
-.tiles {
- position: relative;
- width: 100%;
- height: 100%;
-}
-
-.tiles p {
- z-index: 20;
- position: relative;
- width: 100%;
- height: 100%;
-}
-
-.tiles img {
- max-width: none;
- position: absolute;
-}
diff --git a/src/theme/viewport.css b/src/theme/viewport.css
new file mode 100644
index 0000000..8b4137e
--- /dev/null
+++ b/src/theme/viewport.css
@@ -0,0 +1,11 @@
+.viewport {
+ position: fixed;
+ width: 100%;
+ height: 100%;
+}
+
+.background img {
+ width: 4032px;
+ height: 2268px;
+ }
+
\ No newline at end of file