Skip to content
Closed
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
5 changes: 5 additions & 0 deletions app/assets/viewer_svgs/basemap_around.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions app/assets/viewer_svgs/basemap_opacity.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions app/components/BasemapAttribution.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script setup lang="ts">
import { useBasemapStore } from "@ogw_front/stores/basemap";

const basemapStore = useBasemapStore();
</script>

<template>
<span
v-if="basemapStore.attribution"
data-testid="basemapAttribution"
class="basemap-attribution text-caption text-medium-emphasis px-1"
>
{{ basemapStore.attribution }}
</span>
</template>

<style scoped>
.basemap-attribution {
position: absolute;
left: 8px;
bottom: 4px;
z-index: 1;
background: rgba(var(--v-theme-surface), 0.6);
border-radius: 4px;
pointer-events: none;
}
</style>
39 changes: 39 additions & 0 deletions app/components/Viewer/Basemap/AroundOptions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script setup lang="ts">
import ViewerContextMenuItem, {
type ItemProps,
} from "@ogw_front/components/Viewer/ContextMenu/ContextMenuItem.vue";
import BasemapAround from "@ogw_front/assets/viewer_svgs/basemap_around.svg";
import { useBasemapStore } from "@ogw_front/stores/basemap";

interface Props {
itemProps: ItemProps & { index: number };
tooltip?: string;
}

const { itemProps, tooltip = "Map around" } = defineProps<Props>();
const basemapStore = useBasemapStore();
</script>

<template>
<ViewerContextMenuItem
data-testid="basemapAroundMenu"
:index="itemProps.index"
:itemProps="itemProps"
:tooltip="tooltip"
:btnImage="BasemapAround"
>
<template #options>
<!-- The flat map around the top surfaces draped with the map -->
<v-switch
:model-value="basemapStore.around"
label="Map around"
data-testid="basemapAround"
density="compact"
color="primary"
hide-details
class="px-1"
@update:model-value="(value) => basemapStore.setAround(Boolean(value))"
/>
</template>
</ViewerContextMenuItem>
</template>
53 changes: 53 additions & 0 deletions app/components/Viewer/Basemap/OpacityOptions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script setup lang="ts">
import ViewerContextMenuItem, {
type ItemProps,
} from "@ogw_front/components/Viewer/ContextMenu/ContextMenuItem.vue";
import BasemapOpacity from "@ogw_front/assets/viewer_svgs/basemap_opacity.svg";
import { mapStyleOf } from "@ogw_front/utils/basemap_tree";
import { useBasemapStore } from "@ogw_front/stores/basemap";

interface Props {
itemProps: ItemProps & { index: number };
tooltip?: string;
}

const { itemProps, tooltip = "Opacity" } = defineProps<Props>();
const basemapStore = useBasemapStore();
const PERCENT = 100;
const mapStyle = computed(() => mapStyleOf(itemProps.id));
// The slider works in percent, the store in 0..1
const opacityPercent = computed({
get: () => Math.round((mapStyle.value ? basemapStore.opacities[mapStyle.value] : 1) * PERCENT),
set: (value: number) => {
if (mapStyle.value) {
void basemapStore.setOpacity(mapStyle.value, value / PERCENT);
}
},
});
</script>

<template>
<ViewerContextMenuItem
data-testid="basemapOpacityMenu"
:index="itemProps.index"
:itemProps="itemProps"
:tooltip="tooltip"
:btnImage="BasemapOpacity"
>
<template #options>
<div class="d-flex align-center ga-2 px-1">
<v-slider
v-model="opacityPercent"
data-testid="basemapOpacity"
:min="10"
:max="100"
:step="5"
density="compact"
color="primary"
hide-details
/>
<span class="text-caption text-right" style="min-width: 4ch">{{ opacityPercent }}%</span>
</div>
</template>
</ViewerContextMenuItem>
</template>
6 changes: 5 additions & 1 deletion app/components/Viewer/ContextMenu/ContextMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,11 @@ function toggleShowName(): void {
@click="toggleShowName"
/>

<InfoCard v-model:show="showName" :meta-data="meta_data" />
<InfoCard
v-if="meta_data.viewer_type !== 'basemap'"
v-model:show="showName"
:meta-data="meta_data"
/>
</div>
</div>
</v-menu>
Expand Down
81 changes: 71 additions & 10 deletions app/components/Viewer/ObjectTree/Views/GlobalObjects.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
<script setup lang="ts">
// oxlint-disable max-dependencies
import {
type DataMenuPayload,
type GeodeObjectTypeMenuPayload,
compareSelections,
} from "@ogw_front/utils/treeview";
type BasemapStyle,
basemapGroup,
isBasemapId,
nextBasemapStyle,
withBasemapSelection,
} from "@ogw_front/utils/basemap_tree";
import { type TreeMenuPayload, compareSelections } from "@ogw_front/utils/treeview";
import CommonTreeView from "@ogw_front/components/Viewer/ObjectTree/Base/CommonTreeView.vue";
import type { DisplayItem } from "@ogw_front/composables/virtual_tree";
import ObjectTreeControls from "@ogw_front/components/Viewer/ObjectTree/Base/Controls.vue";
import ObjectTreeItemLabel from "@ogw_front/components/Viewer/ObjectTree/Base/ItemLabel.vue";
import { useBasemapStore } from "@ogw_front/stores/basemap";
import { useDataStore } from "@ogw_front/stores/data";
import { useDataStyleStore } from "@ogw_front/stores/data_style";
import { useHoverhighlight } from "@ogw_front/composables/hover_highlight";
Expand All @@ -19,10 +24,11 @@ const treeviewStore = useTreeviewStore();
const dataStore = useDataStore();
const dataStyleStore = useDataStyleStore();
const hybridViewerStore = useHybridViewerStore();
const basemapStore = useBasemapStore();
const { onHoverEnter, onHoverLeave } = useHoverhighlight();

interface Emits {
"show-menu": [payload: DataMenuPayload | GeodeObjectTypeMenuPayload];
"show-menu": [payload: TreeMenuPayload];
}

const emit = defineEmits<Emits>();
Expand Down Expand Up @@ -53,11 +59,56 @@ const {
applySearchFilter,
} = useTreeFilter(() => treeviewStore.items, { recursiveSort: true });

// The basemap category comes first, out of the data sort and filters
const displayedItems = computed(() =>
basemapStore.available ? [basemapGroup(), ...processedItems.value] : processedItems.value,
);

function basemapIds(selection: readonly string[]): string {
return selection.filter((id) => isBasemapId(id)).join(",");
}

// The tree shows the basemap state: one visible map, or none
watch(
() => basemapStore.style,
(style) => {
const synced = withBasemapSelection(treeviewStore.selection, style);
if (basemapIds(synced) !== basemapIds(treeviewStore.selection)) {
treeviewStore.selection = synced;
}
},
{ immediate: true },
);

function withoutBasemap(selection: readonly string[]): string[] {
return selection.filter((id) => !isBasemapId(id));
}

async function showBasemap(style: BasemapStyle): Promise<void> {
try {
await basemapStore.setStyle(style);
} catch {
// Already reported by the request feedback
}
}

// Only user clicks choose the visible map: clear and import rewrite the selection with data only.
// Basemap rows are never filtered out by the search
function onUpdateSelection(val: string[]): void {
treeviewStore.selection = applySearchFilter(val, treeviewStore.selection);
const previous = treeviewStore.selection;
const { added, removed } = compareSelections(val, previous);
const style = nextBasemapStyle(basemapStore.style, basemapStore.map, added, removed);
if (style !== basemapStore.style) {
void showBasemap(style);
}
const data = applySearchFilter(withoutBasemap(val), withoutBasemap(previous));
treeviewStore.selection = withBasemapSelection(data, style);
}

const visibleSelection = computed<string[]>(() => applySearchFilter(treeviewStore.selection, []));
const visibleSelection = computed<string[]>(() => [
...applySearchFilter(withoutBasemap(treeviewStore.selection), []),
...treeviewStore.selection.filter((id) => isBasemapId(id)),
]);

watch(
() => treeviewStore.selection,
Expand All @@ -69,6 +120,12 @@ watch(

const { added, removed } = compareSelections(current, previous);

// A selection written without the basemap rows (clear, import) shows the basemap state again
const synced = withBasemapSelection(current, basemapStore.style);
if (basemapIds(current) !== basemapIds(synced)) {
treeviewStore.selection = synced;
}

const allObjectIds = new Set(
treeviewStore.items.flatMap((group) => group.children.map((child) => child.id)),
);
Expand All @@ -88,6 +145,10 @@ watch(

function showMenu(event: MouseEvent, item: TreeGroupItem): void {
const actualItem = item.raw || item;
if (isBasemapId(actualItem.id)) {
emit("show-menu", { event, itemId: actualItem.id, context_type: "basemap" });
return;
}
if (!actualItem.children) {
emit("show-menu", { event, itemId: actualItem.id });
return;
Expand Down Expand Up @@ -150,7 +211,7 @@ function expandAll(): void {
}
}
}
traverse(treeviewStore.items);
traverse(displayedItems.value);
opened.value = allIds;
}
</script>
Expand All @@ -172,7 +233,7 @@ function expandAll(): void {
:selected="visibleSelection"
v-model:opened="opened"
v-model:active="treeviewStore.activeItems"
:items="processedItems"
:items="displayedItems"
:options="{
selection: { selectable: true },
search,
Expand All @@ -193,7 +254,7 @@ function expandAll(): void {
<ObjectTreeItemLabel
:item="item as unknown as DisplayItem"
:is-leaf="isLeaf"
:show-crs="isLeaf"
:show-crs="isLeaf && !isBasemapId(item.id as string)"
@contextmenu="showMenu($event, item as unknown as TreeGroupItem)"
/>
</template>
Expand Down
3 changes: 3 additions & 0 deletions app/components/Viewer/Ui.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type ViewerIdResult,
useOverlappingPicker,
} from "@ogw_front/composables/use_overlapping_picker";
import BasemapAttribution from "@ogw_front/components/BasemapAttribution.vue";
import OverlappingObjectsPicker from "@ogw_front/components/Viewer/OverlappingObjectsPicker.vue";
import ProjectCrs from "@ogw_front/components/ProjectCrs.vue";
import ToolActiveChip from "@ogw_front/components/Viewer/ToolActiveChip.vue";
Expand Down Expand Up @@ -110,6 +111,8 @@ defineExpose({ get_viewer_id });

<ProjectCrs />

<BasemapAttribution />

<OverlappingObjectsPicker
:display-intermediate="displayIntermediate"
:intermediate-items="intermediateItems"
Expand Down
5 changes: 5 additions & 0 deletions app/composables/project_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import back_schemas, {
import fileDownload from "js-file-download";
import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_typed_schemas.js";

import { type BasemapSnapshot, useBasemapStore } from "@ogw_front/stores/basemap";
import { type DataStyleSnapshot, useDataStyleStore } from "@ogw_front/stores/data_style";
import { type NewDataItem, useDataStore } from "@ogw_front/stores/data";
import { type TreeviewSnapshot, useTreeviewStore } from "@ogw_front/stores/treeview";
Expand Down Expand Up @@ -31,6 +32,7 @@ interface ProjectSnapshot {
hybridViewer?: CameraSnapshot;
dataStyle?: DataStyleSnapshot;
crs?: { projectCrs?: CRSList };
basemap?: BasemapSnapshot;
}

async function exportProject(): Promise<{ result: Blob }> {
Expand Down Expand Up @@ -79,6 +81,7 @@ async function importProject(file: File): Promise<void> {
const treeviewStore = useTreeviewStore();
const hybridViewerStore = useHybridViewerStore();
const crsStore = useCrsStore();
const basemapStore = useBasemapStore();

await viewerStore.ws_connect();

Expand All @@ -91,6 +94,7 @@ async function importProject(file: File): Promise<void> {
await dataStore.clear();
hybridViewerStore.clear();
crsStore.reset();
basemapStore.reset();

const importProjectSchema = back_schemas.opengeodeweb_back.import_project;
const form = new FormData();
Expand Down Expand Up @@ -123,6 +127,7 @@ async function importProject(file: File): Promise<void> {
? { projectCrs: oldestCrs(snapshot.data?.items ?? []) }
: snapshot.crs,
);
await basemapStore.importStores(snapshot.basemap);
await hybridViewerStore.importStores(snapshot.hybridViewer);
if (snapshot.dataStyle !== undefined) {
await dataStyleStore.importStores(snapshot.dataStyle);
Expand Down
10 changes: 10 additions & 0 deletions app/composables/viewer_context_menu.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type MenuMetaData, useMenuStore } from "@ogw_front/stores/menu";
import type { Ref, ShallowRef } from "vue";
import { BASEMAP_GROUP_ID } from "@ogw_front/utils/basemap_tree";
import type { TreeMenuPayload } from "@ogw_front/utils/treeview";
import type { ViewerIdResult } from "@ogw_front/composables/use_overlapping_picker";
import { useDataStore } from "@ogw_front/stores/data";
Expand All @@ -18,6 +19,15 @@ async function treeMenuTarget(
dataStore: ReturnType<typeof useDataStore>,
payload: TreeMenuPayload,
): Promise<MenuTarget | undefined> {
if (payload.context_type === "basemap") {
return {
id: payload.itemId,
meta_data: {
viewer_type: "basemap",
geode_object_type: payload.itemId === BASEMAP_GROUP_ID ? "Basemap" : "Map",
},
};
}
if (payload.context_type === "geode_object_type") {
const [referenceId] = payload.targetIds;
if (referenceId === undefined) {
Expand Down
Loading
Loading