Skip to content

Commit

Permalink
Add map component and child components
Browse files Browse the repository at this point in the history
  • Loading branch information
chiatt committed Nov 22, 2024
1 parent 0e6deef commit 175c54d
Show file tree
Hide file tree
Showing 14 changed files with 1,418 additions and 0 deletions.
141 changes: 141 additions & 0 deletions afrc/src/afrc/Search/components/InteractiveMap/InteractiveMap.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<script setup lang="ts">
import { onMounted, provide, ref, watch } from "vue";
import { useGettext } from "vue3-gettext";
import { useToast } from "primevue/usetoast";
import MapComponent from "@/afrc/Search/components/InteractiveMap/components/MapComponent.vue";
import MapFilter from "@/afrc/Search/components/InteractiveMap/components/MapFilter/MapFilter.vue";
import InteractionsDrawer from "@/afrc/Search/components/InteractiveMap/components/InteractionsDrawer.vue";
import OverlayControls from "@/afrc/Search/components/InteractiveMap/components/OverlayControls.vue";
import BasemapControls from "@/afrc/Search/components/InteractiveMap/components/BasemapControls.vue";
import { fetchSettings } from "@/afrc/Search/api.ts";
import {
DEFAULT_ERROR_TOAST_LIFE,
ERROR,
} from "@/afrc/Search/constants.ts";
import type { Ref } from "vue";
import type { Feature, Map } from "maplibre-gl";
import type {
Basemap,
MapInteractionItem,
MapLayer,
MapSource,
Settings,
} from "@/afrc/Search/types.ts";
const toast = useToast();
const { $gettext } = useGettext();
const props = defineProps<{
overlays: MapLayer[];
basemaps: Basemap[];
sources: MapSource[];
}>();
const map: Ref<Map | null> = ref(null);
const settings: Ref<Settings | null> = ref(null);
const mapInteractionItems: MapInteractionItem[] = [
{
name: "Foo",
header: "Map Foo",
component: MapFilter,
icon: "pi pi-star",
},
{
name: "Bar",
header: "Map Bar",
component: OverlayControls,
icon: "pi pi-hashtag",
},
{
name: "Baz",
header: "Map Baz",
component: BasemapControls,
icon: "pi pi-globe",
},
];
const basemap: Ref<Basemap | null> = ref(null);
const selectedDrawnFeature: Ref<Feature | null> = ref(null);
const emits = defineEmits(["drawnFeatureSelected", "drawnFeaturesUpdated"]);
console.log(props);
provide("overlays", props.overlays);
provide("basemaps", props.basemaps);
provide("selectedDrawnFeature", selectedDrawnFeature);
watch(
() => props.basemaps,
(updatedBasemaps) => {
for (let updatedBasemap of updatedBasemaps) {
if (updatedBasemap.active) {
basemap.value = updatedBasemap;
break;
}
}
},
{ deep: true, immediate: true },
);
onMounted(async () => {
await fetchSystemSettings();
});
async function fetchSystemSettings() {
try {
console.log("Fetching settings");
settings.value = await fetchSettings();
} catch (error) {
toast.add({
severity: ERROR,
life: DEFAULT_ERROR_TOAST_LIFE,
summary: $gettext("Unable to fetch settings."),
detail: error instanceof Error ? error.message : undefined,
});
}
}
function updateSelectedDrawnFeature(feature: Feature) {
selectedDrawnFeature.value = feature;
emits("drawnFeatureSelected", feature);
}
</script>

<template>
<div
v-if="settings"
style="display: flex; height: 100%; width: 100%"
>
<MapComponent
:settings="settings"
:basemap="basemap"
:overlays="overlays"
:sources="sources"
:is-drawing-enabled="true"
@map-initialized="
(mapInstance) => {
map = mapInstance;
}
"
@drawn-features-updated="
(features) => {
emits('drawnFeaturesUpdated', features);
}
"
@drawn-feature-selected="updateSelectedDrawnFeature"
/>
<InteractionsDrawer
v-if="map"
:map="map"
:settings="settings"
:items="mapInteractionItems"
/>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script setup lang="ts">
import { inject, onMounted, ref, watch } from "vue";
import RadioButton from "primevue/radiobutton";
import type { Map } from "maplibre-gl";
import type { PropType } from "vue";
import type { Basemap } from "@/afrc/Search/types.ts";
defineProps({
map: {
type: Object as PropType<Map>,
required: true,
},
settings: {
type: Object as PropType<Record<string, unknown> | null>,
default: null,
},
});
const basemaps: Basemap[] = inject("basemaps", []);
const selectedBasemap = ref<Basemap | null>(null);
watch(selectedBasemap, (newBasemap) => {
basemaps.forEach((basemap) => {
basemap.active = basemap === newBasemap;
});
});
onMounted(() => {
const activeBasemap = basemaps.find((basemap) => basemap.active);
if (activeBasemap) {
selectedBasemap.value = activeBasemap;
}
});
</script>

<template>
<div
v-for="basemap in basemaps"
:key="basemap.id"
>
<RadioButton
v-model="selectedBasemap"
:value="basemap"
:label="basemap.name"
/>
<label>{{ basemap.name }}</label>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<script setup lang="ts">
import { computed, ref, shallowRef } from "vue";
import Button from "primevue/button";
import {
ACTIVE_LANGUAGE_DIRECTION,
LTR,
LEFT,
RIGHT,
} from "@/afrc/Search/constants.ts";
import type { Component, Ref } from "vue";
import type { Map } from "maplibre-gl";
import type {
MapInteractionItem,
Settings,
} from "@/afrc/Search/types.ts";
const props = defineProps<{
map: Map;
items: MapInteractionItem[];
settings: Settings | null;
}>();
const headerContent: Ref<string | null> = ref(null);
const selectedComponent: Ref<Component | null> = shallowRef(null);
const isOverlayVisible = ref(false);
const drawerPosition = computed(() => {
if (props.settings![ACTIVE_LANGUAGE_DIRECTION] === LTR) {
return RIGHT;
} else {
return LEFT;
}
});
const openDrawer = (item: MapInteractionItem) => {
headerContent.value = item.header;
selectedComponent.value = item.component;
isOverlayVisible.value = true;
};
</script>

<template>
<div class="sidebar-container">
<aside class="sidebar">
<div
v-for="item in items"
:key="item.name"
>
<Button
:icon="item.icon"
@click="
() => {
if (selectedComponent === item.component) {
isOverlayVisible = !isOverlayVisible;
} else {
openDrawer(item);
}
}
"
/>
</div>
</aside>

<div
class="sliding-panel"
:class="[drawerPosition, { 'is-visible': isOverlayVisible }]"
>
<div class="sliding-panel-header">
<span>{{ headerContent }}</span>
<button
class="close-button"
aria-label="Close"
@click="isOverlayVisible = false"
>
×
</button>
</div>

<component
:is="selectedComponent"
:map="props.map"
:settings="props.settings"
/>
</div>
</div>
</template>

<style scoped>
.sidebar-container {
position: relative;
display: flex;
height: 100%;
}
.sidebar {
display: flex;
flex-direction: column;
align-items: center;
width: 3rem;
border-right: 1px solid var(--p-menubar-border-color);
z-index: 1;
}
.sliding-panel {
position: absolute;
top: 0;
bottom: 0;
width: 18rem;
background-color: var(--p-content-background);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
z-index: 2;
overflow: auto;
}
.sliding-panel.left {
left: 3rem;
transform: translateX(-100%);
}
.sliding-panel.left.is-visible {
transform: translateX(0);
}
.sliding-panel.right {
right: 0;
transform: translateX(100%);
}
.sliding-panel.right.is-visible {
transform: translateX(0);
}
.sliding-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
font-weight: bold;
border-bottom: 1px solid var(--p-content-color);
}
.close-button {
background: none;
border: none;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
color: var(--p-content-color);
padding: 0;
margin: 0;
}
.close-button:focus {
outline: none;
}
</style>
Loading

0 comments on commit 175c54d

Please sign in to comment.