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)
}