diff --git a/main.go b/main.go index fb6919e..7bfd3dd 100644 --- a/main.go +++ b/main.go @@ -405,9 +405,6 @@ func (*Api) PostScenes(w http.ResponseWriter, r *http.Request) { } if data.Search != nil { sceneConfig.Scene.Search = string(*data.Search) - if sceneConfig.Layout.Type != layout.Strip { - sceneConfig.Layout.Type = layout.Search - } } scene := sceneSource.Add(sceneConfig, imageSource) @@ -439,9 +436,6 @@ func (*Api) GetScenes(w http.ResponseWriter, r *http.Request, params openapi.Get } if params.Search != nil { sceneConfig.Scene.Search = string(*params.Search) - if sceneConfig.Layout.Type != layout.Strip { - sceneConfig.Layout.Type = layout.Search - } } collection := getCollectionById(string(params.CollectionId)) if collection == nil { diff --git a/ui/package-lock.json b/ui/package-lock.json index 08b61e4..0a1bfee 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -19,6 +19,7 @@ "date-fns": "^2.28.0", "deep-iterator": "^1.1.0", "fast-deep-equal": "^3.1.3", + "kalmanjs": "^1.1.0", "ol": "^6.15.1", "overlayscrollbars": "^1.13.2", "overlayscrollbars-vue": "^0.3.0", @@ -2557,6 +2558,11 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/kalmanjs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kalmanjs/-/kalmanjs-1.1.0.tgz", + "integrity": "sha512-Gkm2DAKIX8geuDcEJ92e80EmQ6TyIzWBJk6AFk+aDLofQ4G/KoDe1RCY9WMuXFe0NQxsTi7HYh9BVQneOtlvkg==" + }, "node_modules/lerc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", @@ -5606,6 +5612,11 @@ "universalify": "^2.0.0" } }, + "kalmanjs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kalmanjs/-/kalmanjs-1.1.0.tgz", + "integrity": "sha512-Gkm2DAKIX8geuDcEJ92e80EmQ6TyIzWBJk6AFk+aDLofQ4G/KoDe1RCY9WMuXFe0NQxsTi7HYh9BVQneOtlvkg==" + }, "lerc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", diff --git a/ui/package.json b/ui/package.json index d7696fb..3e28481 100644 --- a/ui/package.json +++ b/ui/package.json @@ -17,6 +17,7 @@ "date-fns": "^2.28.0", "deep-iterator": "^1.1.0", "fast-deep-equal": "^3.1.3", + "kalmanjs": "^1.1.0", "ol": "^6.15.1", "overlayscrollbars": "^1.13.2", "overlayscrollbars-vue": "^0.3.0", diff --git a/ui/src/api.js b/ui/src/api.js index 2bf2dfc..a13bcf3 100644 --- a/ui/src/api.js +++ b/ui/src/api.js @@ -252,7 +252,7 @@ export function useScene({ reset(); loadSpeed.value = 0; - if (newValue.stale && !newValue.loading && !oldValue?.loading) { + if (newValue?.stale && !newValue?.loading && !oldValue?.loading) { console.log("scene stale, recreating..."); await recreateScene(); return; diff --git a/ui/src/components/CollectionView.vue b/ui/src/components/CollectionView.vue index b077aea..e17f714 100644 --- a/ui/src/components/CollectionView.vue +++ b/ui/src/components/CollectionView.vue @@ -1,5 +1,6 @@ diff --git a/ui/src/components/Controls.vue b/ui/src/components/Controls.vue index 0ae14d1..4a7848d 100644 --- a/ui/src/components/Controls.vue +++ b/ui/src/components/Controls.vue @@ -9,7 +9,7 @@
- {{ favorite ? "favorite" : "favorite_outline" }} + {{ favoriteTag ? "favorite" : "favorite_outline" }}
"/capabilities"); +const tagsSupported = computed(() => capabilities.value?.tags?.supported); + +const fileId = computed(() => region.value?.data?.id); + const emit = defineEmits([ "navigate", "exit", - "favorite", - "add-tag", - "remove-tag", ]); const { idle } = useIdle(5000, { @@ -75,7 +85,7 @@ const { idle } = useIdle(5000, { const showTags = ref(false); -const favorite = computed(() => { +const favoriteTag = computed(() => { return region.value?.data?.tags?.find(tag => tag.name == "fav"); }) @@ -91,16 +101,38 @@ const exit = () => { emit("exit"); } -const onFavorite = () => { - emit("favorite", favorite.value); -} - -const addTag = (tag) => { - emit("add-tag", tag); -} - -const removeTag = (tag) => { - emit("remove-tag", tag); +const toggleFavorite = async () => { + const tagId = favoriteTag?.id || "fav:r0"; + if (!fileId.value) { + return; + } + await postTagFiles(tagId, { + op: "INVERT", + file_id: fileId.value, + }); + await updateRegion(); +} + +const addTag = async (tagId) => { + if (!fileId.value || !tagId) { + return; + } + await postTagFiles(tagId, { + op: "ADD", + file_id: fileId.value, + }); + await updateRegion(); +} + +const removeTag = async (tagId) => { + if (!fileId.value || !tagId) { + return; + } + await postTagFiles(tagId, { + op: "SUBTRACT", + file_id: fileId.value, + }); + await updateRegion(); } onKeyStroke(["ArrowLeft"], left); diff --git a/ui/src/components/MapViewer.vue b/ui/src/components/MapViewer.vue index bda84ac..1336380 100644 --- a/ui/src/components/MapViewer.vue +++ b/ui/src/components/MapViewer.vue @@ -4,19 +4,25 @@ { if (scene?.name && scene?.name != "Map") return; recreateScene(); }); watch(scene, async (newScene, oldScene) => { - if (newScene?.search != oldScene?.search) { - scrollToPixels(0); - } emit("scene", newScene); }); +watch(region, async (newRegion, oldRegion) => { + regionTransition.value = !!((!newRegion && oldRegion) || (newRegion && !oldRegion)); +}, { immediate: true }); + +const { data: capabilities } = useApi(() => "/capabilities"); +const tagsSupported = computed(() => capabilities.value?.tags?.supported); + const contextMenu = ref(null); const { onContextMenu, @@ -146,18 +171,32 @@ const geoview = computed(() => { if (zstr.endsWith("z")) { zstr = zstr.slice(0, -1); } - + const lat = parseFloat(latstr); const lon = parseFloat(lonstr); const z = parseFloat(zstr); if (isNaN(lat) || isNaN(lon) || isNaN(z)) return; const geoview = [lon, lat, z]; + return geoview; }); -const applyGeoview = (geoview) => { +const geoviewView = computed(() => { + const view = Geoview.toView(geoview.value, scene.value?.bounds); + return view; +}); + +const view = computed(() => { + if (region.value) { + return region.value.bounds; + } + return geoviewView.value; +}); + +const applyGeoview = async (geoview) => { + if (!geoview) return; const [lon, lat, z] = geoview; - router.replace({ + await router.replace({ query: { ...router.currentRoute.value.query, p: `${lat.toFixed(7)},${lon.toFixed(7)},${z.toFixed(2)}z`, @@ -165,14 +204,95 @@ const applyGeoview = (geoview) => { }); } -const debouncedApplyGeoview = debounce(1000, applyGeoview); +const applyView = (view) => { + const pg = geoview.value; + const g = Geoview.fromView(view, scene.value?.bounds); + + if (Geoview.equal(g, pg)) return; + applyGeoview(g); +} + +const debouncedApplyView = debounce(1000, applyView); + +const onView = (view) => { + debouncedApplyView(view); + lastView.value = view; +} + +const lastView = ref(null); -const onGeoview = (geoview) => { - debouncedApplyGeoview(geoview); +const exit = async () => { + if (selectTagId.value) { + emit("selectTagId", null); + return; + } + if (!region.value) { + return; + } + const g = Geoview.fromView(lastView.value, scene.value?.bounds); + await applyGeoview(g); + await regionExit(); +} + +const externalExit = async () => { + if (!region.value) { + return; + } + const g = Geoview.fromView(view.value, scene.value?.bounds); + await applyGeoview([ + g[0], + g[1], + Math.max(1, g[2] - 3), + ]) + await regionExit(); +} + +const zoomOut = () => { + viewer.value?.setView(view.value); +} + +const onNav = async (event) => { + if (event.x) { + const valid = await navigate(event.x); + if (!valid) { + viewer.value?.setPendingTransition({ + t: 0.5, + x: (lastView.value?.x - view.value?.x) / 2, + ease: "out", + }); + zoomOut(); + } + return; + } + if (event.zoom < 0) { + await exit(); + return; + } + zoomOut(); } +const { + selectBounds +} = useTags({ + supported: tagsSupported, + selectTagId, + collectionId, + scene, +}); + const onClick = async (event) => { if (!event) return false; + if (region.value) return false; + if (tagsSupported.value && (selectTagId.value || event.originalEvent.ctrlKey)) { + const id = await selectBounds("INVERT", { + x: event.x, + y: event.y, + w: 0, + h: 0, + }); + emit("selectTagId", id); + return false; + } const regions = await getRegions(scene.value?.id, event.x, event.y, 0, 0); if (regions && regions.length > 0) { const region = regions[0]; @@ -182,6 +302,17 @@ const onClick = async (event) => { return false; } +const onBoxSelect = async (bounds, shift) => { + const op = shift ? "SUBTRACT" : "ADD"; + const id = await selectBounds(op, bounds); + emit("selectTagId", id); +} + +defineExpose({ + navigate, + exit: externalExit, +}) + diff --git a/ui/src/components/ScrollViewer.vue b/ui/src/components/ScrollViewer.vue index 347b8de..f001611 100644 --- a/ui/src/components/ScrollViewer.vue +++ b/ui/src/components/ScrollViewer.vue @@ -1,5 +1,5 @@