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
You need to sign in or sign up before continuing.
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 @@
"canvas"
:
"^3.2.0"
,
"class-variance-authority"
:
"^0.7.1"
,
"clsx"
:
"^2.1.1"
,
"geotiff"
:
"^2.1.4-beta.0"
,
"lucide-react"
:
"^0.545.0"
,
"maplibre-gl"
:
"^5.9.0"
,
"react"
:
"^19.1.1"
,
...
...
@@ -1362,6 +1363,12 @@
"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"
:
{
"version"
:
"1.1.1"
,
"resolved"
:
"https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz"
,
...
...
@@ -4056,6 +4063,25 @@
"integrity"
:
"sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A=="
,
"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"
:
{
"version"
:
"1.0.1"
,
"resolved"
:
"https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz"
,
...
...
@@ -4393,6 +4419,12 @@
"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"
:
{
"version"
:
"0.4.1"
,
"resolved"
:
"https://registry.npmjs.org/levn/-/levn-0.4.1.tgz"
,
...
...
@@ -4948,6 +4980,12 @@
"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"
:
{
"version"
:
"1.0.1"
,
"resolved"
:
"https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz"
,
...
...
@@ -4961,6 +4999,12 @@
"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"
:
{
"version"
:
"4.0.0"
,
"resolved"
:
"https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
,
...
...
@@ -5129,6 +5173,18 @@
],
"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"
:
{
"version"
:
"3.0.0"
,
"resolved"
:
"https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz"
,
...
...
@@ -6158,6 +6214,12 @@
"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"
:
{
"version"
:
"2.0.2"
,
"resolved"
:
"https://registry.npmjs.org/which/-/which-2.0.2.tgz"
,
...
...
@@ -6190,6 +6252,12 @@
"integrity"
:
"sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
,
"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"
:
{
"version"
:
"3.1.1"
,
"resolved"
:
"https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz"
,
...
...
@@ -6209,6 +6277,12 @@
"funding"
:
{
"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 @@
"canvas"
:
"^3.2.0"
,
"class-variance-authority"
:
"^0.7.1"
,
"clsx"
:
"^2.1.1"
,
"geotiff"
:
"^2.1.4-beta.0"
,
"lucide-react"
:
"^0.545.0"
,
"maplibre-gl"
:
"^5.9.0"
,
"react"
:
"^19.1.1"
,
...
...
frontend/src/App.tsx
View file @
dbab9ed0
...
...
@@ -3,9 +3,7 @@ import {MapLayerCard} from "@/components/map/map-layer-card.tsx";
import
{
useState
}
from
"
react
"
;
import
{
SingleMap
}
from
"
@/components/map/single-map.tsx
"
;
import
{
SplitMap
}
from
"
@/components/map/split-map.tsx
"
;
import
{
Card
,
CardContent
}
from
"
@/components/ui/card.tsx
"
;
import
{
Switch
}
from
"
@/components/ui/switch.tsx
"
;
import
{
Label
}
from
"
@/components/ui/label.tsx
"
;
import
{
SplitMapCard
}
from
"
@/components/map/split-map-card.tsx
"
;
type
Mode
=
'
single
'
|
'
split
'
;
...
...
@@ -20,20 +18,7 @@ function App() {
<
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"
>
<
MapLayerCard
/>
<
Card
className
=
"bg-white/90 backdrop-blur"
>
<
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
>
<
SplitMapCard
mode
=
{
mode
}
setMode
=
{
setMode
}
/>
</
div
>
<
div
className
=
"h-full w-full"
>
...
...
frontend/src/components/map/single-map.tsx
View file @
dbab9ed0
...
...
@@ -43,4 +43,4 @@ function SingleMap({
);
}
export
{
SingleMap
};
\ No newline at end of file
export
{
SingleMap
};
\ 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