@@ -1,5 +1,5 @@
< script lang = "ts" >
import { onMount , onDestroy } from 'svelte' ;
import { onMount , onDestroy , tick } from 'svelte' ;
import Map from 'ol/Map.js' ;
import type MapBrowserEvent from 'ol/MapBrowserEvent.js' ;
import { Style , Stroke , Fill } from 'ol/style.js' ;
@@ -88,6 +88,9 @@
let locationSearchDebounceTimer : ReturnType < typeof setTimeout > | null = null ;
let selectionBoxCenterPixel : [ number , number ] | null = null ;
let footerCollapsed = false ;
let showBoxTooltip = false ;
let boxSelectButton : HTMLButtonElement ;
let boxTooltipPosition : { left : number ; top : number } | null = null ;
let cameraSource : VectorSource ;
let clusterSource : Cluster ;
@@ -203,6 +206,15 @@
restoreStateFromUrl ();
const hasSeenTooltip = localStorage . getItem ( 'surveilled-box-tooltip-seen' );
if ( ! hasSeenTooltip ) {
setTimeout ( async () => {
showBoxTooltip = true ;
await tick ();
updateTooltipPosition ();
}, 500 );
}
if ( navigator . geolocation ) {
navigator . geolocation . getCurrentPosition (
( position ) => {
@@ -424,19 +436,48 @@
}
}
function updateTooltipPosition() {
if ( ! boxSelectButton ) return ;
const toolbar = boxSelectButton . closest ( '.toolbar' );
if ( ! toolbar ) return ;
const toolbarContainer = toolbar . parentElement ;
if ( ! toolbarContainer ) return ;
const buttonRect = boxSelectButton . getBoundingClientRect ();
const containerRect = toolbarContainer . getBoundingClientRect ();
boxTooltipPosition = {
left : buttonRect.left - containerRect . left + buttonRect . width / 2 ,
top : buttonRect.bottom - containerRect . top + 8 ,
};
}
function dismissBoxTooltip() {
showBoxTooltip = false ;
localStorage . setItem ( 'surveilled-box-tooltip-seen' , 'true' );
}
function toggleBoxSelectMode() {
if ( isBoxSelectMode ) {
disableBoxSelectMode ();
} else {
enableBoxSelectMode ();
}
dismissBoxTooltip ();
}
function enableBoxSelectMode() {
if ( isBoxSelectMode || ! map || ! selectionLayer ) return ;
if ( ! map || ! selectionLayer ) return ;
const source = selectionLayer . getSource ();
if ( ! source ) return ;
if ( boxMoveHandler ) {
unByKey ( boxMoveHandler );
boxMoveHandler = null ;
}
if ( isBoxSelectMode ) {
return ;
}
isBoxSelectMode = true ;
source . clear ();
boxStartPoint = null ;
@@ -456,11 +497,16 @@
}
function handleBoxClick ( event : MapBrowserEvent < PointerEvent | KeyboardEvent | WheelEvent >) {
if ( ! map || ! selectionLayer ) return ;
if ( ! map || ! selectionLayer || ! isBoxSelectMode ) return ;
const source = selectionLayer . getSource ();
if ( ! source ) return ;
const coordinate = event . coordinate ;
if ( ! coordinate ) return ;
event . originalEvent ? . preventDefault ? .();
event . originalEvent ? . stopPropagation ? .();
const lonLat = toLonLat ( coordinate );
const lonLatTuple : [ number , number ] = [ lonLat [ 0 ], lonLat [ 1 ]];
@@ -480,7 +526,7 @@
}
function updateBoxPreview ( endCoordinate : number []) {
if ( ! map || ! selectionLayer || ! boxStartPoint ) return ;
if ( ! map || ! selectionLayer || ! boxStartPoint || ! isBoxSelectMode ) return ;
const source = selectionLayer . getSource ();
if ( ! source ) return ;
@@ -515,9 +561,6 @@
width : 2.5 ,
lineDash : [ 5 , 3 ],
}),
fill : new Fill ({
color : 'rgba(239, 68, 68, 0.18)' ,
}),
})
);
source . clear ();
@@ -551,8 +594,7 @@
}
function disableBoxSelectMode ( keepSelection = false ) {
if ( ! isBoxSelectMode || ! map) return ;
isBoxSelectMode = false ;
if ( ! map ) return ;
if ( boxMoveHandler ) {
unByKey ( boxMoveHandler );
@@ -572,8 +614,12 @@
if ( map . getTargetElement ()) {
map . getTargetElement (). style . cursor = '' ;
}
setStatusMessage ( '' );
updateUrlState ();
if ( isBoxSelectMode ) {
isBoxSelectMode = false ;
setStatusMessage ( '' );
updateUrlState ();
}
}
function handleRefresh() {
@@ -946,13 +992,21 @@
< div
class = "toolbar bg-bg-secondary/80 backdrop-blur-sm border border-border-color rounded-lg px-2 py-1.5 flex items-center gap-1 shadow-lg"
>
< button
class = "toolbar-btn { isBoxSelectMode ? 'active' : '' } "
title = "Select Area (B)"
on:click = { toggleBoxSelectMode }
>
< Square size = { 16 } / >
</ button >
< div class = "relative" >
{ #if showBoxTooltip }
< div
class = "absolute -inset-1 rounded-lg border-2 border-accent-red animate-pulse pointer-events-none z-10"
></ div >
{ /if }
< button
bind:this = { boxSelectButton }
class="toolbar-btn { isBoxSelectMode ? 'active' : '' } relative "
title = "Select Area (B)"
on:click = { toggleBoxSelectMode }
>
< Square size = { 16 } / >
</ button >
</ div >
< button class = "toolbar-btn" title = "Refresh (R)" on:click = { handleRefresh } >
< RefreshCw size = { 16 } / >
</ button >
@@ -983,6 +1037,30 @@
< option value = "satellite" > Satellite</ option >
</ select >
</ div >
{ #if showBoxTooltip && boxTooltipPosition }
< div
class = "absolute z-[1300] pointer-events-none"
style = "left: { boxTooltipPosition . left } px; top: { boxTooltipPosition . top } px; transform: translateX(-50%);"
>
< div
class = "bg-bg-secondary border border-border-color rounded-lg px-3 py-2 shadow-lg text-sm text-text-primary relative"
>
< div
class = "absolute -top-2 left-1/2 -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-b-4 border-l-transparent border-r-transparent border-b-border-color"
></ div >
< div
class = "absolute -top-[7px] left-1/2 -translate-x-1/2 w-0 h-0 border-l-[7px] border-r-[7px] border-b-[7px] border-l-transparent border-r-transparent border-b-bg-secondary"
></ div >
< div class = "relative z-10" > Click here to draw a box and search for cameras</ div >
< button
class = "absolute top-1 right-1 text-text-secondary hover:text-text-primary pointer-events-auto"
on:click = { dismissBoxTooltip }
>
×
</ button >
</ div >
</ div >
{ /if }
</ div >
< button
class = "sm:hidden fixed bottom-4 left-4 z-[1100] bg-bg-secondary border border-border-color rounded-full px-3 py-2 text-xs font-semibold text-text-primary shadow-lg"