Skip to content

Commit ac1c483

Browse files
authored
Merge pull request #672 from SlideRuleEarth/carlos-dev3
Carlos dev3 popup cleanup
2 parents f5c21f1 + ad42e76 commit ac1c483

File tree

5 files changed

+269
-86
lines changed

5 files changed

+269
-86
lines changed
Lines changed: 166 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,188 @@
1-
<template>
2-
<Teleport to="body">
3-
<div
4-
class="feature-menu-overlay"
5-
v-if="visible"
6-
:style="menuStyle"
7-
@mousedown.stop
8-
@click.stop
9-
>
10-
<div
11-
v-for="(feature, index) in menuData as Feature<Geometry>[]"
12-
:key="index"
13-
class="feature-menu-item"
14-
@click="handleSelect(feature)"
15-
>
16-
{{ feature.get('name') || feature.getId() || `Feature ${index + 1}` }}
17-
</div>
18-
</div>
19-
</Teleport>
20-
</template>
21-
221
<script setup lang="ts">
23-
import { ref } from 'vue';
24-
import type { Feature } from 'ol';
25-
import type Geometry from 'ol/geom/Geometry';
2+
import { ref, computed, defineExpose } from 'vue';
3+
import SrFeatureTreeNode, { type FeatureNode, type MiniFeature, type SelectPayload } from './SrFeatureTreeNode.vue';
4+
import { useRecTreeStore } from '@/stores/recTreeStore';
5+
6+
// --- utilities to accept anything from OL + clusters and keep a tiny feature API
7+
function isMiniFeature(o: any): o is MiniFeature {
8+
return !!o && typeof o.get === 'function' && typeof o.getId === 'function' && typeof o.getGeometry === 'function';
9+
}
10+
function unwrapCluster(item: any): MiniFeature[] {
11+
const contained = item?.get?.('features');
12+
if (Array.isArray(contained) && contained.length && isMiniFeature(contained[0])) return contained as MiniFeature[];
13+
return isMiniFeature(item) ? [item] : [];
14+
}
15+
function coerceToMiniFeatures(arr: any[]): MiniFeature[] {
16+
const out: MiniFeature[] = [];
17+
for (const x of arr ?? []) out.push(...unwrapCluster(x));
18+
return out;
19+
}
20+
function safeLabel(f: MiniFeature, i: number) {
21+
return (f.get('name') as string) || (f.getId()?.toString() ?? '') || `Feature ${i + 1}`;
22+
}
2623
27-
const menuData = ref<Feature<Geometry>[]>([]); // use a strongly typed array
28-
// Internal state
24+
const menuData = ref<MiniFeature[]>([]);
2925
const visible = ref(false);
3026
const menuStyle = ref<Record<string, string>>({});
3127
28+
const emit = defineEmits<{ (e: 'select', payload: SelectPayload): void }>();
3229
33-
// Define Emits
34-
const emit = defineEmits<{
35-
(e: 'select', feature: Feature<Geometry>): void
36-
}>();
30+
// ----- Build reqId -> { keys, labels, reqIds, order } from recTreeStore.treeData
31+
type RecPath = { keys: string[]; labels: string[]; reqIds: number[]; order: number[] };
32+
const recTree = useRecTreeStore();
3733
38-
// Public methods
39-
const showMenu = (x: number, y: number, features: Feature<Geometry>[]) => {
40-
visible.value = true;
41-
menuData.value = features;
34+
function buildIndex(): Map<number, RecPath> {
35+
const idx = new Map<number, RecPath>();
36+
let seq = 0;
37+
function walk(nodes: any, keys: string[], labels: string[], reqIds: number[], order: number[]) {
38+
for (const n of (nodes || [])) {
39+
const k = [...keys, n.key];
40+
const l = [...labels, n.label];
41+
const r = [...reqIds, Number(n.data) || 0]; // every node in your recTree has data=reqId
42+
const o = [...order, seq++];
43+
// index by this node's reqId too, so *every level* is individually addressable
44+
const thisReqId = Number(n.data) || 0;
45+
if (thisReqId > 0) idx.set(thisReqId, { keys: k, labels: l, reqIds: r, order: o });
46+
if (n.children?.length) walk(n.children, k, l, r, o);
47+
}
48+
}
49+
walk(recTree.treeData, [], [], [], []);
50+
return idx;
51+
}
52+
53+
// ensure a path exists; attach {kind:'record', reqId} payload at each level
54+
function ensurePath(root: FeatureNode[], path: RecPath): FeatureNode {
55+
let cursor = root;
56+
let node: FeatureNode | undefined;
57+
for (let i = 0; i < path.labels.length; i++) {
58+
const key = `rec-${path.keys[i]}`;
59+
node = cursor.find(n => n.key === key);
60+
if (!node) {
61+
node = {
62+
key,
63+
label: path.labels[i],
64+
expanded: true,
65+
children: [],
66+
payload: path.reqIds[i] > 0 ? { kind: 'record', reqId: path.reqIds[i] } : undefined
67+
};
68+
cursor.push(node);
69+
} else if (!node.payload && path.reqIds[i] > 0) {
70+
node.payload = { kind: 'record', reqId: path.reqIds[i] };
71+
}
72+
if (!node.children) node.children = [];
73+
cursor = node.children;
74+
}
75+
return node!;
76+
}
77+
78+
// sort to match records traversal order
79+
function makeSorter(orderMap: Map<string, number[]>) {
80+
return (a: FeatureNode, b: FeatureNode) => {
81+
const ao = orderMap.get(a.key) ?? [Number.MAX_SAFE_INTEGER];
82+
const bo = orderMap.get(b.key) ?? [Number.MAX_SAFE_INTEGER];
83+
const len = Math.min(ao.length, bo.length);
84+
for (let i = 0; i < len; i++) if (ao[i] !== bo[i]) return ao[i] - bo[i];
85+
return ao.length - bo.length || a.label.localeCompare(b.label);
86+
};
87+
}
88+
89+
const rootNodes = computed<FeatureNode[]>(() => {
90+
const items = menuData.value;
91+
if (!items?.length) return [];
92+
93+
const idx = buildIndex();
94+
const roots: FeatureNode[] = [];
95+
const orderMap = new Map<string, number[]>();
96+
97+
// Build tree
98+
items.forEach((f, i) => {
99+
const reqId = Number(f.get('req_id')) || 0;
100+
const path = reqId > 0 ? idx.get(reqId) : undefined;
101+
102+
// If we know the record path from the tree, build it and stamp ordering
103+
let recordLeaf: FeatureNode;
104+
if (path) {
105+
recordLeaf = ensurePath(roots, path);
106+
path.keys.forEach((k, j) => {
107+
const nk = `rec-${k}`;
108+
if (!orderMap.has(nk)) orderMap.set(nk, path.order.slice(0, j + 1));
109+
});
110+
} else {
111+
// fall back to a single "Unassigned" node
112+
const key = 'rec-unassigned';
113+
recordLeaf = roots.find(r => r.key === key) ?? (() => {
114+
const n: FeatureNode = { key, label: 'Unassigned', expanded: true, children: [] };
115+
roots.push(n);
116+
orderMap.set(key, [Number.MAX_SAFE_INTEGER]);
117+
return n;
118+
})();
119+
}
42120
43-
const menuWidth = 220;
44-
const menuHeight = 160;
121+
// Put features directly under the record (to match Analysis menu feel)
122+
const label = safeLabel(f, i);
45123
46-
let left = x;
47-
let top = y;
124+
// skip if it’s just a duplicate record polygon
125+
if (!label.startsWith('Record:')) {
126+
recordLeaf.children!.push({
127+
key: `${recordLeaf.key}-leaf-${i}-${(f.getId() ?? i).toString()}`,
128+
label,
129+
payload: { kind: 'feature', feature: f }
130+
});
131+
}
48132
49-
const ww = window.innerWidth;
50-
const wh = window.innerHeight;
133+
});
134+
135+
// Sort consistently with records order
136+
const sortLevel = makeSorter(orderMap);
137+
function sortTree(nodes: FeatureNode[]) {
138+
nodes.sort(sortLevel);
139+
nodes.forEach(n => { if (n.children?.length) sortTree(n.children); });
140+
}
141+
sortTree(roots);
142+
return roots;
143+
});
144+
145+
// forward selection
146+
function handleSelect(payload: SelectPayload) {
147+
emit('select', payload);
148+
hideMenu();
149+
}
150+
151+
const showMenu = (x: number, y: number, features: any[]) => {
152+
visible.value = true;
153+
menuData.value = coerceToMiniFeatures(features);
154+
155+
const menuWidth = 320, menuHeight = 360;
156+
let left = x, top = y;
157+
const ww = window.innerWidth, wh = window.innerHeight;
51158
if (left + menuWidth > ww) left = ww - menuWidth - 8;
52159
if (top + menuHeight > wh) top = wh - menuHeight - 8;
53-
left = Math.max(0, left);
54-
top = Math.max(0, top);
160+
left = Math.max(0, left); top = Math.max(0, top);
55161
56162
menuStyle.value = {
57-
position: 'absolute',
58-
left: `${left}px`,
59-
top: `${top}px`,
60-
zIndex: '1200',
61-
background: '#222',
62-
borderRadius: '6px',
63-
boxShadow: '0 3px 18px rgba(0,0,0,0.3)',
64-
padding: '8px 0',
65-
minWidth: '180px',
163+
position:'absolute', left:`${left}px`, top:`${top}px`, zIndex:'1200',
164+
background:'#222', borderRadius:'8px', boxShadow:'0 6px 24px rgba(0,0,0,0.35)',
165+
padding:'6px 0', minWidth:'260px', maxWidth:'420px', maxHeight:'60vh', overflow:'auto'
66166
};
67167
};
168+
const hideMenu = () => { visible.value = false; };
68169
69-
const hideMenu = () => {
70-
visible.value = false;
71-
};
72-
73-
// Emit selected feature
74-
function handleSelect(feature: Feature<Geometry>) {
75-
emit('select', feature);
76-
hideMenu();
77-
}
78-
79-
// Expose API
80170
defineExpose({ showMenu, hideMenu, menuData });
81171
</script>
82172

83-
<style scoped>
84-
.feature-menu-overlay {
85-
color: #fff;
86-
font-size: 1rem;
87-
user-select: none;
88-
max-width: 280px;
89-
}
90-
91-
.feature-menu-item {
92-
padding: 0.5rem 1rem;
93-
cursor: pointer;
94-
white-space: nowrap;
95-
}
173+
<template>
174+
<Teleport to="body">
175+
<div v-if="visible" class="feature-menu-overlay" :style="menuStyle" @mousedown.stop @click.stop>
176+
<div v-if="rootNodes.length === 0" class="menu-header">No features</div>
177+
<ul v-else class="tree-root">
178+
<SrFeatureTreeNode v-for="n in rootNodes" :key="n.key" :node="n" @select="handleSelect" />
179+
</ul>
180+
</div>
181+
</Teleport>
182+
</template>
96183

97-
.feature-menu-item:hover {
98-
background-color: #444;
99-
}
184+
<style scoped>
185+
.feature-menu-overlay { color:#fff; font-size:.95rem; user-select:none; }
186+
.menu-header { padding:.5rem .75rem; opacity:.8; }
187+
.tree-root { list-style:none; margin:0; padding-left:.25rem; }
100188
</style>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue';
3+
4+
// What we actually need from OL objects:
5+
export type MiniFeature = {
6+
get: (key: any) => any;
7+
getId: () => any;
8+
getGeometry: () => { getType?: () => string } | null | undefined;
9+
};
10+
11+
// Discriminated payload for any clickable node
12+
export type SelectPayload =
13+
| { kind: 'record'; reqId: number }
14+
| { kind: 'feature'; feature: MiniFeature };
15+
16+
export type FeatureNode = {
17+
key: string;
18+
label: string;
19+
expanded?: boolean;
20+
children?: FeatureNode[];
21+
// attach a payload when this node should be “clickable”
22+
payload?: SelectPayload;
23+
};
24+
25+
const props = defineProps<{ node: FeatureNode }>();
26+
27+
const emit = defineEmits<{
28+
(e: 'select', payload: SelectPayload): void
29+
}>();
30+
31+
const hasChildren = computed(() => !!props.node.children && props.node.children.length > 0);
32+
33+
function toggleExpand(e?: MouseEvent) {
34+
e?.stopPropagation();
35+
props.node.expanded = !props.node.expanded;
36+
}
37+
38+
function selectNode() {
39+
if (props.node.payload) emit('select', props.node.payload);
40+
else if (hasChildren.value) toggleExpand(); // fallback
41+
}
42+
</script>
43+
44+
<template>
45+
<li class="tree-node">
46+
<div class="tree-row" :class="{ branch: hasChildren, leaf: !hasChildren }" @click.stop="selectNode">
47+
<button v-if="hasChildren" class="chev" :class="node.expanded ? 'open' : 'closed'" @click.stop="toggleExpand" aria-label="toggle">
48+
49+
</button>
50+
<span v-else class="dot">•</span>
51+
<span class="label" :title="node.label">{{ node.label }}</span>
52+
</div>
53+
54+
<ul v-if="hasChildren && node.expanded" class="tree-children">
55+
<SrFeatureTreeNode
56+
v-for="child in node.children"
57+
:key="child.key"
58+
:node="child"
59+
@select="(p) => $emit('select', p)"
60+
/>
61+
</ul>
62+
</li>
63+
</template>
64+
65+
<style scoped>
66+
.tree-node { list-style: none; margin: 0; }
67+
.tree-row { display:flex; align-items:center; gap:.35rem; padding:.35rem .75rem; cursor:pointer; border-radius:6px; }
68+
.tree-row:hover { background:#333; }
69+
.branch .label { font-weight:600; } .leaf .label { font-weight:400; }
70+
.chev { width:1.1ch; transform:rotate(0); transition:transform 120ms ease; opacity:.85; background:transparent; border:0; padding:0; }
71+
.chev.open { transform:rotate(90deg); }
72+
.dot { width:1ch; text-align:center; opacity:.7; }
73+
.tree-children { margin:0; padding-left:.75rem; }
74+
</style>

web-client/src/components/SrMap.vue

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@
5858
import Polygon, { fromExtent as polygonFromExtent } from 'ol/geom/Polygon.js';
5959
import { unByKey } from 'ol/Observable.js';
6060
61+
import type { SelectPayload, MiniFeature } from '@/components/SrFeatureTreeNode.vue';
62+
import OLFeature from 'ol/Feature.js';
63+
6164
6265
6366
const dragAreaEl = document.createElement('div');
@@ -81,11 +84,29 @@
8184
const tooltipRef = ref();
8285
8386
const wasRecordsLayerVisible = ref(false);
84-
const isDrawing = ref(false);
87+
const isDrawing = ref(false);
88+
89+
function unwrapClusterToArray(item: MiniFeature): FeatureLike[] {
90+
const inner = item.get?.('features');
91+
return Array.isArray(inner) && inner.length ? (inner as FeatureLike[]) : [item as unknown as FeatureLike];
92+
}
93+
function toVectorFeatures(arr: FeatureLike[]): Feature<Geometry>[] {
94+
return arr.filter((f): f is Feature<Geometry> => f instanceof OLFeature);
95+
}
96+
97+
function onFeatureMenuSelect(payload: SelectPayload) {
98+
if (payload.kind === 'record') {
99+
// Do what your Analysis menu does: e.g. navigate/select/zoom
100+
router.push(`/analyze/${payload.reqId}`);
101+
featureMenuOverlayRef.value?.hideMenu();
102+
return;
103+
}
85104
86-
function onFeatureMenuSelect(feature: Feature<Geometry>) {
87-
onFeatureClick([feature]); // Use your existing handler logic
88-
featureMenuOverlayRef.value.hideMenu();
105+
// kind === 'feature'
106+
const likes = unwrapClusterToArray(payload.feature);
107+
const vectors = toVectorFeatures(likes);
108+
if (vectors.length) onFeatureClick(vectors); // your existing handler
109+
featureMenuOverlayRef.value?.hideMenu();
89110
}
90111
91112
const reqParamsStore = useReqParamsStore();

web-client/src/components/SrPlotCntrl.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ const shouldDisplayGradientColorLegend = computed(() => {
180180
const selectedColorEncodeData = chartStore.getSelectedColorEncodeData(reqIdStr.value);
181181
let should = false;
182182
if(selectedColorEncodeData && (selectedColorEncodeData !== 'unset')){
183-
if(func.includes('atl03')){
183+
if(func.includes('atl03') && (selectedColorEncodeData !== 'solid')){
184184
if(selectedColorEncodeData !== 'atl08_class' && selectedColorEncodeData !== 'atl24_class' && selectedColorEncodeData !== 'atl03_cnf'){
185185
should = true;
186186
}

0 commit comments

Comments
 (0)