Commit dbab9ed0 authored by Heisenberg5124's avatar Heisenberg5124
Browse files

Initial commit

parent 00816a34
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
"canvas": "^3.2.0", "canvas": "^3.2.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"geotiff": "^2.1.4-beta.0",
"lucide-react": "^0.545.0", "lucide-react": "^0.545.0",
"maplibre-gl": "^5.9.0", "maplibre-gl": "^5.9.0",
"react": "^19.1.1", "react": "^19.1.1",
...@@ -1362,6 +1363,12 @@ ...@@ -1362,6 +1363,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@petamoriken/float16": {
"version": "3.9.3",
"resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz",
"integrity": "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==",
"license": "MIT"
},
"node_modules/@radix-ui/number": { "node_modules/@radix-ui/number": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
...@@ -4056,6 +4063,25 @@ ...@@ -4056,6 +4063,25 @@
"integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/geotiff": {
"version": "2.1.4-beta.0",
"resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.1.4-beta.0.tgz",
"integrity": "sha512-jb6SYvHMyiCqwqgGGLDAxtig9h1g6O+n1wEyNEE4QgVEXOItYaWrEgPg9SAnwdoZm2yx6DpFtilbGG65hvZgpQ==",
"license": "MIT",
"dependencies": {
"@petamoriken/float16": "^3.4.7",
"lerc": "^3.0.0",
"pako": "^2.0.4",
"parse-headers": "^2.0.2",
"quick-lru": "^6.1.1",
"web-worker": "^1.5.0",
"xml-utils": "^1.10.2",
"zstddec": "^0.2.0-alpha.3"
},
"engines": {
"node": ">=10.19"
}
},
"node_modules/get-nonce": { "node_modules/get-nonce": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
...@@ -4393,6 +4419,12 @@ ...@@ -4393,6 +4419,12 @@
"json-buffer": "3.0.1" "json-buffer": "3.0.1"
} }
}, },
"node_modules/lerc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz",
"integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==",
"license": "Apache-2.0"
},
"node_modules/levn": { "node_modules/levn": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
...@@ -4948,6 +4980,12 @@ ...@@ -4948,6 +4980,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
...@@ -4961,6 +4999,12 @@ ...@@ -4961,6 +4999,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/parse-headers": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz",
"integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==",
"license": "MIT"
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
...@@ -5129,6 +5173,18 @@ ...@@ -5129,6 +5173,18 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/quick-lru": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz",
"integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/quickselect": { "node_modules/quickselect": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
...@@ -6158,6 +6214,12 @@ ...@@ -6158,6 +6214,12 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/web-worker": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",
"integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==",
"license": "Apache-2.0"
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
...@@ -6190,6 +6252,12 @@ ...@@ -6190,6 +6252,12 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/xml-utils": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz",
"integrity": "sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==",
"license": "CC0-1.0"
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
...@@ -6209,6 +6277,12 @@ ...@@ -6209,6 +6277,12 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/zstddec": {
"version": "0.2.0-alpha.3",
"resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.2.0-alpha.3.tgz",
"integrity": "sha512-uHyE3TN8jRFOaMVwdhERfrcaabyoUUawIRDKXE6x0nCU7mzyIZO0LndJ3AtVUiKLF0lC+8F5bMSySWEF586PSA==",
"license": "MIT AND BSD-3-Clause"
} }
} }
} }
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
"canvas": "^3.2.0", "canvas": "^3.2.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"geotiff": "^2.1.4-beta.0",
"lucide-react": "^0.545.0", "lucide-react": "^0.545.0",
"maplibre-gl": "^5.9.0", "maplibre-gl": "^5.9.0",
"react": "^19.1.1", "react": "^19.1.1",
......
...@@ -3,9 +3,7 @@ import {MapLayerCard} from "@/components/map/map-layer-card.tsx"; ...@@ -3,9 +3,7 @@ import {MapLayerCard} from "@/components/map/map-layer-card.tsx";
import {useState} from "react"; import {useState} from "react";
import {SingleMap} from "@/components/map/single-map.tsx"; import {SingleMap} from "@/components/map/single-map.tsx";
import {SplitMap} from "@/components/map/split-map.tsx"; import {SplitMap} from "@/components/map/split-map.tsx";
import {Card, CardContent} from "@/components/ui/card.tsx"; import {SplitMapCard} from "@/components/map/split-map-card.tsx";
import {Switch} from "@/components/ui/switch.tsx";
import {Label} from "@/components/ui/label.tsx";
type Mode = 'single' | 'split'; type Mode = 'single' | 'split';
...@@ -20,20 +18,7 @@ function App() { ...@@ -20,20 +18,7 @@ function App() {
<div className="h-screen w-screen overflow-hidden relative"> <div className="h-screen w-screen overflow-hidden relative">
<div className="absolute right-4 top-4 z-20 w-[min(320px,42vw)] space-y-3"> <div className="absolute right-4 top-4 z-20 w-[min(320px,42vw)] space-y-3">
<MapLayerCard/> <MapLayerCard/>
<Card className="bg-white/90 backdrop-blur"> <SplitMapCard mode={mode} setMode={setMode}/>
<CardContent className="p-4">
<div className="flex items-center justify-between gap-3">
<Label htmlFor="mode-switch" className="text-xs text-muted-foreground">
Split mode
</Label>
<Switch
id="mode-switch"
checked={mode === 'split'}
onCheckedChange={(checked) => setMode(checked ? 'split' : 'single')}
/>
</div>
</CardContent>
</Card>
</div> </div>
<div className="h-full w-full"> <div className="h-full w-full">
......
...@@ -43,4 +43,4 @@ function SingleMap({ ...@@ -43,4 +43,4 @@ function SingleMap({
); );
} }
export { SingleMap }; export {SingleMap};
\ No newline at end of file \ No newline at end of file
import {Card, CardContent} from "@/components/ui/card.tsx";
import {Label} from "@/components/ui/label.tsx";
import {Switch} from "@/components/ui/switch.tsx";
interface SplitMapCardProps {
mode: 'single' | 'split'
setMode: (mode: 'single' | 'split') => void;
}
function SplitMapCard({mode, setMode}: SplitMapCardProps) {
return (
<Card className="bg-white/90 backdrop-blur">
<CardContent>
<div className="flex items-center justify-between gap-3">
<Label htmlFor="mode-switch" className="text-xs text-muted-foreground">
Split mode
</Label>
<Switch
id="mode-switch"
checked={mode === 'split'}
onCheckedChange={(checked) => setMode(checked ? 'split' : 'single')}
/>
</div>
</CardContent>
</Card>
)
}
export {SplitMapCard}
\ No newline at end of file
import {useCallback, useEffect, useRef, useState} from 'react';
import type {ViewState, ViewStateChangeEvent} from 'react-map-gl/maplibre';
import * as React from "react";
import {MapViewer} from "@/components/map/map-viewer.tsx";
interface SplitMapViewerProps {
leftYear: number;
rightYear: number;
initialDivider?: number;
initialViewState?: ViewState;
style?: React.CSSProperties;
}
interface CameraState {
longitude: number;
latitude: number;
zoom: number;
bearing?: number;
pitch?: number;
}
const FALLBACK_VIEW = {
latitude: 48.78035,
longitude: 9.17289,
zoom: 13,
bearing: 0,
pitch: 0
};
export function SplitMapViewer({
leftYear,
rightYear,
initialDivider = 0.5,
initialViewState,
style
}: SplitMapViewerProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const [divider, setDivider] = useState(Math.min(0.95, Math.max(0.05, initialDivider)));
const [viewState, setViewState] = useState<CameraState>(initialViewState ?? FALLBACK_VIEW);
const [dragging, setDragging] = useState(false);
const onMove = useCallback((e: ViewStateChangeEvent) => {
setViewState(e.viewState);
}, []);
useEffect(() => {
if (!dragging) return;
const onMoveDoc = (ev: MouseEvent) => {
const el = containerRef.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const pct = (ev.clientX - rect.left) / rect.width;
setDivider(Math.min(0.98, Math.max(0.02, pct)));
};
const onUpDoc = () => setDragging(false);
window.addEventListener('mousemove', onMoveDoc);
window.addEventListener('mouseup', onUpDoc);
return () => {
window.removeEventListener('mousemove', onMoveDoc);
window.removeEventListener('mouseup', onUpDoc);
};
}, [dragging]);
const leftClip = `inset(0 ${100 - divider * 100}% 0 0)`;
const rightClip = `inset(0 0 0 ${divider * 100}%)`;
return (
<div
ref={containerRef}
style={{
position: 'relative',
width: '100%',
height: '100%',
overflow: 'hidden',
...style
}}
>
{/* Right map (bottom layer) */}
<div
style={{
position: 'absolute',
inset: 0,
clipPath: rightClip
}}
><MapViewer
year={rightYear}
viewState={viewState}
onMove={onMove}
style={{height: '100%'}}
mapStyleUrl="https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
/>
</div>
{/* Left map (top layer) */}
<div
style={{
position: 'absolute',
inset: 0,
clipPath: leftClip,
pointerEvents: 'auto'
}}
>
<MapViewer
year={leftYear}
viewState={viewState}
onMove={onMove}
style={{height: '100%'}}
mapStyleUrl="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
/>
</div>
{/* Divider */}
<div
role="separator"
aria-orientation="vertical"
onMouseDown={() => setDragging(true)}
style={{
position: 'absolute',
top: 0,
bottom: 0,
left: `${divider * 100}%`,
width: 2,
transform: 'translateX(-1px)',
background: '#fff',
boxShadow: '0 0 0 1px rgba(0,0,0,0.25)',
cursor: 'col-resize',
zIndex: 10
}}
>
<div
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 14,
height: 14,
borderRadius: 9999,
background: '#fff',
boxShadow: '0 1px 3px rgba(0,0,0,0.35)'
}}
/>
</div>
</div>
);
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment