Feat major version viewer

This commit is contained in:
Jean Andreani
2024-04-04 09:37:25 +00:00
committed by Andreani Jean
parent 627d9d739f
commit d493af65c3
14 changed files with 308 additions and 235 deletions

View File

@@ -6,8 +6,8 @@
import axios from 'axios'
import { onMounted, onUnmounted, ref, computed } from 'vue'
import { useSequenceStore } from '@/store/sequence'
import { Viewer, StandaloneMap } from 'geovisio'
import { createUrlLink } from '@/utils'
import { Viewer, StandaloneMap, Editor } from 'geovisio'
import { createUrlLink, manageSlashUrl } from '@/utils'
import {
createLink,
createSequenceLink,
@@ -18,16 +18,19 @@ import { hasASessionCookieDecoded } from '@/utils/auth'
import type { ViewerInterface, MapInterface } from '@/views/interfaces/common'
const sequenceStore = useSequenceStore()
const { t } = useI18n()
const emit = defineEmits<{ (e: 'triggerReady', value: boolean): void }>()
let mapIsLoaded = ref<boolean>(false)
let viewer = ref()
const props = defineProps({
id: { type: String, default: 'viewer' },
fetchOptions: { type: Object, default: {} },
geovisioViewer: { type: Boolean, default: true },
viewerType: { type: String, default: 'viewer' },
bbox: { type: Array, default: null },
userId: { type: String, default: '' }
picId: { type: String, default: null },
seqId: { type: String, default: null },
userId: { type: String, default: '' },
seqBruteDeg: { type: Number, default: 0 },
roadDegrees: { type: Number, default: 0 }
})
const isLogged = computed((): boolean => {
const cookie = hasASessionCookieDecoded()
@@ -52,15 +55,15 @@ async function getSequenceId(imgId: string): Promise<{
}
}
function createViewerButton(link: HTMLDivElement): void {
link.innerHTML = `<div>${createFullScreenButton()}</div>`
link.innerHTML = `<div id='instanceBtn'>${createFullScreenButton()}</div>`
viewer.value.addEventListener(
'picture-loaded',
'psv:picture-loaded',
async (e: { detail: { picId: string } }): Promise<void> => {
const sequenceInformation = await getSequenceId(e.detail.picId)
let href: string
if (isLogged.value && sequenceInformation.username === userName.value) {
href = `${window.location.origin}/sequence/${sequenceInformation.sequenceId}?currentPic=${e.detail.picId}`
link.innerHTML = `<div>
link.innerHTML = `<div id='instanceBtn'>
${createFullScreenButton()}
${createSequenceLink(
href,
@@ -73,7 +76,7 @@ function createViewerButton(link: HTMLDivElement): void {
picId: e.detail.picId,
link: createUrlLink(e.detail.picId)
})
link.innerHTML = `<div>
link.innerHTML = `<div id='instanceBtn'>
${createFullScreenButton()}
${createLink(
href,
@@ -84,7 +87,7 @@ function createViewerButton(link: HTMLDivElement): void {
}
)
}
async function setupViewerMap(tiles: string): Promise<void> {
function setupViewer(tiles: string): void {
const maxZoom = import.meta.env.VITE_MAX_ZOOM
const zoom = import.meta.env.VITE_ZOOM
const center = import.meta.env.VITE_CENTER
@@ -131,6 +134,12 @@ async function setupViewerMap(tiles: string): Promise<void> {
}
}
}
if (props.picId) {
paramsViewer = {
...paramsViewer,
selectedPicture: props.picId
}
}
if (props.fetchOptions) {
paramsViewer = {
...paramsViewer,
@@ -141,7 +150,7 @@ async function setupViewerMap(tiles: string): Promise<void> {
reportLink.className = 'gvs-group gvs-group-large gvs-group-btnpanel'
viewer.value = new Viewer(
'viewer', // Div ID
`${import.meta.env.VITE_API_URL}/api/search`,
`${manageSlashUrl()}/api/search`,
{
...paramsViewer,
widgets: { customWidget: reportLink }
@@ -151,7 +160,7 @@ async function setupViewerMap(tiles: string): Promise<void> {
createViewerButton(reportLink)
}
}
async function setupMap(tiles: string): Promise<void> {
function setupStandAlone(tiles: string): void {
let paramsMap: MapInterface
paramsMap = { users: [props.userId], minZoom: 7 }
if (tiles && tiles.length) {
@@ -163,35 +172,69 @@ async function setupMap(tiles: string): Promise<void> {
const bbox = [props.bbox[0], props.bbox[1], props.bbox[2], props.bbox[3]]
viewer.value = new StandaloneMap(
props.id, // Div ID
`${import.meta.env.VITE_API_URL}/api/search`,
`${manageSlashUrl()}/api/search`,
{
...paramsMap,
bounds: bbox,
padding: { top: 70, bottom: 70, left: 70, right: 70 },
maxZoom: 14,
speed: 10,
zoom: 14
}
)
viewer.value.addEventListener('ready', () => {
viewer.value.fitBounds(bbox, {
padding: { top: 70, bottom: 70, left: 70, right: 70 },
maxZoom: 14,
speed: 10
})
})
}
function setupEditor(tiles: string): void {
const raster = import.meta.env.VITE_RASTER_TILE
let paramsMap: MapInterface
paramsMap = {
users: [props.userId],
minZoom: 7,
selectedSequence: props.seqId,
raster: JSON.parse(raster)
}
if (tiles && tiles.length) {
paramsMap = {
...paramsMap,
style: tiles
}
}
if (props.picId) {
paramsMap = {
...paramsMap,
selectedPicture: props.picId
}
}
if (props.fetchOptions) {
paramsMap = {
...paramsMap,
...props.fetchOptions
}
}
try {
viewer.value = new Editor(
'viewer', // Div ID
`${manageSlashUrl()}/api/search`,
{
...paramsMap
}
)
} catch (e) {
console.log(e)
}
}
onMounted(async (): Promise<void> => {
const tiles = import.meta.env.VITE_TILES
try {
if (props.geovisioViewer) await setupViewerMap(tiles)
else await setupMap(tiles)
if (props.viewerType === 'standAlone') setupStandAlone(tiles)
else if (props.viewerType === 'editor') setupEditor(tiles)
else setupViewer(tiles)
mapIsLoaded.value = true
emit('triggerReady', mapIsLoaded.value)
} catch (err) {
mapIsLoaded.value = true
console.log(err)
}
})
onUnmounted((): void => {
if (viewer.value && props.geovisioViewer) viewer.value.destroy()
if (viewer.value && props.viewerType) viewer.value.destroy()
})
</script>

View File

@@ -35,6 +35,7 @@
:road-degrees="roadDegrees"
:seq-brute-deg="angleValue"
@triggerAngle="captureAngle"
@triggerMovingAngle="triggerMovingAngle"
/>
</div>
<div class="entry-button">
@@ -54,8 +55,10 @@ import InformationCard from '@/components/InformationCard.vue'
import Button from '@/components/Button.vue'
import Input from '@/components/Input.vue'
import WidgetOrientation from '@/components/sequence/WidgetOrientation.vue'
import { modulo180 } from '@/utils/calc'
const emit = defineEmits<{
(e: 'triggerAngle', value: number): void
(e: 'triggerMovingAngle', value: number): void
}>()
let angleValue = ref<number>(0)
let angleInputValue = ref<number>(0)
@@ -77,8 +80,13 @@ function captureAngle(value: number | string) {
const valueNum = Number(value)
angleInputValue.value = valueNum
angleValue.value = valueNum + props.roadDegrees
const movingAngle = modulo180(angleValue.value, Math.round(props.roadDegrees))
emit('triggerMovingAngle', movingAngle)
if (isDisabled(valueNum)) return (errorAngleValue.value = true)
}
function triggerMovingAngle(value: number) {
emit('triggerMovingAngle', value)
}
function triggerAngle() {
const valueToSend = angleValue.value - Number(props.roadDegrees)
if (isDisabled(valueToSend)) return

View File

@@ -48,6 +48,7 @@
<script setup lang="ts">
import { onMounted, ref, watchEffect } from 'vue'
import { modulo180 } from '@/utils/calc'
let angleValue = ref<number>(0)
let angle = ref<number>(0)
let prevRotation = ref<number>(0)
@@ -60,6 +61,7 @@ let center: { x: number; y: number } = { x: 0, y: 0 }
const R2D: number = 180 / Math.PI
const emit = defineEmits<{
(e: 'triggerAngle', value: number): void
(e: 'triggerMovingAngle', value: number): void
}>()
const props = defineProps({
roadDegrees: { type: Number, default: 0 },
@@ -102,6 +104,7 @@ function mouseup(): void {
prevRotation.value = rotation.value
angleValue.value = angle.value
if (angleValue.value !== 0) {
emit('triggerMovingAngle', modulo180(angle.value, props.roadDegrees))
emit('triggerAngle', modulo180(angle.value, props.roadDegrees))
}
}
@@ -115,13 +118,6 @@ function clickAndMove(value: number): void {
let closestMultiple = Math.ceil(moduloAngle / 45) * value
return emit('triggerAngle', closestMultiple)
}
function modulo180(value: number, roadDegrees: number): number {
let moduloAngle = (value - roadDegrees) % 360
if (moduloAngle < -180) moduloAngle += 360
if (moduloAngle > 180) moduloAngle -= 360
return Math.round(moduloAngle)
}
</script>
<style scoped lang="scss">

View File

@@ -4,6 +4,7 @@ import HeaderOpen from '../../../../components/header/HeaderOpen.vue'
import i18n from '../../config'
import { createRouter, createWebHistory } from 'vue-router/dist/vue-router'
vi.mock('vue-router')
import.meta.env.VITE_API_URL = 'api-url/'
const router = createRouter({
history: createWebHistory(),
@@ -53,7 +54,6 @@ describe('Template', () => {
})
})
it('should render all the commons entries', () => {
import.meta.env.VITE_API_URL = 'api-url/'
const wrapper = shallowMount(HeaderOpen, {
global: {
plugins: [i18n, router]

View File

@@ -1,12 +1,13 @@
import pako from 'pako'
import { useCookies } from 'vue3-cookies'
import { manageSlashUrl } from '@/utils'
const { cookies } = useCookies()
function getAuthRoute(authRoute: string, nextRoute: string): string {
const next = `${location.protocol}//${location.host}${nextRoute}`
return `${
import.meta.env.VITE_API_URL
}api/${authRoute}?next_url=${encodeURIComponent(`${next}`)}`
return `${manageSlashUrl()}api/${authRoute}?next_url=${encodeURIComponent(
`${next}`
)}`
}
// This function to decode the flask cookie and have the user information like username

8
src/utils/calc.ts Normal file
View File

@@ -0,0 +1,8 @@
function modulo180(value: number, roadDegrees: number): number {
let moduloAngle = (value - roadDegrees) % 360
if (moduloAngle < -180) moduloAngle += 360
if (moduloAngle > 180) moduloAngle -= 360
return Math.round(moduloAngle)
}
export { modulo180 }

View File

@@ -1,3 +1,11 @@
export function createUrlLink(picId: string): string {
function createUrlLink(picId: string): string {
return encodeURIComponent(`${window.location.origin}/#focus=pic&pic=${picId}`)
}
function manageSlashUrl(): string {
let apiUrl = import.meta.env.VITE_API_URL
if (apiUrl.charAt(apiUrl.length - 1) !== '/') apiUrl += '/'
return apiUrl
}
export { createUrlLink, manageSlashUrl }

View File

@@ -6,9 +6,10 @@
<script setup lang="ts">
import { computed } from 'vue'
import { manageSlashUrl } from '@/utils'
const myAccountUrl = computed<string>(
() => `${import.meta.env.VITE_API_URL}oauth/realms/geovisio/account`
() => `${manageSlashUrl()}oauth/realms/geovisio/account`
)
</script>

View File

@@ -7,9 +7,15 @@
@trigger="menuIsOpen = !menuIsOpen"
/>
</div>
<section class="entry-viewer">
<section v-if="sequence" class="entry-viewer">
<Viewer
:key="viewerType"
:fetch-options="{ fetchOptions: { credentials: 'include' } }"
:pic-id="pictures[0].id"
:seq-id="sequence.id"
:viewer-type="viewerType"
:seq-brute-deg="seqBruteDeg"
:road-degrees="seqDegrees"
ref="viewerRef"
/>
</section>
@@ -162,6 +168,7 @@
:seq-brute-deg="seqBruteDeg"
:is-loading="isLoadingOrient"
@triggerAngle="orientSequence"
@triggerMovingAngle="orientMovingSequence"
/>
</div>
<div v-if="panelView === 'sort' && isSequenceOwner" class="entry-panel">
@@ -179,7 +186,7 @@
</template>
<script setup lang="ts">
import { onMounted, ref, watchEffect, computed } from 'vue'
import { onMounted, ref, watchEffect, nextTick, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useSequenceStore } from '@/store/sequence'
@@ -241,6 +248,7 @@ let isLoadingSort = ref<boolean>(false)
let seqDegrees = ref<number>(0)
let seqBruteDeg = ref<number>(0)
let panelView = ref<string>('photos')
let viewerType = ref<string>('viewer')
const collapseMenu = ref<HTMLDivElement>()
const deleteAll = ref<HTMLDivElement>()
const menuHeight = ref<string>('0')
@@ -268,22 +276,11 @@ onMounted(async () => {
if (itemSelected.value.length || !getCurrentPicId(collItemsReady[0].id)) {
return
}
await nextTick()
if (!viewerRef.value) return
viewerRef.value.viewer.addEventListener('picture-loaded', (): void => {
if (!viewerRef.value) return
const seqRelativeDeg = viewerRef.value.viewer.getPictureRelativeHeading()
seqBruteDeg.value =
viewerRef.value.viewer.getPictureMetadata().properties['view:azimuth']
seqDegrees.value = seqBruteDeg.value - seqRelativeDeg
})
if (!viewerRef.value) return
viewerRef.value.viewer._api.onceReady().then(() => {
if (!sequence.value || !viewerRef.value) return
viewerRef.value.viewer.goToPicture(
getCurrentPicId(collItemsReady[0].id),
sequence.value.id
)
})
const viewerMap = viewerRef.value.viewer
await viewerMap._api.onceReady()
if (sequence.value) viewerMap.select(sequence.value.id, itemSelected.value)
await goToThePageAndScroll()
} catch (err) {
console.log(err)
@@ -296,7 +293,7 @@ watchEffect(() => {
async function goToThePageAndScroll() {
if (!viewerRef || !viewerRef.value || !viewerRef.value.viewer) return
viewerRef.value.viewer.addEventListener(
'picture-loaded',
'psv:picture-loaded',
async (e: { detail: { picId: string } }): Promise<void> => {
if (!pictureExistInList(getCurrentPicId(e.detail.picId))) {
await goToTheGoodPage(getCurrentPicId(e.detail.picId))
@@ -311,8 +308,17 @@ async function goToThePageAndScroll() {
}
function setPanelView(value: string): void {
panelView.value = value
if (value === 'orientation') window.location.hash = '#background=aerial'
else window.location.hash = '#background=streets'
if (value === 'orientation') {
viewerType.value = 'editor'
setSeqDegrees()
} else viewerType.value = 'viewer'
}
function setSeqDegrees(): void {
if (!viewerRef.value) return
const seqRelativeDeg = viewerRef.value.viewer.psv.getPictureRelativeHeading()
seqBruteDeg.value =
viewerRef.value.viewer.psv.getPictureMetadata().properties['view:azimuth']
seqDegrees.value = seqBruteDeg.value - seqRelativeDeg
}
async function setNewSequenceTitle(value: string | null): Promise<void> {
isLoadingTitle.value = true
@@ -435,7 +441,7 @@ async function patchCollection(): Promise<void> {
const { data } = await fetchCollectionItems(route.params.id, '?limit=100')
pictures.value = data.features
}
if (viewerRef.value) viewerRef.value.viewer.reloadVectorTiles()
if (viewerRef.value) viewerRef.value.viewer.map.reloadVectorTiles()
isLoading.value = false
}
async function goToNextPage(value: string): Promise<void> {
@@ -459,6 +465,11 @@ function triggerCheck(value: CheckboxInterface): void {
.map((e) => e.id)
} else picturesToDelete.value = []
}
function orientMovingSequence(value: number): void {
if (!viewerRef.value) return
viewerRef.value.viewer.previewSequenceHeadingChange(value)
}
async function orientSequence(value: number): Promise<void> {
isLoadingOrient.value = true
const { data }: { data: ResponseUserSequenceInterface } =
@@ -466,7 +477,6 @@ async function orientSequence(value: number): Promise<void> {
relative_heading: value
})
formatSequenceFetched(data)
if (viewerRef.value) viewerRef.value.viewer.clearPictureMetadataCache()
sequenceStore.addToastText(t('pages.sequence.orientation_updated'), 'success')
isLoadingOrient.value = false
}
@@ -528,9 +538,9 @@ async function selectImageAndMove(
picturesToDelete.value.length < 2 &&
item.properties['geovisio:status'] === 'ready'
) {
if (viewerRef.value) {
const viewerMap = await viewerRef.value.viewer
viewerMap.goToPicture(item.id, sequence.value?.id)
if (viewerRef.value && sequence.value) {
const viewerMap = viewerRef.value.viewer
viewerMap.select(sequence.value.id, item.id)
}
itemSelected.value = item.id
await goToTheGoodPage(item.id)
@@ -575,8 +585,8 @@ async function patchOrDeleteCollectionItems(reqType: string): Promise<void> {
pictures.value = data.features
isLoading.value = false
if (viewerRef.value) {
viewerRef.value.viewer.reloadVectorTiles()
viewerRef.value.viewer.goToPicture(pictures.value[0].id, route.params.id)
viewerRef.value.viewer.map.reloadVectorTiles()
viewerRef.value.viewer.select(pictures.value[0].id, route.params.id)
}
scrollIntoSelected(
picturesToDelete.value[0],

View File

@@ -26,7 +26,7 @@
credentials: 'include'
}
}"
:geovisio-viewer="false"
viewer-type="standAlone"
:user-id="getUserId"
:bbox="collectionBbox"
ref="viewerRef"
@@ -372,10 +372,10 @@ function triggerBboxFilter(): void {
if (viewerRef.value) {
isLoading.value = true
const currentBbox = [
viewerRef.value.viewer._map.getBounds()._ne.lng,
viewerRef.value.viewer._map.getBounds()._ne.lat,
viewerRef.value.viewer._map.getBounds()._sw.lng,
viewerRef.value.viewer._map.getBounds()._sw.lat
viewerRef.value.viewer.map.getBounds()._ne.lng,
viewerRef.value.viewer.map.getBounds()._ne.lat,
viewerRef.value.viewer.map.getBounds()._sw.lng,
viewerRef.value.viewer.map.getBounds()._sw.lat
]
filterBbox.value = currentBbox
formatUri()
@@ -414,7 +414,7 @@ async function patchCollection(sequence: SequenceLinkInterface): Promise<void> {
else visible = 'true'
await patchACollection(sequence.id, { visible: visible })
await fetchAndFormatSequence()
if (viewerRef.value) viewerRef.value.viewer.reloadVectorTiles()
if (viewerRef.value) viewerRef.value.viewer.map.reloadVectorTiles()
isLoading.value = false
}
async function deleteCollection(
@@ -424,7 +424,7 @@ async function deleteCollection(
if (confirm(t('pages.sequence.conf_sequence_msg'))) {
await deleteACollection(sequence.id)
await fetchAndFormatSequence()
if (viewerRef.value) viewerRef.value.viewer.reloadVectorTiles()
if (viewerRef.value) viewerRef.value.viewer.map.reloadVectorTiles()
}
isLoading.value = false
}
@@ -490,10 +490,10 @@ function bboxIsInsideOther(mainBbox: number[], bboxInside: number[]): boolean {
function goToSequence(sequence: SequenceLinkInterface): void {
if (!viewerRef.value) return
const currentBbox = [
viewerRef.value.viewer._map.getBounds()._ne.lng,
viewerRef.value.viewer._map.getBounds()._ne.lat,
viewerRef.value.viewer._map.getBounds()._sw.lng,
viewerRef.value.viewer._map.getBounds()._sw.lat
viewerRef.value.viewer.map.getBounds()._ne.lng,
viewerRef.value.viewer.map.getBounds()._ne.lat,
viewerRef.value.viewer.map.getBounds()._sw.lng,
viewerRef.value.viewer.map.getBounds()._sw.lat
]
if (
seqId.value === sequence.id &&
@@ -503,7 +503,7 @@ function goToSequence(sequence: SequenceLinkInterface): void {
}
seqId.value = sequence.id
viewerRef.value.viewer.select(seqId.value)
viewerRef.value.viewer._map.flyTo({
viewerRef.value.viewer.map.flyTo({
center: [
sequence.extent.spatial.bbox[0][0],
sequence.extent.spatial.bbox[0][1]
@@ -597,20 +597,17 @@ async function updateFilters(value: {
await updateSequence(uri.value)
}
watchEffect(() => {
if (viewerRef.value && viewerRef.value.viewer) {
viewerRef.value.viewer.addEventListener(
'select',
(e: { detail: { seqId: string } }) => {
if (seqId.value === e.detail.seqId) return
seqId.value = e.detail.seqId
scrollIntoSelected(
e.detail.seqId,
userSequences.value.map((e) => e.id)
)
if (viewerRef.value) viewerRef.value.viewer.select(e.detail.seqId)
}
if (!viewerRef || !viewerRef.value) return
const viewerMap = viewerRef.value.viewer
viewerMap.addEventListener('select', (e: { detail: { seqId: string } }) => {
if (seqId.value === e.detail.seqId) return
seqId.value = e.detail.seqId
scrollIntoSelected(
e.detail.seqId,
userSequences.value.map((e) => e.id)
)
}
viewerMap.select(e.detail.seqId)
})
})
</script>
<style lang="scss">

View File

@@ -166,13 +166,12 @@ import License from '@/components/License.vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import authConfig from '../composables/auth'
import { manageSlashUrl } from '@/utils'
const { t } = useI18n()
const { authConf } = authConfig()
const apiUrl = computed((): string =>
import.meta.env.VITE_API_URL
? import.meta.env.VITE_API_URL
: 'https://panoramax.ign.fr/'
import.meta.env.VITE_API_URL ? manageSlashUrl() : 'https://panoramax.ign.fr/'
)
const formatTextInfoCard = computed((): string => {

View File

@@ -2,6 +2,8 @@ export interface MapInterface {
startWide?: boolean
users?: string[]
maxZoom?: number
selectedSequence?: string
selectedPicture?: string
minZoom?: number
style?: object | string
zoom?: number
@@ -20,7 +22,7 @@ export interface ViewerInterface {
credentials: string
}
hash?: boolean
picId?: string
selectedPicture?: string
widgets?: {
customWidget: HTMLAnchorElement
}