diff --git a/config/config.example.toml b/config/config.example.toml index fa20ebc..1a7a8dd 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -14,12 +14,22 @@ defaultNestName = "Unknown Nest" url = "http://127.0.0.1:7272" secret = "" -# Optional Koji integrtion. Uncomment to disable +# Optional Koji integration. Uncomment to disable [server.koji] url = "http://127.0.0.1:8080" secret = "secret" projectName = "reactmap" +# Filter geofences based on user permissions +# When enabled, the /api/koji endpoint will only return geofences for areas +# the user has permission to access. This affects the "Show Map Fences" feature. +# +# Example: If a user only has permissions for ["Aurora", "Wheaton"], they will +# only see those two geofences on the map, not all geofences from Koji. +# +# Users with "everywhere" permissions (no area restrictions) will see all fences. +filterByPermissions = false + # Optional nominatim integration. Uncomment to disable [server.nominatim] url = "http://127.0.0.1:500" @@ -105,6 +115,9 @@ defaultLat = 51.516855 defaultLon = -0.080500 defaultZoom = 15 +# Default setting for showing area fences on map (new users only) +defaultShowMapFences = false + ### Map Styles [[client.mapStyles]] diff --git a/messages/de.json b/messages/de.json index 61ee12d..4bfbd1d 100644 --- a/messages/de.json +++ b/messages/de.json @@ -119,6 +119,8 @@ "boosted": "Gestärkt", "settings_show_debug_title": "Map-Debug-Menü anzeigen", "settings_show_debug_description": "Zeigt detaillierte Informationen über die App", + "settings_show_map_fences_title": "Gebiete anzeigen", + "settings_show_map_fences_description": "Gebietsgrenzen von Koji auf der Karte anzeigen", "search_address_loading": "Lädt...", "search_address_no_place_found": "Nichts gefunden", "search_area_no_areas_found": "Keine Gebiete gefunden", diff --git a/messages/es.json b/messages/es.json index 248d12a..90ef450 100644 --- a/messages/es.json +++ b/messages/es.json @@ -119,6 +119,8 @@ "boosted": "Impulsado", "settings_show_debug_title": "Mostrar el menú de depuración del mapa", "settings_show_debug_description": "Mostrar información detallada sobre la aplicación", + "settings_show_map_fences_title": "Mostrar límites del mapa", + "settings_show_map_fences_description": "Mostrar los límites de las áreas de Koji en el mapa", "search_address_loading": "Cargando...", "search_address_no_place_found": "No encontré nada", "search_area_no_areas_found": "No se encontraron áreas", diff --git a/messages/pt.json b/messages/pt.json index 530c8d9..8bfcdd3 100644 --- a/messages/pt.json +++ b/messages/pt.json @@ -119,6 +119,8 @@ "boosted": "Aprimorado", "settings_show_debug_title": "Exibir menu de depuração do mapa", "settings_show_debug_description": "Exibir informações detalhadas sobre o aplicativo", + "settings_show_map_fences_title": "Mostrar limites do mapa", + "settings_show_map_fences_description": "Exibir os limites das áreas do Koji no mapa", "search_address_loading": "Carregando...", "search_address_no_place_found": "Não encontrei nada.", "search_area_no_areas_found": "Nenhuma área encontrada", diff --git a/src/app.css b/src/app.css index bd79c87..9f075e3 100644 --- a/src/app.css +++ b/src/app.css @@ -51,7 +51,8 @@ --tier-4: var(--color-emerald-600); --nest-polygon: rgba(152, 248, 163, 0.3); --nest-polygon-stroke: rgba(152, 248, 163, 0.6); - --nest-polygon-selected: rgba(165, 243, 174, 0.5); + --fence-fill: rgba(100, 149, 237, 0.15); + --fence-stroke: rgba(100, 149, 237, 0.7); --nest-circle: rgba(121, 241, 135, 0.8); --nest-circle-stroke: rgba(152, 248, 163, 0.6); --spawnpoint: rgba(116, 223, 253, 0.6); diff --git a/src/components/map/FenceLayer.svelte b/src/components/map/FenceLayer.svelte new file mode 100644 index 0000000..642b0a0 --- /dev/null +++ b/src/components/map/FenceLayer.svelte @@ -0,0 +1,30 @@ + + + + + + diff --git a/src/components/map/Map.svelte b/src/components/map/Map.svelte index 733514b..e8ae1a4 100644 --- a/src/components/map/Map.svelte +++ b/src/components/map/Map.svelte @@ -21,6 +21,7 @@ } from "@/lib/map/events"; import maplibre from "maplibre-gl"; import GeometryLayer from "@/components/map/GeometryLayer.svelte"; + import FenceLayer from "@/components/map/FenceLayer.svelte"; import DebugMenu from "@/components/map/DebugMenu.svelte"; import { hasLoadedFeature, LoadedFeature } from "@/lib/services/initialLoad.svelte.js"; import { openToast } from "@/lib/ui/toasts.svelte.js"; @@ -28,6 +29,7 @@ import MarkerCurrentLocation from "@/components/map/MarkerCurrentLocation.svelte"; import MarkerContextMenu from "@/components/map/MarkerContextMenu.svelte"; import { getCurrentScoutData } from "@/lib/features/scout.svelte.js"; + import { getMapFencesGeojson } from "@/lib/features/mapFences.svelte"; import { Coords } from "@/lib/utils/coordinates"; import { isAnyModalOpen } from "@/lib/ui/modal.svelte.js"; import { @@ -161,6 +163,9 @@ + {#if getUserSettings().showMapFences && hasLoadedFeature(LoadedFeature.KOJI)} + + {/if} + onSettingsChange("showMapFences", !getUserSettings().showMapFences)} + value={getUserSettings().showMapFences} + /> + + +function buildMapFencesGeojson(): FeatureCollection { + const geofences = getKojiGeofences() + const styles = typeof document !== 'undefined' + ? getComputedStyle(document.documentElement) + : null + const strokeColor = styles?.getPropertyValue('--fence-stroke') || 'rgba(100, 149, 237, 0.7)' + const fillColor = styles?.getPropertyValue('--fence-fill') || 'rgba(100, 149, 237, 0.15)' + + return { + type: 'FeatureCollection', + features: geofences.map((fence, index): MapFenceFeature => ({ + type: 'Feature', + geometry: fence.geometry, + id: `fence-${index}`, + properties: { + id: `fence-${index}`, + name: fence.properties.name, + strokeColor, + fillColor + } + })) + } +} + +export function getMapFencesGeojson(): FeatureCollection { + return buildMapFencesGeojson() +} diff --git a/src/lib/map/layers.ts b/src/lib/map/layers.ts index d3fe227..e725984 100644 --- a/src/lib/map/layers.ts +++ b/src/lib/map/layers.ts @@ -7,6 +7,7 @@ export enum MapSourceId { SELECTED_WEATHER = "selectedWeather", SCOUT_BIG_POINTS = "scoutBigPoints", SCOUT_SMALL_POINTS = "scoutSmallPoints", + MAP_FENCES = "mapFences", } export enum MapObjectLayerId { diff --git a/src/lib/services/config/configTypes.d.ts b/src/lib/services/config/configTypes.d.ts index a285dc7..806259c 100644 --- a/src/lib/services/config/configTypes.d.ts +++ b/src/lib/services/config/configTypes.d.ts @@ -36,6 +36,7 @@ type General = { defaultLat?: number defaultLon?: number defaultZoom?: number + defaultShowMapFences?: boolean } export type DbCreds = { @@ -108,6 +109,7 @@ export type ServerConfig = { url: string secret: string projectName: string + filterByPermissions?: boolean } nominatim?: { url: string diff --git a/src/lib/services/userSettings.svelte.ts b/src/lib/services/userSettings.svelte.ts index fb26832..7652bb7 100644 --- a/src/lib/services/userSettings.svelte.ts +++ b/src/lib/services/userSettings.svelte.ts @@ -44,6 +44,7 @@ export type UserSettings = { loadMapObjectsWhileMoving: boolean; loadMapObjectsPadding: number; showDebugMenu: boolean; + showMapFences: boolean; mapIconSize: number; searchRange: number; filters: { @@ -87,6 +88,7 @@ export function getDefaultUserSettings(): UserSettings { loadMapObjectsWhileMoving: false, loadMapObjectsPadding: 20, showDebugMenu: false, + showMapFences: general.defaultShowMapFences ?? false, mapIconSize: 1, searchRange: 20_000, filters: { diff --git a/src/routes/api/koji/+server.ts b/src/routes/api/koji/+server.ts index c2ef807..ff28c81 100644 --- a/src/routes/api/koji/+server.ts +++ b/src/routes/api/koji/+server.ts @@ -1,8 +1,32 @@ import { error, json } from '@sveltejs/kit'; import { fetchKojiGeofences } from '@/lib/server/api/kojiApi'; +import { getServerConfig } from '@/lib/services/config/config.server'; +import type { KojiFeatures } from '@/lib/features/koji'; export async function GET(event) { const data = await fetchKojiGeofences(event.fetch) if (!data) error(500) + + const kojiConfig = getServerConfig().koji + if (kojiConfig?.filterByPermissions) { + const perms = event.locals.perms + + // If user has any "everywhere" permissions, they can see all fences + if (perms.everywhere && perms.everywhere.length > 0) { + return json(data) + } + + // Otherwise, filter to only show fences matching user's permitted areas + const permittedAreaNames = new Set( + perms.areas.map(area => area.name.toLowerCase()) + ) + + const filteredData: KojiFeatures = data.filter( + fence => permittedAreaNames.has(fence.properties.name.toLowerCase()) + ) + + return json(filteredData) + } + return json(data) }