Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
HFT-informatics-weekend
SI - Time Series Map Viewer
Commits
dbab9ed0
Commit
dbab9ed0
authored
Oct 12, 2025
by
Heisenberg5124
Browse files
Initial commit
parent
00816a34
Changes
6
Hide whitespace changes
Inline
Side-by-side
frontend/package-lock.json
View file @
dbab9ed0
...
@@ -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"
}
}
}
}
}
}
frontend/package.json
View file @
dbab9ed0
...
@@ -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"
,
...
...
frontend/src/App.tsx
View file @
dbab9ed0
...
@@ -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"
>
...
...
frontend/src/components/map/single-map.tsx
View file @
dbab9ed0
...
@@ -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
frontend/src/components/map/split-map-card.tsx
0 → 100644
View file @
dbab9ed0
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
frontend/src/components/map/split-map-viewer.tsx
deleted
100644 → 0
View file @
00816a34
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
>
);
}
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment