Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion config/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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]]
Expand Down
2 changes: 2 additions & 0 deletions messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
30 changes: 30 additions & 0 deletions src/components/map/FenceLayer.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script lang="ts">
import { FillLayer, GeoJSON, LineLayer } from "svelte-maplibre";
import type { FeatureCollection, Polygon } from "geojson";
import { MapSourceId } from "@/lib/map/layers";

let {
data
}: {
data: FeatureCollection<Polygon>
} = $props();
</script>

<GeoJSON
id={MapSourceId.MAP_FENCES}
{data}
>
<FillLayer
paint={{
'fill-color': ["get", "fillColor"],
'fill-opacity': 0.5,
}}
/>
<LineLayer
layout={{ 'line-cap': 'round', 'line-join': 'round' }}
paint={{
'line-color': ["get", "strokeColor"],
'line-width': 2
}}
/>
</GeoJSON>
5 changes: 5 additions & 0 deletions src/components/map/Map.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
} 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";
import { addMapObjects } from "@/lib/mapObjects/mapObjectsState.svelte";
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 {
Expand Down Expand Up @@ -161,6 +163,9 @@
<GeometryLayer id={MapSourceId.SELECTED_WEATHER} reactive={false} />
<GeometryLayer id={MapSourceId.SCOUT_BIG_POINTS} data={getCurrentScoutData().bigPoints} />
<GeometryLayer id={MapSourceId.SCOUT_SMALL_POINTS} data={getCurrentScoutData().smallPoints} />
{#if getUserSettings().showMapFences && hasLoadedFeature(LoadedFeature.KOJI)}
<FenceLayer data={getMapFencesGeojson()} />
{/if}

<GeoJSON
id={MapSourceId.MAP_OBJECTS}
Expand Down
7 changes: 7 additions & 0 deletions src/components/menus/profile/SectionAdvanced.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@
/>
</div>

<Toggle
title={m.settings_show_map_fences_title()}
description={m.settings_show_map_fences_description()}
onclick={() => onSettingsChange("showMapFences", !getUserSettings().showMapFences)}
value={getUserSettings().showMapFences}
/>

<Toggle
title={m.settings_show_debug_title()}
description={m.settings_show_debug_description()}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/features/koji.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export async function loadKojiGeofences() {
if (!result.ok) {
console.error("Error while fetching geofences")
}

geofences = await result.json()
}

Expand Down
39 changes: 39 additions & 0 deletions src/lib/features/mapFences.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Feature, FeatureCollection, Polygon } from 'geojson';
import { getKojiGeofences } from './koji';

type MapFenceProperties = {
id: string
name: string
strokeColor: string
fillColor: string
}

type MapFenceFeature = Feature<Polygon, MapFenceProperties>

function buildMapFencesGeojson(): FeatureCollection<Polygon, MapFenceProperties> {
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<Polygon, MapFenceProperties> {
return buildMapFencesGeojson()
}
1 change: 1 addition & 0 deletions src/lib/map/layers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export enum MapSourceId {
SELECTED_WEATHER = "selectedWeather",
SCOUT_BIG_POINTS = "scoutBigPoints",
SCOUT_SMALL_POINTS = "scoutSmallPoints",
MAP_FENCES = "mapFences",
}

export enum MapObjectLayerId {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/services/config/configTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type General = {
defaultLat?: number
defaultLon?: number
defaultZoom?: number
defaultShowMapFences?: boolean
}

export type DbCreds = {
Expand Down Expand Up @@ -108,6 +109,7 @@ export type ServerConfig = {
url: string
secret: string
projectName: string
filterByPermissions?: boolean
}
nominatim?: {
url: string
Expand Down
2 changes: 2 additions & 0 deletions src/lib/services/userSettings.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type UserSettings = {
loadMapObjectsWhileMoving: boolean;
loadMapObjectsPadding: number;
showDebugMenu: boolean;
showMapFences: boolean;
mapIconSize: number;
searchRange: number;
filters: {
Expand Down Expand Up @@ -87,6 +88,7 @@ export function getDefaultUserSettings(): UserSettings {
loadMapObjectsWhileMoving: false,
loadMapObjectsPadding: 20,
showDebugMenu: false,
showMapFences: general.defaultShowMapFences ?? false,
mapIconSize: 1,
searchRange: 20_000,
filters: {
Expand Down
24 changes: 24 additions & 0 deletions src/routes/api/koji/+server.ts
Original file line number Diff line number Diff line change
@@ -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)
}