Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
26fd683
feat(basemap): basemap store following the project CRS
MaxNumerique Oct 26, 2026
5a27e26
feat(basemap): toolbar menu with style and opacity, and Esri attribution
MaxNumerique Oct 26, 2026
74fbdff
fix(basemap): readable style and opacity menu
MaxNumerique Oct 27, 2026
50c13da
feat(basemap): map around switch for the draped basemap
MaxNumerique Oct 27, 2026
c2e88c6
trigger
MaxNumerique Oct 27, 2026
f3540d3
feat(basemap): shown with the project crs, one map at a time
MaxNumerique Nov 2, 2026
5ff821f
feat(basemap): basemap category in the object tree
MaxNumerique Nov 2, 2026
745d753
feat(basemap): opacity and map around context menus
MaxNumerique Nov 2, 2026
f920225
refactor(basemap): remove the toolbar tool
MaxNumerique Nov 2, 2026
05e651e
fix(basemap): keep the basemap through imports, hide the category wit…
MaxNumerique Nov 2, 2026
ff1bc97
Apply prepare changes
MaxNumerique Nov 2, 2026
e51e726
perf(basemap): read the data through the shared light crs query
MaxNumerique Nov 2, 2026
1da8aea
Apply prepare changes
MaxNumerique Nov 2, 2026
562f955
Merge branch 'feat/crs_assign' into feat/crs_basemap
MaxNumerique Nov 3, 2026
4f97014
feat(basemap): several maps shown together, crop to data on each map
MaxNumerique Nov 3, 2026
e282a48
Merge remote-tracking branch 'origin/next' into feat/crs_basemap
MaxNumerique Nov 3, 2026
66f692c
feat(basemap): satellite, plan and routes stacked in one basemap
MaxNumerique Nov 3, 2026
657e27e
Apply prepare changes
MaxNumerique Nov 3, 2026
0da57ed
perf(basemap): apply the map states with the basemap
MaxNumerique Nov 3, 2026
632a62e
style(basemap): primitive ternary branches only
MaxNumerique Nov 3, 2026
f625cb2
fix(basemap): keep map states in sync after a build and coalesce reco…
MaxNumerique Nov 3, 2026
e9a3e05
Merge branch 'next' of https://github.com/Geode-solutions/OpenGeodeWe…
MaxNumerique Nov 3, 2026
11878b2
oxlint
MaxNumerique Nov 3, 2026
f2ca261
feat(basemap): visible area of the map and its screen resolution
MaxNumerique Nov 4, 2026
c2433d4
feat(basemap): sharper map of the visible area once the camera rests
MaxNumerique Nov 4, 2026
5f05846
fix(basemap): detail store cleanup on failure, reset and finest zoom
MaxNumerique Nov 4, 2026
3251098
fix(basemap): remembered step never describes a detail off screen
MaxNumerique Nov 4, 2026
10df29e
fix(basemap): remember the step before clearing the detail
MaxNumerique Nov 4, 2026
515b399
fix(basemap): read the viewport size from the render window holder
MaxNumerique Nov 4, 2026
2ec719a
fix(basemap): drop the detail as soon as the view leaves it, show the…
MaxNumerique Nov 4, 2026
08851fb
refactor(basemap): shared viewer calls, tree rows derived from the maps
MaxNumerique Nov 4, 2026
02a98bf
Merge remote-tracking branch 'origin/next' into feat/crs_basemap
MaxNumerique Nov 4, 2026
6260d2d
refactor(basemap): split the basemap stores under the file size limit
MaxNumerique Nov 4, 2026
67b3f3e
test(basemap): response helpers at module scope
MaxNumerique Nov 4, 2026
61b53f2
perf(basemap): coalesce the detail layer sync
MaxNumerique Nov 4, 2026
b1ef280
fix(basemap): keep the built basemap while it is hidden
MaxNumerique Nov 4, 2026
66bcdc3
test(basemap): back source names and a neutral attribution
MaxNumerique Nov 4, 2026
0c079dd
fix(basemap): a superseded build keeps the maps of the newer one
MaxNumerique Nov 4, 2026
2a12753
trigger
MaxNumerique Nov 4, 2026
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.
28 changes: 28 additions & 0 deletions app/components/BasemapAttribution.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script setup lang="ts">
import { useBasemapStore } from "@ogw_front/stores/basemap";

const basemapStore = useBasemapStore();
// A hidden basemap stays built: its attribution goes with its maps
</script>

<template>
<span
v-if="basemapStore.attribution && basemapStore.maps.length > 0"
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>
51 changes: 51 additions & 0 deletions app/components/Viewer/Basemap/CropOptions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script setup lang="ts">
import { MAP_STYLES, mapStyleOf } from "@ogw_front/utils/basemap_tree";
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 = "Crop to data" } = defineProps<Props>();
const basemapStore = useBasemapStore();
// A map menu crops that map, the basemap category menu crops every map
const mapStyles = computed(() => {
const mapStyle = mapStyleOf(itemProps.id);
if (mapStyle === undefined) {
return MAP_STYLES;
}
return [mapStyle];
});
// Cropped: only the map draped on the top surfaces, without the flat map around
const cropped = computed(() => mapStyles.value.map((mapStyle) => basemapStore.crop[mapStyle]));
const indeterminate = computed(() => new Set(cropped.value).size > 1);
</script>

<template>
<ViewerContextMenuItem
data-testid="basemapCropMenu"
:index="itemProps.index"
:itemProps="itemProps"
:tooltip="tooltip"
:btnImage="BasemapAround"
>
<template #options>
<v-switch
:model-value="!indeterminate && cropped.every(Boolean)"
:indeterminate="indeterminate"
label="Crop to data"
data-testid="basemapCrop"
density="compact"
color="primary"
hide-details
class="px-1"
@update:model-value="(value) => basemapStore.setCrop(Boolean(value), mapStyles)"
/>
</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
59 changes: 49 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 MapStyle,
basemapGroup,
isBasemapId,
mapId,
nextBasemapMaps,
} 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,40 @@ const {
applySearchFilter,
} = useTreeFilter(() => treeviewStore.items, { recursiveSort: true });

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

// The basemap rows show the basemap state: they are never stored in the data selection
const basemapRows = computed<string[]>(() => basemapStore.maps.map((mapStyle) => mapId(mapStyle)));

async function showBasemap(maps: MapStyle[]): Promise<void> {
try {
await basemapStore.setMaps(maps);
} catch {
// Already reported by the request feedback
}
}

// Basemap rows are never filtered out by the search
function onUpdateSelection(val: string[]): void {
treeviewStore.selection = applySearchFilter(val, treeviewStore.selection);
const { added, removed } = compareSelections(val, basemapRows.value);
const maps = nextBasemapMaps(basemapStore.maps, basemapStore.shown, added, removed);
if (maps.join(",") !== basemapStore.maps.join(",")) {
void showBasemap(maps);
}
const data = val.filter((id) => !isBasemapId(id));
treeviewStore.selection = applySearchFilter(data, treeviewStore.selection);
}

const visibleSelection = computed<string[]>(() => applySearchFilter(treeviewStore.selection, []));
const visibleSelection = computed<string[]>(() => [
...applySearchFilter(treeviewStore.selection, []),
...basemapRows.value,
]);

watch(
() => treeviewStore.selection,
Expand Down Expand Up @@ -88,6 +123,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 @@ -155,7 +194,7 @@ function expandAll(): void {
}
}
}
traverse(treeviewStore.items);
traverse(displayedItems.value);
opened.value = allIds;
}
</script>
Expand All @@ -177,7 +216,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 @@ -198,7 +237,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
6 changes: 6 additions & 0 deletions app/components/Viewer/Ui.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ 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";
import type { TreeMenuPayload } from "@ogw_front/utils/treeview";
import ViewerContextMenu from "@ogw_front/components/Viewer/ContextMenu/ContextMenu.vue";
import ViewerObjectTreeLayout from "@ogw_front/components/Viewer/ObjectTree/Layout.vue";
import { getCurrentInstance } from "vue";
import { useBasemapDetailStore } from "@ogw_front/stores/basemap_detail";
import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
import { useMenuStore } from "@ogw_front/stores/menu";
import { useViewerStore } from "@ogw_front/stores/viewer";
Expand All @@ -29,6 +31,8 @@ const emit = defineEmits<{
const menuStore = useMenuStore();
const viewerStore = useViewerStore();
const hybridViewerStore = useHybridViewerStore();
// The sharper map of the visible area follows the camera of this viewer
useBasemapDetailStore();

const hoverHighlightLabel = computed(
() =>
Expand Down Expand Up @@ -110,6 +114,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 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 All @@ -15,7 +16,7 @@
import { useAppStore } from "@ogw_front/stores/app";
import { useBackStore } from "@ogw_front/stores/back";
import { useCrsStore } from "@ogw_front/stores/crs";
import { useFeedbackStore } from "@ogw_front/stores/feedback";

Check failure on line 19 in app/composables/project_manager.ts

View workflow job for this annotation

GitHub Actions / test / oxlint

import(max-dependencies)

app/composables/project_manager.ts:19:34: File has too many dependencies (18). Maximum allowed is 15.
import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
import { useViewerStore } from "@ogw_front/stores/viewer";

Expand All @@ -31,6 +32,7 @@
hybridViewer?: CameraSnapshot;
dataStyle?: DataStyleSnapshot;
crs?: { projectCrs?: CRSList };
basemap?: BasemapSnapshot;
}

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

await viewerStore.ws_connect();

Expand All @@ -91,6 +94,7 @@
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 @@
} else {
crsStore.importStores(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