add header + gitlab cy
31
.eslintrc.js
@@ -1,26 +1,27 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
node: true
|
||||
},
|
||||
parser: "vue-eslint-parser",
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser",
|
||||
extraFileExtensions: [".vue"],
|
||||
parser: '@typescript-eslint/parser',
|
||||
extraFileExtensions: ['.vue'],
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
sourceType: 'module'
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:vue/base",
|
||||
"prettier",
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:vue/base',
|
||||
'prettier'
|
||||
],
|
||||
rules: {
|
||||
"vue/require-default-prop": "off",
|
||||
'vue/require-default-prop': 'off',
|
||||
'prettier/prettier': 'error'
|
||||
// override/add rules settings here, such as:
|
||||
// 'vue/no-unused-vars': 'error'
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
15
.gitlab-cy.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
build-job:
|
||||
stage: build
|
||||
script:
|
||||
- yarn build
|
||||
|
||||
test-job2:
|
||||
stage: test
|
||||
script:
|
||||
- yarn test
|
||||
|
||||
deploy-prod:
|
||||
stage: deploy
|
||||
script:
|
||||
yarn start
|
||||
environment: production
|
||||
5
.prettierrc.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
2
geovisio.d.ts
vendored
@@ -1 +1 @@
|
||||
declare module "geovisio";
|
||||
declare module 'geovisio'
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
"format": "prettier . --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"axios": "^1.2.3",
|
||||
"bootstrap": "^5.2.3",
|
||||
"bootstrap-icons": "^1.10.3",
|
||||
"geovisio": "^1.4.0",
|
||||
"vue": "^3.2.45",
|
||||
"vue-axios": "^3.5.2",
|
||||
|
||||
BIN
src/.DS_Store
vendored
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from "vue-router";
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
BIN
src/assets/images/logo-panoramax.jpeg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
src/assets/images/logo.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
@@ -1,7 +1,9 @@
|
||||
html {
|
||||
font-size: 62.5%; /* 1rem = 10px */
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
#app {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -17,6 +19,11 @@ h5 {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
:root {
|
||||
--white: #ffffff;
|
||||
--black: #181818;
|
||||
}
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<template>
|
||||
<ul class="collection-list">
|
||||
<li
|
||||
v-for="collection in state.collections"
|
||||
:key="collection.id"
|
||||
class="collection-item"
|
||||
>
|
||||
<span>{{ collection.title }}</span>
|
||||
<span>{{ collection.license }}</span>
|
||||
<p>{{ collection.description }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import axios from "axios";
|
||||
import { onMounted } from "vue";
|
||||
import { reactive } from "vue";
|
||||
|
||||
interface Collection {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
license: string;
|
||||
}
|
||||
|
||||
const state = reactive<{ collections: Collection[] }>({
|
||||
collections: [],
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const { data } = await axios.get("collections");
|
||||
state.collections = data.collections.slice(-10);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.collection-list {
|
||||
margin: 0;
|
||||
}
|
||||
.collection-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid black;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
47
src/components/Header.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<header class="header">
|
||||
<nav class="nav">
|
||||
<router-link to="/">
|
||||
<img src="@/assets/images/logo-panoramax.jpeg" alt="" class="logo" />
|
||||
</router-link>
|
||||
<ul class="nav-list">
|
||||
<li class="nav-list-item">
|
||||
<Link text="Verser des photos" path="/" />
|
||||
</li>
|
||||
<li class="nav-list-item">
|
||||
<Link text="Aide" path="/" />
|
||||
</li>
|
||||
<li>
|
||||
<Link icon="bi bi-person-circle" path="/" />
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Link from '@/components/Link.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
height: 8rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
.logo {
|
||||
height: 4rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.nav-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.nav-list-item {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
</style>
|
||||
42
src/components/Link.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<a
|
||||
v-if="props.look === 'external'"
|
||||
:href="props.path"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>
|
||||
<i v-if="props.icon" :class="props.icon" class="icon"></i>
|
||||
<span v-if="props.text">{{ props.text }}</span>
|
||||
</a>
|
||||
<router-link v-else :to="props.path" class="link">
|
||||
<i v-if="props.icon" :class="props.icon" class="icon"></i>
|
||||
<span v-if="props.text">{{ props.text }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
interface Props {
|
||||
text: string
|
||||
path: string
|
||||
look: string
|
||||
type: string
|
||||
alt: string
|
||||
icon: string
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
color: var(--black);
|
||||
font-size: 2.4rem;
|
||||
}
|
||||
.link {
|
||||
color: var(--black);
|
||||
text-decoration: none;
|
||||
}
|
||||
.link:hover {
|
||||
background-color: transparent;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
166
src/lib/API.js
@@ -1,166 +0,0 @@
|
||||
/**
|
||||
* API contains various utility functions to communicate with the backend
|
||||
*
|
||||
* @param {string} endpoint The <a href="https://github.com/radiantearth/stac-api-spec/tree/master/item-search">STAC Item Search</a> API endpoint
|
||||
* @param {string} [picturesTiles] The pictures vector tiles endpoint, defaults to /api/map/{z}/{x}/{y}.mvt relatively to endpoint parameter
|
||||
* @private
|
||||
*/
|
||||
class API {
|
||||
constructor(endpoint, picturesTiles) {
|
||||
// Parse local endpoints
|
||||
if(endpoint.startsWith("/")) {
|
||||
endpoint = window.location.href.split("/").slice(0, 3).join("/") + endpoint;
|
||||
}
|
||||
//endpoint = 'https://geovisio.osc-fr1.scalingo.io';
|
||||
|
||||
// Check endpoint
|
||||
if(!API.isValidHttpUrl(endpoint)) {
|
||||
throw new Error("endpoint parameter is not a valid URL");
|
||||
}
|
||||
|
||||
this._endpoint = endpoint;
|
||||
this._picturesTiles = picturesTiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full URL for listing pictures around a specific location
|
||||
*
|
||||
* @param {number} lat Latitude
|
||||
* @param {number} lon Longitude
|
||||
* @returns {string} The corresponding URL
|
||||
*/
|
||||
getPicturesAroundCoordinatesUrl(lat, lon) {
|
||||
if(isNaN(parseFloat(lat)) || isNaN(parseFloat(lon))) {
|
||||
throw new Error("lat and lon parameters should be valid numbers");
|
||||
}
|
||||
|
||||
const factor = 0.0005;
|
||||
const bbox = [ lon - factor, lat - factor, lon + factor, lat + factor ].map(d => d.toFixed(4)).join(",");
|
||||
return `${this._endpoint}?bbox=[${bbox}]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full URL for retrieving a specific picture metadata
|
||||
*
|
||||
* @param {string} picId The picture unique identifier
|
||||
* @returns {string} The corresponding URL
|
||||
*/
|
||||
getPictureMetadataUrl(picId) {
|
||||
API.isPictureIdValid(picId);
|
||||
return `${this._endpoint}?ids=["${picId}"]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full URL for pictures vector tiles
|
||||
*
|
||||
* @returns {string} The URL
|
||||
* @fires Error If URL can't be determined
|
||||
*/
|
||||
getPicturesTilesUrl() {
|
||||
// Explicitly defined URL
|
||||
if(this._picturesTiles) {
|
||||
return this._picturesTiles;
|
||||
}
|
||||
// GeoVisio endpoint
|
||||
else if(this.isGeoVisioEndpoint()) {
|
||||
return this._endpoint.replace("/api/search", "/api/map/{z}/{x}/{y}.mvt");
|
||||
}
|
||||
// Unknown endpoint, throw an error
|
||||
else {
|
||||
throw new Error("Pictures vector tiles URL is unknown");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a picture ID for a given sequence
|
||||
*
|
||||
* @param {string} seqId The sequence ID
|
||||
* @returns {Promise} Promise resolving on a single picture metadata, or null if no image found
|
||||
*/
|
||||
getPictureThumbnailForSequence(seqId) {
|
||||
const url = `${this._endpoint}?limit=1&collections=["${seqId}"]`;
|
||||
|
||||
return fetch(url)
|
||||
.then(res => res.json())
|
||||
.then(res => res.features.length > 0 ? res.features[0] : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get thumbnail URL for a specific picture
|
||||
*
|
||||
* @param {string} picId The picture unique identifier
|
||||
* @returns {Promise} The corresponding URL on resolve, or undefined if no thumbnail could be found
|
||||
*/
|
||||
getPictureThumbnailUrl(picId) {
|
||||
if(this.isGeoVisioEndpoint()) {
|
||||
return Promise.resolve(`${this.getGeoVisioRoot()}/api/pictures/${picId}/thumb.jpg`);
|
||||
}
|
||||
else {
|
||||
return fetch(this.getPictureMetadataUrl(picId))
|
||||
.then(res => res.json())
|
||||
.then(res => Object.values(res?.features.pop()?.assets || {})
|
||||
.find(a => a.roles.includes("thumbnail") && a.type.startsWith("image/"))
|
||||
?.href
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if given endpoint is a GeoVisio server or not
|
||||
*
|
||||
* @returns {boolean} True if is a GeoVisio instance
|
||||
*/
|
||||
isGeoVisioEndpoint() {
|
||||
return this._endpoint.includes("/api/search");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the GeoVisio root API endpoint (instead of search)
|
||||
* This is mainly an internal utility
|
||||
*
|
||||
* @returns {string} The root of GeoVisio endpoint
|
||||
* @fires Error If it's not a Geovisio API
|
||||
*/
|
||||
getGeoVisioRoot() {
|
||||
if(this.isGeoVisioEndpoint()) {
|
||||
return this._endpoint.replace(/\/api\/search.*$/, "");
|
||||
}
|
||||
else {
|
||||
throw new Error("Can't get root endpoint on a third-party STAC API");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks URL string validity
|
||||
*
|
||||
* @param {string} str The URL to check
|
||||
* @returns {boolean} True if valid
|
||||
*/
|
||||
static isValidHttpUrl(str) {
|
||||
let url;
|
||||
|
||||
try {
|
||||
url = new URL(str);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return url.protocol === "http:" || url.protocol === "https:";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks picture ID validity
|
||||
*
|
||||
* @param {string} picId The picture unique identifier
|
||||
* @returns {boolean} True if valid
|
||||
* @throws {Error} If not valid
|
||||
*/
|
||||
static isPictureIdValid(picId) {
|
||||
if(!picId || typeof picId !== "string" || picId.length === 0) {
|
||||
throw new Error("picId should be a valid picture unique identifier");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default API;
|
||||
401
src/lib/Map.js
@@ -1,401 +0,0 @@
|
||||
import "./css/Map.css";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
import maplibregl from "maplibre-gl";
|
||||
import MiniComponentButtons from "./MiniComponentButtons";
|
||||
import MarkerSVG from "./img/marker.svg";
|
||||
|
||||
/**
|
||||
* @summary Map showing photo location
|
||||
* @private
|
||||
*/
|
||||
export default class Map {
|
||||
/**
|
||||
* @param {external:photo-sphere-viewer.Viewer} psv The viewer
|
||||
* @param {object} [options] Optional settings (can be any of [MapLibre GL settings](https://maplibre.org/maplibre-gl-js-docs/api/map/#map-parameters))
|
||||
*/
|
||||
constructor(psv, options = {}) {
|
||||
this.psv = psv;
|
||||
this.container = document.createElement("div");
|
||||
this.container.classList.add("psv-map", "gvs-map-small");
|
||||
this.psv.parent.appendChild(this.container);
|
||||
|
||||
// Create map
|
||||
this._mapContainer = document.createElement("div");
|
||||
this._map = new maplibregl.Map({
|
||||
container: this._mapContainer,
|
||||
style: "https://tile-vect.openstreetmap.fr/styles/basic/style.json",
|
||||
center: [0, 0],
|
||||
zoom: 0,
|
||||
hash: "map",
|
||||
...options,
|
||||
});
|
||||
this._map.addControl(new maplibregl.NavigationControl(), "top-right");
|
||||
|
||||
// Widgets and markers
|
||||
this._miniButtons = new MiniComponentButtons(this);
|
||||
this.container.appendChild(this._mapContainer);
|
||||
this._picMarker = this._getPictureMarker();
|
||||
|
||||
this._map.on("load", () => {
|
||||
this._createPicturesTilesLayer();
|
||||
this._listenToViewerEvents();
|
||||
this._map.resize();
|
||||
});
|
||||
|
||||
// Cache for pictures and sequences thumbnails
|
||||
this._picThumbUrl = {};
|
||||
this._seqThumbPicId = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Change map rendering between small or wide
|
||||
*
|
||||
* @param {boolean} isWide True to make wide
|
||||
*/
|
||||
setWide(isWide) {
|
||||
if (isWide) {
|
||||
this.container.classList.remove("gvs-map-small");
|
||||
} else {
|
||||
this.container.classList.add("gvs-map-small");
|
||||
}
|
||||
this._map.resize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce component visibility (shown as a badge button)
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_minimize() {
|
||||
this.container.classList.add("gvs-map-minimized");
|
||||
}
|
||||
|
||||
/**
|
||||
* Show component as a classic widget (invert operation of minimize)
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_maximize() {
|
||||
this.container.classList.remove("gvs-map-minimized");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create pictures/sequences vector tiles layer
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_createPicturesTilesLayer() {
|
||||
this._map.addSource("geovisio", {
|
||||
type: "vector",
|
||||
tiles: [this.psv._myApi.getPicturesTilesUrl()],
|
||||
minzoom: 0,
|
||||
maxzoom: 14,
|
||||
});
|
||||
|
||||
this._map.addLayer({
|
||||
id: "sequences",
|
||||
type: "line",
|
||||
source: "geovisio",
|
||||
"source-layer": "sequences",
|
||||
...this._getSequencesLayerStyleProperties(),
|
||||
});
|
||||
|
||||
this._map.addLayer({
|
||||
id: "pictures",
|
||||
type: "circle",
|
||||
source: "geovisio",
|
||||
"source-layer": "pictures",
|
||||
...this._getPicturesLayerStyleProperties(),
|
||||
});
|
||||
|
||||
// Map interaction events (pointer cursor, click)
|
||||
this._picPopup = new maplibregl.Popup({
|
||||
closeButton: false,
|
||||
closeOnClick: false,
|
||||
offset: 3,
|
||||
});
|
||||
|
||||
this._map.on("mouseenter", "pictures", (e) => {
|
||||
this._map.getCanvas().style.cursor = "pointer";
|
||||
this._attachPreviewToPictures(e, "pictures");
|
||||
});
|
||||
|
||||
this._map.on("mouseleave", "pictures", () => {
|
||||
this._map.getCanvas().style.cursor = "";
|
||||
this._picPopup.remove();
|
||||
});
|
||||
|
||||
this._map.on("click", "pictures", (e) => {
|
||||
e.preventDefault();
|
||||
this.psv.goToPicture(e.features[0].properties.id);
|
||||
});
|
||||
|
||||
this._map.on("mouseenter", "sequences", (e) => {
|
||||
if (this._map.getZoom() <= 15) {
|
||||
this._map.getCanvas().style.cursor = "pointer";
|
||||
if (e.features[0].properties.id) {
|
||||
this._attachPreviewToPictures(e, "sequences");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._map.on("mouseleave", "sequences", () => {
|
||||
this._map.getCanvas().style.cursor = "";
|
||||
this._picPopup.remove();
|
||||
});
|
||||
|
||||
this._map.on("click", "sequences", (e) => {
|
||||
e.preventDefault();
|
||||
if (e.features[0].properties.id && this._map.getZoom() <= 15) {
|
||||
this._getPictureIdForSequence(e.features[0].properties.id).then(
|
||||
(picId) => {
|
||||
if (picId) {
|
||||
this.psv.goToPicture(picId);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this._map.on("click", (e) => {
|
||||
if (e.defaultPrevented === false) {
|
||||
this._picPopup.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* MapLibre paint/layout properties for pictures layer
|
||||
* This is useful when selected picture changes to allow partial update
|
||||
*
|
||||
* @returns {object} Paint/layout properties
|
||||
* @private
|
||||
*/
|
||||
_getPicturesLayerStyleProperties() {
|
||||
return {
|
||||
paint: {
|
||||
"circle-radius": ["interpolate", ["linear"], ["zoom"], 14, 2, 22, 9],
|
||||
"circle-color": "#FF6F00",
|
||||
"circle-opacity": ["interpolate", ["linear"], ["zoom"], 14, 0, 15, 1],
|
||||
"circle-stroke-color": "#ffffff",
|
||||
"circle-stroke-width": [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
17,
|
||||
0,
|
||||
20,
|
||||
2,
|
||||
],
|
||||
},
|
||||
layout: {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* MapLibre paint/layout properties for sequences layer
|
||||
*
|
||||
* @returns {object} Paint/layout properties
|
||||
* @private
|
||||
*/
|
||||
_getSequencesLayerStyleProperties() {
|
||||
return {
|
||||
paint: {
|
||||
"line-width": [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
0,
|
||||
0.5,
|
||||
10,
|
||||
2,
|
||||
14,
|
||||
4,
|
||||
16,
|
||||
5,
|
||||
22,
|
||||
3,
|
||||
],
|
||||
"line-color": "#FF6F00",
|
||||
},
|
||||
layout: {
|
||||
"line-cap": "square",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a preview popup to a single picture.
|
||||
* This is a mouseenter over pictures event handler
|
||||
*
|
||||
* @param {object} e Event data
|
||||
* @param {string} from Calling layer name
|
||||
* @private
|
||||
*/
|
||||
_attachPreviewToPictures(e, from) {
|
||||
let f = e.features[0];
|
||||
|
||||
let coordinates =
|
||||
from === "pictures" ? f.geometry.coordinates.slice() : e.lngLat;
|
||||
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
|
||||
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
|
||||
}
|
||||
|
||||
// Display thumbnail
|
||||
this._picPopup
|
||||
.setLngLat(coordinates)
|
||||
.setHTML(`<i class="gvs-map-thumb">Loading...</i>`)
|
||||
.addTo(this._map);
|
||||
|
||||
this._picPopup._loading = f.properties.id;
|
||||
|
||||
const p =
|
||||
from === "pictures"
|
||||
? this._getPictureThumbURL(f.properties.id)
|
||||
: this._getPictureIdForSequence(f.properties.id).then((picId) =>
|
||||
this._getPictureThumbURL(picId)
|
||||
);
|
||||
|
||||
p.then((thumbUrl) => {
|
||||
if (this._picPopup._loading === f.properties.id) {
|
||||
delete this._picPopup._loading;
|
||||
|
||||
if (thumbUrl) {
|
||||
this._picPopup.setHTML(
|
||||
`<img class="gvs-map-thumb" src="${thumbUrl}" alt="Thumbnail of hovered picture" />`
|
||||
);
|
||||
} else {
|
||||
this._picPopup.setHTML(`<i>No thumbnail</i>`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get picture URL for a given picture ID
|
||||
*
|
||||
* @param {string} picId The picture ID
|
||||
* @returns {Promise} Promise resolving on picture thumbnail URL, or null on timeout
|
||||
* @private
|
||||
*/
|
||||
_getPictureThumbURL(picId) {
|
||||
let res = null;
|
||||
|
||||
if (picId) {
|
||||
if (this._picThumbUrl[picId] !== undefined) {
|
||||
res =
|
||||
typeof this._picThumbUrl[picId] === "string"
|
||||
? Promise.resolve(this._picThumbUrl[picId])
|
||||
: this._picThumbUrl[picId];
|
||||
} else {
|
||||
this._picThumbUrl[picId] = this.psv._myApi
|
||||
.getPictureThumbnailUrl(picId)
|
||||
.then((url) => {
|
||||
if (url) {
|
||||
this._picThumbUrl[picId] = url;
|
||||
return url;
|
||||
} else {
|
||||
this._picThumbUrl[picId] = null;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this._picThumbUrl[picId] = null;
|
||||
});
|
||||
res = this._picThumbUrl[picId];
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a picture thumbnail URL for a given sequence
|
||||
*
|
||||
* @param {string} seqId The sequence ID
|
||||
* @returns {Promise} Promise resolving on picture thumbnail URL, or null on timeout
|
||||
* @private
|
||||
*/
|
||||
_getPictureIdForSequence(seqId) {
|
||||
let res;
|
||||
if (this._seqThumbPicId[seqId] !== undefined) {
|
||||
if (typeof this._seqThumbPicId[seqId] === "string") {
|
||||
res = Promise.resolve(this._seqThumbPicId[seqId]);
|
||||
} else if (this._seqThumbPicId[seqId] === null) {
|
||||
res = Promise.resolve(null);
|
||||
} else {
|
||||
res = this._seqThumbPicId[seqId];
|
||||
}
|
||||
} else {
|
||||
this._seqThumbPicId[seqId] = this.psv._myApi
|
||||
.getPictureThumbnailForSequence(seqId)
|
||||
.then((picMeta) => {
|
||||
if (picMeta) {
|
||||
const thumbAsset = Object.values(picMeta.assets).find((a) =>
|
||||
a?.roles?.includes("thumbnail")
|
||||
);
|
||||
|
||||
if (thumbAsset) {
|
||||
this._seqThumbPicId[seqId] = picMeta.id;
|
||||
this._picThumbUrl[picMeta.id] = thumbAsset.href;
|
||||
return picMeta.id;
|
||||
} else {
|
||||
this._seqThumbPicId[seqId] = null;
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
this._seqThumbPicId[seqId] = null;
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this._seqThumbPicId[seqId] = null;
|
||||
return null;
|
||||
});
|
||||
res = this._seqThumbPicId[seqId];
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ready-to-use picture marker
|
||||
*
|
||||
* @returns {maplibregl.Marker} The generated marker
|
||||
* @private
|
||||
*/
|
||||
_getPictureMarker() {
|
||||
const img = document.createElement("img");
|
||||
img.src = MarkerSVG;
|
||||
return new maplibregl.Marker({
|
||||
element: img,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start listening to picture changes in PSV
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_listenToViewerEvents() {
|
||||
// Switched picture
|
||||
this.psv.on("picture-loaded", (e, d) => {
|
||||
// Show marker corresponding to selection
|
||||
this._picMarker
|
||||
.setLngLat([d.lon, d.lat])
|
||||
.setRotation(d.x)
|
||||
.addTo(this._map);
|
||||
|
||||
// Move map to picture coordinates
|
||||
this._map.flyTo({
|
||||
center: [d.lon, d.lat],
|
||||
zoom: this._map.getZoom() < 15 ? 20 : this._map.getZoom(),
|
||||
maxDuration: 2000,
|
||||
});
|
||||
});
|
||||
|
||||
// Picture view rotated
|
||||
this.psv.on("view-rotated", (e, d) => {
|
||||
this._picMarker.setRotation(d.x);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import './css/MiniComponentButtons.css';
|
||||
import Map from './Map';
|
||||
import MinimizeIcon from './img/minimize.svg';
|
||||
import ExpandIcon from './img/expand.svg';
|
||||
import PictureIcon from './img/picture.svg';
|
||||
import MapIcon from './img/map.svg';
|
||||
|
||||
/**
|
||||
* Mini component buttons is a helper to create top-right corner buttons on reduced version of map/viewer.
|
||||
* It offers a same design style for both, and handle events.
|
||||
*
|
||||
* @param {Viewer|Map} parent The parent container
|
||||
* @private
|
||||
*/
|
||||
export default class MiniComponentButtons {
|
||||
constructor(parent) {
|
||||
this._parentName = parent instanceof Map ? "map" : "psv";
|
||||
this._parentLabel = this._parentName === "map" ? "map" : "picture";
|
||||
this._parent = parent;
|
||||
this._createButtonsDOM();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the DOM
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_createButtonsDOM() {
|
||||
// Button in bottom left corner (mini widget hidden)
|
||||
this.badgeBtn = document.createElement("button");
|
||||
this.badgeBtn.classList.add("gvs-mini-buttons-badge");
|
||||
const badgeImg = document.createElement("img");
|
||||
badgeImg.src = this._parentName === "map" ? MapIcon : PictureIcon;
|
||||
badgeImg.alt = "Expand";
|
||||
badgeImg.title = `Show the ${this._parentLabel}`;
|
||||
this.badgeBtn.appendChild(badgeImg);
|
||||
this.badgeBtn.addEventListener("click", () => {
|
||||
this.badgeBtn.classList.remove("gvs-visible");
|
||||
this._parent._maximize();
|
||||
});
|
||||
(this._parentName === "map" ? this._parent.psv.parent : this._parent.parent).appendChild(this.badgeBtn);
|
||||
|
||||
// Buttons in map/viewer top right corner (mini widget visible)
|
||||
this.container = document.createElement("div");
|
||||
this.container.classList.add("gvs-mini-buttons");
|
||||
|
||||
const btnMinimize = document.createElement("button");
|
||||
const btnMinimizeIcon = document.createElement("img");
|
||||
btnMinimizeIcon.src = MinimizeIcon;
|
||||
btnMinimizeIcon.alt = "Minimize";
|
||||
btnMinimizeIcon.title = `Hide the ${this._parentLabel}, you can show it again using button in bottom left corner`;
|
||||
btnMinimize.addEventListener("click", () => {
|
||||
this._parent._minimize();
|
||||
this.badgeBtn.classList.add("gvs-visible");
|
||||
});
|
||||
btnMinimize.appendChild(btnMinimizeIcon);
|
||||
this.container.appendChild(btnMinimize);
|
||||
|
||||
const btnExpand = document.createElement("button");
|
||||
const btnExpandIcon = document.createElement("img");
|
||||
btnExpandIcon.src = ExpandIcon;
|
||||
btnExpandIcon.alt = "Expand";
|
||||
btnExpandIcon.title = `Make the ${this._parentLabel} appear in full page`;
|
||||
btnExpand.appendChild(btnExpandIcon);
|
||||
btnExpand.addEventListener("click", () => {
|
||||
if(this._parentName === "map") { this._parent.psv.setMapWide(true); }
|
||||
else { this._parent.setMapWide(false); }
|
||||
});
|
||||
this.container.appendChild(btnExpand);
|
||||
|
||||
this._parent.container.appendChild(this.container);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { AbstractButton, registerButton, DEFAULTS } from 'photo-sphere-viewer';
|
||||
|
||||
/**
|
||||
* @summary Navigation bar next picture in sequence button class
|
||||
* @augments {external:photo-sphere-viewer.buttons.AbstractButton}
|
||||
* @memberof {external:photo-sphere-viewer.buttons}
|
||||
* @private
|
||||
*/
|
||||
class NextButton extends AbstractButton {
|
||||
|
||||
static id = 'sequence-next';
|
||||
static icon = '<svg viewBox="0 0 100 100" version="1.1" id="svg4" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <path d="M 66.176728,53.93303 41.721163,78.388595 c -2.148533,2.14854 -5.68704,2.14854 -7.835573,0 -2.14854,-2.148533 -2.14854,-5.68704 0,-7.835572 L 54.359518,50.015045 33.822875,29.478403 c -2.14854,-2.14854 -2.14854,-5.68704 0,-7.835581 C 34.897115,20.568582 36.350225,20 37.74086,20 c 1.390103,0 2.843722,0.56865 3.917993,1.642822 l 24.518272,24.455565 c 2.14854,2.14854 2.14854,5.68704 0,7.83558 z" fill="currentColor" /></svg>';
|
||||
|
||||
/**
|
||||
* @param {external:photo-sphere-viewer.components.Navbar} navbar The navigation bar
|
||||
*/
|
||||
constructor(navbar) {
|
||||
super(navbar, 'psv-button--hover-scale psv-sequence-button');
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @description Goes to next picture in sequence
|
||||
*/
|
||||
onClick() {
|
||||
try {
|
||||
this.psv.goToNextPicture();
|
||||
} catch(e) {
|
||||
console.warn(e);
|
||||
this.disable();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DEFAULTS.lang[NextButton.id] = 'Next picture in sequence';
|
||||
registerButton(NextButton, 'caption:left');
|
||||
export default NextButton;
|
||||
@@ -1,70 +0,0 @@
|
||||
import { AbstractButton, registerButton, DEFAULTS } from 'photo-sphere-viewer';
|
||||
|
||||
/**
|
||||
* @summary Navigation bar play picture in sequence button class
|
||||
* @augments {external:photo-sphere-viewer.buttons.AbstractButton}
|
||||
* @memberof {external:photo-sphere-viewer.buttons}
|
||||
* @private
|
||||
*/
|
||||
class PlayButton extends AbstractButton {
|
||||
|
||||
static id = 'sequence-play';
|
||||
static icon = '<svg version="1.1" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <path d="m 78.839122,44.639674 -50.40778,-33.54823 c -4.2989,-2.86606 -10.11502,0.25309 -10.11502,5.3948 v 67.01458 c 0,5.22643 5.81612,8.26069 10.11502,5.39481 l 50.40778,-33.54823 c 3.79286,-2.44456 3.79286,-8.17667 -7.1e-4,-10.70542 z" fill="currentColor" /> </svg> ';
|
||||
static iconActive = '<svg version="1.1" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <path d="m 11.057615,10 v 80 h 31.72461 V 10 Z m 46.15821,0 v 80 h 31.72656 V 10 Z" fill="currentColor" /></svg>';
|
||||
|
||||
/**
|
||||
* @param {external:photo-sphere-viewer.components.Navbar} navbar The navigation bar
|
||||
*/
|
||||
constructor(navbar) {
|
||||
super(navbar, 'psv-button--hover-scale psv-sequence-button');
|
||||
|
||||
this.psv.on("sequence-playing", this);
|
||||
this.psv.on("sequence-stopped", this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy() {
|
||||
this.psv.off("sequence-playing", this);
|
||||
this.psv.off("sequence-stopped", this);
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle events
|
||||
*
|
||||
* @param {Event} e Event metadata
|
||||
* @private
|
||||
*/
|
||||
handleEvent(e) {
|
||||
/* eslint-disable */
|
||||
switch (e.type) {
|
||||
// @formatter:off
|
||||
case "sequence-playing": this.toggleActive(true); break;
|
||||
case "sequence-stopped": this.toggleActive(false); break;
|
||||
// @formatter:on
|
||||
}
|
||||
/* eslint-enable */
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @description Goes to play picture in sequence
|
||||
*/
|
||||
onClick() {
|
||||
if(this.psv.isSequencePlaying()) {
|
||||
this.toggleActive(false);
|
||||
this.psv.stopSequence();
|
||||
}
|
||||
else {
|
||||
this.toggleActive(true);
|
||||
this.psv.playSequence();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DEFAULTS.lang[PlayButton.id] = 'Play/pause navigation';
|
||||
registerButton(PlayButton, 'caption:left');
|
||||
export default PlayButton;
|
||||
@@ -1,38 +0,0 @@
|
||||
import { AbstractButton, registerButton, DEFAULTS } from 'photo-sphere-viewer';
|
||||
|
||||
/**
|
||||
* @summary Navigation bar previous picture in sequence button class
|
||||
* @augments {external:photo-sphere-viewer.buttons.AbstractButton}
|
||||
* @memberof {external:photo-sphere-viewer.buttons}
|
||||
* @private
|
||||
*/
|
||||
class PreviousButton extends AbstractButton {
|
||||
|
||||
static id = 'sequence-prev';
|
||||
static icon = '<svg viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <path d="m 33.823272,53.93303 24.455565,24.455565 c 2.148533,2.14854 5.68704,2.14854 7.835573,0 2.14854,-2.148533 2.14854,-5.68704 0,-7.835572 L 45.640482,50.015045 66.177125,29.478403 c 2.14854,-2.14854 2.14854,-5.68704 0,-7.835581 C 65.102885,20.568582 63.649775,20 62.25914,20 c -1.390103,0 -2.843722,0.56865 -3.917993,1.642822 L 33.822875,46.098387 c -2.14854,2.14854 -2.14854,5.68704 0,7.83558 z" fill="currentColor" /> </svg> ';
|
||||
|
||||
/**
|
||||
* @param {external:photo-sphere-viewer.components.Navbar} navbar The navigation bar
|
||||
*/
|
||||
constructor(navbar) {
|
||||
super(navbar, 'psv-button--hover-scale psv-sequence-button');
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @description Goes to previous picture in sequence
|
||||
*/
|
||||
onClick() {
|
||||
try {
|
||||
this.psv.goToPrevPicture();
|
||||
} catch(e) {
|
||||
console.warn(e);
|
||||
this.disable();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DEFAULTS.lang[PreviousButton.id] = 'Previous picture in sequence';
|
||||
registerButton(PreviousButton, 'caption:left');
|
||||
export default PreviousButton;
|
||||
@@ -1,582 +0,0 @@
|
||||
import "./css/Viewer.css";
|
||||
import "photo-sphere-viewer/dist/photo-sphere-viewer.css";
|
||||
import { Viewer as PSViewer } from "photo-sphere-viewer";
|
||||
import { EquirectangularTilesAdapter } from "photo-sphere-viewer/dist/adapters/equirectangular-tiles";
|
||||
import {
|
||||
VirtualTourPlugin,
|
||||
MODE_GPS as VTP_MODE_GPS,
|
||||
MODE_3D as VTP_MODE_3D,
|
||||
MODE_SERVER as VTP_MODE_SERVER,
|
||||
} from "photo-sphere-viewer/dist/plugins/virtual-tour";
|
||||
import "photo-sphere-viewer/dist/plugins/virtual-tour.css";
|
||||
import API from "./API";
|
||||
import "./PreviousButton";
|
||||
import "./NextButton";
|
||||
import "./PlayButton";
|
||||
import Map from "./Map";
|
||||
import MiniComponentButtons from "./MiniComponentButtons";
|
||||
import LoaderImgBase from "./img/loader_base.jpg";
|
||||
import LoaderImgTile0 from "./img/loader_0.jpg";
|
||||
import LoaderImgTile1 from "./img/loader_1.jpg";
|
||||
|
||||
const BASE_PANORAMA = {
|
||||
baseUrl: LoaderImgBase,
|
||||
width: 1280,
|
||||
cols: 2,
|
||||
rows: 1,
|
||||
tileUrl: (col) => (col === 0 ? LoaderImgTile0 : LoaderImgTile1),
|
||||
};
|
||||
|
||||
class Viewer extends PSViewer {
|
||||
constructor(container, endpoint, options = {}) {
|
||||
super({
|
||||
container,
|
||||
adapter: [
|
||||
EquirectangularTilesAdapter,
|
||||
{
|
||||
showErrorTile: false,
|
||||
},
|
||||
],
|
||||
caption: "GeoVisio",
|
||||
panorama: BASE_PANORAMA,
|
||||
minFov: 5,
|
||||
navbar: ["zoom"]
|
||||
.concat(
|
||||
options.player === undefined || options.player
|
||||
? ["sequence-prev", "sequence-play", "sequence-next"]
|
||||
: []
|
||||
)
|
||||
.concat(["caption", "move"]),
|
||||
plugins: [
|
||||
[
|
||||
VirtualTourPlugin,
|
||||
{
|
||||
positionMode: VTP_MODE_GPS,
|
||||
renderMode: VTP_MODE_3D,
|
||||
dataMode: VTP_MODE_SERVER,
|
||||
preload: true,
|
||||
rotateSpeed: false,
|
||||
getNode: (picId) => {
|
||||
if (!picId) {
|
||||
return;
|
||||
}
|
||||
return fetch(this._myApi.getPictureMetadataUrl(picId))
|
||||
.then((res) => res.json())
|
||||
.then((metadata) => {
|
||||
metadata = metadata.features.pop();
|
||||
if (!metadata) {
|
||||
throw new Error(
|
||||
"Picture with ID " + picId + " was not found"
|
||||
);
|
||||
}
|
||||
|
||||
const horizontalFov =
|
||||
metadata.properties["pers:interior_orientation"] &&
|
||||
metadata.properties["pers:interior_orientation"][
|
||||
"field_of_view"
|
||||
]
|
||||
? parseInt(
|
||||
metadata.properties["pers:interior_orientation"][
|
||||
"field_of_view"
|
||||
]
|
||||
)
|
||||
: 70;
|
||||
const is360 = horizontalFov === 360;
|
||||
const hdUrl = (
|
||||
Object.values(metadata.assets).find((a) =>
|
||||
a.roles.includes("data")
|
||||
) || {}
|
||||
).href;
|
||||
const matrix = metadata.properties["tiles:tile_matrix_sets"]
|
||||
? metadata.properties["tiles:tile_matrix_sets"].geovisio
|
||||
: null;
|
||||
const prev = metadata.links.find(
|
||||
(l) => l.rel === "prev" && l.type === "application/geo+json"
|
||||
);
|
||||
const next = metadata.links.find(
|
||||
(l) => l.rel === "next" && l.type === "application/geo+json"
|
||||
);
|
||||
const baseUrlWebp = Object.values(metadata.assets).find(
|
||||
(a) => a.roles.includes("visual") && a.type === "image/webp"
|
||||
);
|
||||
const baseUrlJpeg = Object.values(metadata.assets).find(
|
||||
(a) => a.roles.includes("visual") && a.type === "image/jpeg"
|
||||
);
|
||||
const tileUrl =
|
||||
metadata?.asset_templates?.tiles_webp ||
|
||||
metadata?.asset_templates?.tiles;
|
||||
|
||||
const res = {
|
||||
id: metadata.id,
|
||||
links: metadata.links
|
||||
.filter(
|
||||
(l) =>
|
||||
["next", "prev"].includes(l.rel) &&
|
||||
l.type === "application/geo+json"
|
||||
)
|
||||
.map((l) => ({
|
||||
nodeId: l.id,
|
||||
position: l.geometry.coordinates,
|
||||
})),
|
||||
panorama: is360
|
||||
? {
|
||||
baseUrl: (baseUrlWebp || baseUrlJpeg).href,
|
||||
cols: matrix && matrix.tileMatrix[0].matrixWidth,
|
||||
rows: matrix && matrix.tileMatrix[0].matrixHeight,
|
||||
width:
|
||||
matrix &&
|
||||
matrix.tileMatrix[0].matrixWidth *
|
||||
matrix.tileMatrix[0].tileWidth,
|
||||
tileUrl:
|
||||
matrix &&
|
||||
((col, row) =>
|
||||
tileUrl.href
|
||||
.replace(/\{TileCol\}/g, col)
|
||||
.replace(/\{TileRow\}/g, row)),
|
||||
}
|
||||
: {
|
||||
// Flat pictures are shown only using a cropped base panorama
|
||||
baseUrl: hdUrl,
|
||||
basePanoData: (img) => {
|
||||
const verticalFov =
|
||||
(horizontalFov * img.height) / img.width;
|
||||
const panoWidth = (img.width * 360) / horizontalFov;
|
||||
const panoHeight = (img.height * 180) / verticalFov;
|
||||
|
||||
return {
|
||||
fullWidth: panoWidth,
|
||||
fullHeight: panoHeight,
|
||||
croppedWidth: img.width,
|
||||
croppedHeight: img.height,
|
||||
croppedX: (panoWidth - img.width) / 2,
|
||||
croppedY: (panoHeight - img.height) / 2,
|
||||
poseHeading:
|
||||
metadata.properties["view:azimuth"] || 0,
|
||||
};
|
||||
},
|
||||
// This is only to mock loading of tiles (which are not available for flat pictures)
|
||||
cols: 2,
|
||||
rows: 1,
|
||||
width: 2,
|
||||
tileUrl: () => "",
|
||||
},
|
||||
position: metadata.geometry.coordinates,
|
||||
sequence: {
|
||||
id: metadata.collection,
|
||||
nextPic: next ? next.id : undefined,
|
||||
prevPic: prev ? prev.id : undefined,
|
||||
},
|
||||
sphereCorrection: metadata.properties["view:azimuth"]
|
||||
? {
|
||||
pan:
|
||||
-metadata.properties["view:azimuth"] *
|
||||
(Math.PI / 180),
|
||||
}
|
||||
: { pan: 0 },
|
||||
horizontalFov,
|
||||
};
|
||||
console.log(res);
|
||||
return res;
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
this._myApi = new API(endpoint, options?.map?.picturesTiles);
|
||||
this._myVTour = this.getPlugin(VirtualTourPlugin);
|
||||
this._sequencePlaying = false;
|
||||
this._prevSequence = null;
|
||||
|
||||
// Call appropriate functions at start according to initial options
|
||||
if (typeof options === "object") {
|
||||
const onReady = () => {
|
||||
if (options.picId) {
|
||||
this.goToPicture(options.picId);
|
||||
}
|
||||
|
||||
if (options.position) {
|
||||
this.goToPosition(...options.position).catch((e) => console.log(e));
|
||||
}
|
||||
};
|
||||
|
||||
if (options.map) {
|
||||
this._map = new Map(this, options.map);
|
||||
this._map._map.once("load", onReady);
|
||||
this._mapWide = false;
|
||||
this.container.classList.add("psv--has-map");
|
||||
this._miniButtons = new MiniComponentButtons(this);
|
||||
if (typeof options.map === "object" && options.map.startWide) {
|
||||
this.setMapWide(true);
|
||||
}
|
||||
} else {
|
||||
onReady();
|
||||
}
|
||||
}
|
||||
|
||||
// Custom events handlers
|
||||
this.on("position-updated", (e, position) => {
|
||||
/**
|
||||
* Event for viewer rotation
|
||||
*
|
||||
* @event view-rotated
|
||||
* @type {object}
|
||||
* @property {number} x New x position (in degrees, 0-360), corresponds to heading (0° = North, 90° = East, 180° = South, 270° = West)
|
||||
* @property {number} y New y position (in degrees)
|
||||
*/
|
||||
this.trigger("view-rotated", this._positionToXY(position));
|
||||
|
||||
this._onTilesStartLoading();
|
||||
});
|
||||
|
||||
this._myVTour.on("node-changed", (e, nodeId) => {
|
||||
if (nodeId) {
|
||||
const picMeta = this.getPictureMetadata();
|
||||
// Set rotation angle + fov on initial load / sequence change
|
||||
const currentSequence = picMeta.sequence.id;
|
||||
if (
|
||||
!this._prevSequence ||
|
||||
currentSequence !== this._prevSequence ||
|
||||
picMeta.horizontalFov < 360
|
||||
) {
|
||||
this.setOption(
|
||||
"maxFov",
|
||||
Math.min((picMeta.horizontalFov * 3) / 4, 90)
|
||||
);
|
||||
this.rotate({
|
||||
longitude: -picMeta.sphereCorrection.pan,
|
||||
latitude: 0,
|
||||
});
|
||||
this.zoom(50);
|
||||
}
|
||||
|
||||
this._prevSequence = currentSequence;
|
||||
|
||||
/**
|
||||
* Event for picture load (low-resolution image is loaded)
|
||||
*
|
||||
* @event picture-loaded
|
||||
* @type {object}
|
||||
* @property {string} picId The picture unique identifier
|
||||
* @property {number} lon Longitude (WGS84)
|
||||
* @property {number} lat Latitude (WGS84)
|
||||
* @property {number} x New x position (in degrees, 0-360), corresponds to heading (0° = North, 90° = East, 180° = South, 270° = West)
|
||||
* @property {number} y New y position (in degrees)
|
||||
*/
|
||||
this.trigger("picture-loaded", {
|
||||
...this._positionToXY(this.getPosition()),
|
||||
picId: nodeId,
|
||||
lon: picMeta.position[0],
|
||||
lat: picMeta.position[1],
|
||||
});
|
||||
}
|
||||
|
||||
this._onTilesStartLoading();
|
||||
|
||||
// Update prev/next picture in sequence buttons
|
||||
if (this.navbar.getButton("sequence-prev")) {
|
||||
if (
|
||||
this.getPictureMetadata() &&
|
||||
this.getPictureMetadata().sequence.prevPic
|
||||
) {
|
||||
this.navbar.getButton("sequence-prev").enable();
|
||||
} else {
|
||||
this.navbar.getButton("sequence-prev").disable();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.navbar.getButton("sequence-next")) {
|
||||
if (
|
||||
this.getPictureMetadata() &&
|
||||
this.getPictureMetadata().sequence.nextPic
|
||||
) {
|
||||
this.navbar.getButton("sequence-next").enable();
|
||||
} else {
|
||||
this.navbar.getButton("sequence-next").disable();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts result from getPosition or position-updated event into x/y coordinates
|
||||
*
|
||||
* @private
|
||||
* @param {object} pos latitude/longitude as given by PSV
|
||||
* @returns {object} Same coordinates as x/y and in degrees
|
||||
*/
|
||||
_positionToXY(pos) {
|
||||
return {
|
||||
x: pos.longitude * (180 / Math.PI),
|
||||
y: pos.latitude * (180 / Math.PI),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for loading a new range of tiles
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onTilesStartLoading() {
|
||||
if (this._tilesQueueTimer) {
|
||||
clearInterval(this._tilesQueueTimer);
|
||||
delete this._tilesQueueTimer;
|
||||
}
|
||||
this._tilesQueueTimer = setInterval(() => {
|
||||
if (Object.keys(this.adapter.queue.tasks).length === 0) {
|
||||
if (this._myVTour.prop.currentNode) {
|
||||
/**
|
||||
* Event launched when all visible tiles of a picture are loaded
|
||||
*
|
||||
* @event picture-tiles-loaded
|
||||
* @type {object}
|
||||
* @property {string} picId The picture unique identifier
|
||||
*/
|
||||
this.trigger("picture-tiles-loaded", {
|
||||
picId: this._myVTour.prop.currentNode.id,
|
||||
});
|
||||
}
|
||||
clearInterval(this._tilesQueueTimer);
|
||||
delete this._tilesQueueTimer;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click handler for next/prev navbar buttons
|
||||
*
|
||||
* @param {('next'|'prev')} type Set if it's next or previous button
|
||||
* @private
|
||||
*/
|
||||
_onNextPrevPicClick(type) {
|
||||
if (
|
||||
this.getPictureMetadata() &&
|
||||
this.getPictureMetadata().sequence[type + "Pic"]
|
||||
) {
|
||||
// Actually change current picture
|
||||
if (type === "prev") {
|
||||
this.goToPrevPicture();
|
||||
} else if (type === "next") {
|
||||
this.goToNextPicture();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get position of sphere currently shown to user
|
||||
*
|
||||
* @returns {object} Position in format { x: heading in degrees (0° = North, 90° = East, 180° = South, 270° = West), y: top/bottom position in degrees (-90° = bottom, 0° = front, 90° = top) }
|
||||
*/
|
||||
getXY() {
|
||||
return this._positionToXY(this.getPosition());
|
||||
}
|
||||
|
||||
/**
|
||||
* Access currently shown picture metadata
|
||||
*
|
||||
* @returns {object} Picture metadata
|
||||
*/
|
||||
getPictureMetadata() {
|
||||
return this._myVTour.prop.currentNode
|
||||
? Object.assign({}, this._myVTour.prop.currentNode)
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the map object (if any)
|
||||
* Allows you to call any of [the MapLibre GL Map function](https://maplibre.org/maplibre-gl-js-docs/api/map/) for advanced map management
|
||||
*
|
||||
* @returns {null|external:maplibre-gl.Map} The map
|
||||
*/
|
||||
getMap() {
|
||||
return this._map ? this._map._map : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays in viewer specified picture
|
||||
*
|
||||
* @param {string} picId The picture unique identifier
|
||||
*/
|
||||
goToPicture(picId) {
|
||||
// Actually move to wanted picture
|
||||
console.log(picId);
|
||||
this._myVTour.setCurrentNode(picId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes continuously to next picture in sequence as long as possible
|
||||
*/
|
||||
playSequence() {
|
||||
this._sequencePlaying = true;
|
||||
|
||||
/**
|
||||
* Event for sequence starting to play
|
||||
*
|
||||
* @event sequence-playing
|
||||
* @type {object}
|
||||
*/
|
||||
this.trigger("sequence-playing");
|
||||
|
||||
const nextPicturePlay = () => {
|
||||
if (this._sequencePlaying) {
|
||||
this.once("picture-tiles-loaded", () => {
|
||||
this._playTimer = setTimeout(() => {
|
||||
nextPicturePlay();
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
try {
|
||||
this.goToNextPicture();
|
||||
} catch (e) {
|
||||
this.stopSequence();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Stop playing if user clicks on image
|
||||
this.on("click", (e) => {
|
||||
console.log(e);
|
||||
this.stopSequence();
|
||||
});
|
||||
|
||||
nextPicturePlay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops playing current sequence
|
||||
*/
|
||||
stopSequence() {
|
||||
this._sequencePlaying = false;
|
||||
|
||||
if (this._playTimer) {
|
||||
clearTimeout(this._playTimer);
|
||||
delete this._playTimer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event for sequence stopped playing
|
||||
*
|
||||
* @event sequence-stopped
|
||||
* @type {object}
|
||||
*/
|
||||
this.trigger("sequence-stopped");
|
||||
}
|
||||
|
||||
/**
|
||||
* Is there any sequence being played right now ?
|
||||
*
|
||||
* @returns {boolean} True if sequence is playing
|
||||
*/
|
||||
isSequencePlaying() {
|
||||
return this._sequencePlaying;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays next picture in current sequence (if any)
|
||||
*/
|
||||
goToNextPicture() {
|
||||
if (!this.getPictureMetadata()) {
|
||||
throw new Error("No picture currently selected");
|
||||
}
|
||||
|
||||
const next = this.getPictureMetadata().sequence.nextPic;
|
||||
if (next) {
|
||||
this.goToPicture(next);
|
||||
} else {
|
||||
throw new Error("No next picture available");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays previous picture in current sequence (if any)
|
||||
*/
|
||||
goToPrevPicture() {
|
||||
if (!this.getPictureMetadata()) {
|
||||
throw new Error("No picture currently selected");
|
||||
}
|
||||
|
||||
const prev = this.getPictureMetadata().sequence.prevPic;
|
||||
if (prev) {
|
||||
this.goToPicture(prev);
|
||||
} else {
|
||||
throw new Error("No previous picture available");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays in viewer a picture near to given coordinates
|
||||
*
|
||||
* @param {number} lat Latitude (WGS84)
|
||||
* @param {number} lon Longitude (WGS84)
|
||||
* @returns {Promise} Resolves on picture ID if picture found, otherwise rejects
|
||||
*/
|
||||
goToPosition(lat, lon) {
|
||||
return fetch(this._myApi.getPicturesAroundCoordinatesUrl(lat, lon))
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (res.features.length > 0) {
|
||||
this.goToPicture(res.features[0].id);
|
||||
return res.features[0].id;
|
||||
} else {
|
||||
return Promise.reject(
|
||||
new Error("No picture found nearby given coordinates")
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the map shown as main element instead of viewer (wide map mode) ?
|
||||
*
|
||||
* @returns {boolean} True if map is wider than viewer
|
||||
*/
|
||||
isMapWide() {
|
||||
if (!this._map) {
|
||||
throw new Error("Map is not enabled");
|
||||
}
|
||||
return this._mapWide;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the map size (either small in a corner or wider than viewer)
|
||||
*
|
||||
* @param {boolean} wideMap True to make map wider than viewer
|
||||
*/
|
||||
setMapWide(wideMap) {
|
||||
if (!this._map) {
|
||||
throw new Error("Map is not enabled");
|
||||
}
|
||||
|
||||
// Change map size
|
||||
this._mapWide = wideMap;
|
||||
this._map.setWide(this._mapWide);
|
||||
|
||||
// Change viewer size
|
||||
if (this._mapWide) {
|
||||
this.container.classList.add("gvs-small");
|
||||
} else {
|
||||
this.container.classList.remove("gvs-small");
|
||||
}
|
||||
this.autoSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce component visibility (shown as a badge button)
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_minimize() {
|
||||
this.container.classList.add("gvs-minimized");
|
||||
}
|
||||
|
||||
/**
|
||||
* Show component as a classic widget (invert operation of minimize)
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_maximize() {
|
||||
this.container.classList.remove("gvs-minimized");
|
||||
}
|
||||
}
|
||||
|
||||
export default Viewer;
|
||||
@@ -1,37 +0,0 @@
|
||||
/* Map layout */
|
||||
.psv-map {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.psv-map .maplibregl-map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Small map layout */
|
||||
.psv-map.gvs-map-small {
|
||||
z-index: 90;
|
||||
}
|
||||
|
||||
.psv-map.gvs-map-small .maplibregl-map {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.psv-map.gvs-map-small .maplibregl-ctrl-attrib,
|
||||
.psv-map.gvs-map-small .maplibregl-ctrl-top-right .maplibregl-ctrl-group {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Picture thumbnail on map */
|
||||
.gvs-map-thumb {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
width: 150px;
|
||||
height: 75px;
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Visible mini widget buttons
|
||||
*/
|
||||
.gvs-mini-buttons {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.gvs-mini-buttons button {
|
||||
border: none;
|
||||
background: none;
|
||||
color: white;
|
||||
margin: 0 3px;
|
||||
padding: 3px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gvs-mini-buttons button img {
|
||||
height: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.gvs-mini-buttons button:hover {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Hide when parent is wide */
|
||||
.psv-map:not(.gvs-map-small) .gvs-mini-buttons,
|
||||
.psv-container:not(.gvs-small) .gvs-mini-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Badge button (mini widget hidden)
|
||||
*/
|
||||
.gvs-mini-buttons-badge {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
border: none;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 28px;
|
||||
background-color: #1565C0;
|
||||
color: white;
|
||||
z-index: 1000;
|
||||
box-shadow: 2px 2px 5px #555;
|
||||
}
|
||||
|
||||
.gvs-mini-buttons-badge img {
|
||||
vertical-align: middle;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.gvs-mini-buttons-badge.gvs-visible {
|
||||
display: unset;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/* Reduce navbar when map is visible */
|
||||
.psv--has-map .psv-navbar {
|
||||
left: 340px;
|
||||
right: 0;
|
||||
border-top-left-radius: 10px;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.psv--has-map {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Small widget layout */
|
||||
.psv-container.gvs-small,
|
||||
.psv-map.gvs-map-small {
|
||||
position: absolute;
|
||||
top: unset;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
width: 320px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.psv-container.gvs-small {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Minimized widget layout */
|
||||
.psv-container.gvs-minimized,
|
||||
.psv-map.gvs-map-minimized {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide navbar buttons */
|
||||
.psv-container.gvs-small .psv-navbar {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
version="1.1"
|
||||
id="svg3"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs7" />
|
||||
<path
|
||||
d="m 47.191182,9.8463618 h -7.807086 c -2.718629,0 -4.923181,-2.2044175 -4.923181,-4.9231809 C 34.460915,2.204012 36.665332,0 39.384096,0 H 59.076819 C 61.795447,0 64,2.204012 64,4.9231809 V 24.615904 c 0,2.718629 -2.204417,4.922641 -4.923181,4.922641 -2.719169,0 -4.923181,-2.204012 -4.923181,-4.922641 V 16.808818 L 40.404409,30.558047 c -1.922649,1.922649 -5.039807,1.922649 -6.962456,0 -1.922649,-1.922649 -1.922649,-5.039807 0,-6.962456 z M 16.808818,54.153638 h 7.807086 c 2.718629,0 4.922641,2.204012 4.922641,4.923181 C 29.538545,61.795447 27.334533,64 24.615904,64 H 4.9231809 C 2.204012,64 0,61.795583 0,59.076819 V 39.384096 c 0,-2.718629 2.204012,-4.923181 4.9231809,-4.923181 2.7186283,0 4.9231809,2.204417 4.9231809,4.923181 v 7.807086 L 23.595591,33.441953 c 1.922649,-1.922649 5.039807,-1.922649 6.962456,0 1.922649,1.922649 1.922649,5.039807 0,6.962456 z"
|
||||
id="path7"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.135141" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 276 KiB |
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
version="1.1"
|
||||
id="svg3"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M 19.692758,56.197145 V 2.0126606 L 1.5632003,9.1193325 C 0.6204633,9.4909745 0,10.39927 0,11.410826 v 48.896935 c 0,0.814739 0.4008795,1.575569 1.0759311,2.033685 0.6718081,0.457401 1.5261764,0.556167 2.2836594,0.25794 z M 39.384118,7.8025185 24.614851,2.0128268 V 56.197312 l 14.769267,5.78969 z m 4.923589,0 V 61.987002 L 62.417305,54.888148 C 63.342495,54.533405 64,53.63745 64,52.588838 V 3.6919025 C 64,2.8771635 63.598471,2.116981 62.924068,1.6588821 62.25226,1.2008331 61.397891,1.1027165 60.639746,1.4002929 Z"
|
||||
fill-rule="evenodd"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.166328" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 834 B |
@@ -1,63 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 12.7 12.7"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
|
||||
sodipodi:docname="marker.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="px"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
width="32px"
|
||||
inkscape:zoom="10.248373"
|
||||
inkscape:cx="19.710447"
|
||||
inkscape:cy="21.320458"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="573"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="Calque 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
style="fill:#bf360c;fill-opacity:1;stroke:#ffffff;stroke-width:0.661458;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path6342"
|
||||
sodipodi:type="arc"
|
||||
sodipodi:cx="-8.9855032"
|
||||
sodipodi:cy="-0.0052228207"
|
||||
sodipodi:rx="5.9781651"
|
||||
sodipodi:ry="5.9781651"
|
||||
sodipodi:start="0"
|
||||
sodipodi:end="1.5707963"
|
||||
sodipodi:arc-type="slice"
|
||||
d="M -3.007338,-0.00522282 A 5.9781651,5.9781651 0 0 1 -8.9855032,5.9729423 v -5.97816512 z"
|
||||
transform="matrix(-0.70710482,-0.70710874,0.70710482,-0.70710874,0,0)" />
|
||||
<circle
|
||||
style="fill:#ff6f00;fill-opacity:1;stroke:#ffffff;stroke-width:0.660027;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path846"
|
||||
cx="6.3499999"
|
||||
cy="6.5445719"
|
||||
r="2.6401064" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="m 29.367321,38.633596 v 15.314723 c 0,2.209304 -1.790308,3.999734 -3.999777,3.999734 -2.209329,0 -3.999779,-1.790289 -3.999779,-3.999734 V 48.290257 L 6.829052,62.828813 C 6.048068,63.609235 5.0237031,64 4.0004875,64 2.9772861,64 1.9529213,63.609235 1.1719231,62.82826 c -1.56256411,-1.562547 -1.56256411,-4.094945 0,-5.656357 L 15.710636,42.633345 h -5.658121 c -2.2093289,0 -3.9997786,-1.790289 -3.9997786,-3.999734 0,-2.209447 1.7903077,-3.999736 3.9997786,-3.999736 h 15.314886 c 2.209328,-5.67e-4 4.000204,1.790856 4.000204,4.000161 z M 62.829349,1.1719104 c -1.562564,-1.5625472 -4.09499,-1.5625472 -5.656418,0 L 42.634216,15.710467 v -5.658061 c 0,-2.2093045 -1.790307,-3.9997349 -3.999776,-3.9997349 -2.209329,0 -3.99978,1.7902884 -3.99978,3.9997349 v 15.314722 c 0,2.209304 1.790308,3.999735 3.99978,3.999735 h 15.314887 c 2.209328,0 3.999777,-1.790288 3.999777,-3.999735 0,-2.209447 -1.790307,-3.999735 -3.999777,-3.999735 H 48.291204 L 62.829917,6.8288363 c 1.560294,-1.561412 1.560294,-4.093953 -0.0011,-5.6563583 z"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.141897" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
id="path2"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.168712"
|
||||
d="m 61.68284,5.313288 -0.0014,6.748e-4 H 2.2374168 C 1.0386307,5.313946 0,6.2727926 0,7.5513628 V 56.449295 c 0,1.198786 0.95884664,2.237417 2.2374168,2.237417 H 61.762583 C 62.961369,58.686712 64,57.727866 64,56.449295 V 7.4703015 C 63.920256,6.2715155 62.961309,5.3132863 61.68284,5.313288 Z M 5.4330661,8.4301824 H 58.565944 c 1.198787,0 2.237416,0.9588466 2.237416,2.2374166 v 39.391054 l -0.08007,-0.0023 -11.425652,-19.015735 c -0.639268,-1.038628 -2.157025,-1.038628 -2.796277,0 L 37.392207,46.301178 22.69116,20.495097 c -0.639268,-1.118377 -2.157025,-1.118377 -2.796277,0 L 3.195651,49.738034 V 10.667599 c 0,-1.1987859 0.958845,-2.2374166 2.2374151,-2.2374166 z M 37.312794,12.744542 c -2.956348,0 -5.353325,2.396317 -5.353325,5.352664 0,2.957023 2.396977,5.353325 5.353325,5.353325 2.956347,0 5.353324,-2.396302 5.353324,-5.353325 0,-2.956516 -2.396977,-5.352664 -5.353324,-5.352664 z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,2 +0,0 @@
|
||||
import Viewer from './Viewer';
|
||||
export default Viewer;
|
||||
@@ -1,193 +0,0 @@
|
||||
import API from './../API';
|
||||
|
||||
const ENDPOINT = "http://360-dev.pavie.info";
|
||||
const URL_OK = ENDPOINT + "/api/search";
|
||||
const TILES_OK = ENDPOINT + "/api/map/{z}/{x}/{y}.mvt";
|
||||
const THIRD_PARTY_URL = "http://my.custom.api/points/to/stac/search";
|
||||
|
||||
describe('constructor', () => {
|
||||
it('works with valid endpoint', () => {
|
||||
const api = new API(URL_OK);
|
||||
expect(api._endpoint).toBe(URL_OK);
|
||||
});
|
||||
|
||||
it('works with relative path', () => {
|
||||
const api = new API("/api/search");
|
||||
expect(api._endpoint).toBe("http://localhost/api/search");
|
||||
});
|
||||
|
||||
it('fails if endpoint is invalid', () => {
|
||||
expect(() => new API("not an url")).toThrow("endpoint parameter is not a valid URL");
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPicturesAroundCoordinatesUrl', () => {
|
||||
it('works with valid coordinates', () => {
|
||||
const api = new API(URL_OK);
|
||||
expect(api.getPicturesAroundCoordinatesUrl(48.7, -1.25)).toBe(`${URL_OK}?bbox=[-1.2505,48.6995,-1.2495,48.7005]`);
|
||||
});
|
||||
|
||||
it('fails if coordinates are invalid', () => {
|
||||
const api = new API(URL_OK);
|
||||
expect(() => api.getPicturesAroundCoordinatesUrl()).toThrow("lat and lon parameters should be valid numbers");
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPictureMetadataUrl', () => {
|
||||
it('works with valid ID', () => {
|
||||
const api = new API(URL_OK);
|
||||
expect(api.getPictureMetadataUrl("whatever-id")).toBe(`${URL_OK}?ids=["whatever-id"]`);
|
||||
});
|
||||
|
||||
it('fails if picId is invalid', () => {
|
||||
const api = new API(URL_OK);
|
||||
expect(() => api.getPictureMetadataUrl()).toThrow("picId should be a valid picture unique identifier");
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPicturesTilesUrl', () => {
|
||||
it('works if URL is set in options', () => {
|
||||
const url = "http://360-dev.pavie.info/custom/route/{z}/{x}/{y}";
|
||||
const api = new API(URL_OK, url);
|
||||
expect(api.getPicturesTilesUrl()).toBe(url);
|
||||
});
|
||||
|
||||
it('works if endpoint is Geovisio', () => {
|
||||
const api = new API(URL_OK);
|
||||
expect(api.getPicturesTilesUrl()).toBe(TILES_OK);
|
||||
});
|
||||
|
||||
it('fails with custom endpoint and no pictures URL set', () => {
|
||||
const api = new API(THIRD_PARTY_URL);
|
||||
expect(() => api.getPicturesTilesUrl()).toThrow("Pictures vector tiles URL is unknown");
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPictureThumbnailUrl', () => {
|
||||
it('works with a geovisio instance', () => {
|
||||
const api = new API(URL_OK);
|
||||
return api.getPictureThumbnailUrl("picId").then(url => {
|
||||
expect(url).toBe(ENDPOINT + "/api/pictures/picId/thumb.jpg");
|
||||
});
|
||||
});
|
||||
|
||||
it('works with a third-party STAC API', () => {
|
||||
// Mock API search
|
||||
const thumbUrl = "http://my.custom.api/pic/thumb.jpg";
|
||||
global.fetch = jest.fn(() => Promise.resolve({
|
||||
json: () => Promise.resolve({
|
||||
features: [ {
|
||||
"assets": {
|
||||
"thumb": {
|
||||
"href": thumbUrl,
|
||||
"roles": ["thumbnail"],
|
||||
"type": "image/jpeg"
|
||||
}
|
||||
}
|
||||
}]
|
||||
})
|
||||
}));
|
||||
|
||||
const api = new API(THIRD_PARTY_URL);
|
||||
return api.getPictureThumbnailUrl("picId").then(url => {
|
||||
expect(url).toBe(thumbUrl);
|
||||
});
|
||||
});
|
||||
|
||||
it('works with a third-party STAC API without thumbnail', () => {
|
||||
// Mock API search
|
||||
global.fetch = jest.fn(() => Promise.resolve({
|
||||
json: () => Promise.resolve({
|
||||
features: [ {
|
||||
"assets": {}
|
||||
}]
|
||||
})
|
||||
}));
|
||||
|
||||
const api = new API(THIRD_PARTY_URL);
|
||||
return api.getPictureThumbnailUrl("picId").then(url => {
|
||||
expect(url).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPictureThumbnailForSequence', () => {
|
||||
it('works with an existing sequence', () => {
|
||||
const resPicId = "cbfc3add-8173-4464-98c8-de2a43c6a50f";
|
||||
const thumbUrl = "http://my.custom.api/pic/thumb.jpg";
|
||||
// Mock API search
|
||||
global.fetch = jest.fn(() => Promise.resolve({
|
||||
json: () => Promise.resolve({
|
||||
features: [ {
|
||||
"id": resPicId,
|
||||
"assets": {
|
||||
"thumb": {
|
||||
"href": thumbUrl,
|
||||
"roles": ["thumbnail"],
|
||||
"type": "image/jpeg"
|
||||
}
|
||||
}
|
||||
}]
|
||||
})
|
||||
}));
|
||||
|
||||
const api = new API(THIRD_PARTY_URL);
|
||||
return api.getPictureThumbnailForSequence("208b981a-262e-4966-97b6-98ee0ceb8df0").then(picMeta => {
|
||||
expect(picMeta.id).toBe(resPicId);
|
||||
expect(Object.values(picMeta.assets).find(a => a.roles.includes("thumbnail")).href).toBe(thumbUrl);
|
||||
});
|
||||
});
|
||||
|
||||
it('works with no results', () => {
|
||||
// Mock API search
|
||||
global.fetch = jest.fn(() => Promise.resolve({
|
||||
json: () => Promise.resolve({
|
||||
features: []
|
||||
})
|
||||
}));
|
||||
|
||||
const api = new API(THIRD_PARTY_URL);
|
||||
return api.getPictureThumbnailForSequence("208b981a-262e-4966-97b6-98ee0ceb8df0").then(picMeta => {
|
||||
expect(picMeta).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isGeoVisioInstance', () => {
|
||||
it('works with Geovisio', () => {
|
||||
const api = new API(URL_OK);
|
||||
expect(api.isGeoVisioEndpoint()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('works with any other STAC API instance', () => {
|
||||
const api = new API(THIRD_PARTY_URL);
|
||||
expect(api.isGeoVisioEndpoint()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGeoVisioRoot', () => {
|
||||
it('works with simple URL', () => {
|
||||
const api = new API(URL_OK);
|
||||
expect(api.getGeoVisioRoot()).toBe(ENDPOINT);
|
||||
});
|
||||
|
||||
it('works with URL and added parameters', () => {
|
||||
const api = new API(URL_OK+"?bla=bla");
|
||||
expect(api.getGeoVisioRoot()).toBe(ENDPOINT);
|
||||
});
|
||||
|
||||
it('fails on not-Geovisio API', () => {
|
||||
const api = new API(THIRD_PARTY_URL);
|
||||
expect(() => api.getGeoVisioRoot()).toThrow("Can't get root endpoint on a third-party STAC API");
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidHttpUrl', () => {
|
||||
it('works with valid endpoint', () => {
|
||||
expect(API.isValidHttpUrl(URL_OK)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('fails if endpoint is invalid', () => {
|
||||
expect(API.isValidHttpUrl("not an url")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
42
src/main.ts
@@ -1,26 +1,30 @@
|
||||
import { createApp } from "vue";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import axios from "axios";
|
||||
import VueAxios from "vue-axios";
|
||||
import fr from "./locales/fr.json";
|
||||
import "./assets/main.css";
|
||||
import { createApp } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import axios from 'axios'
|
||||
import VueAxios from 'vue-axios'
|
||||
import fr from './locales/fr.json'
|
||||
import './assets/main.css'
|
||||
import 'bootstrap/dist/css/bootstrap.css'
|
||||
import 'bootstrap/dist/js/bootstrap.js'
|
||||
import 'bootstrap-icons/font/bootstrap-icons.css'
|
||||
|
||||
axios.defaults.baseURL = import.meta.env.VITE_API_URL;
|
||||
axios.defaults.baseURL = import.meta.env.VITE_API_URL
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: "fr",
|
||||
locale: 'fr',
|
||||
globalInjection: true,
|
||||
legacy: false,
|
||||
messages: {
|
||||
fr,
|
||||
},
|
||||
});
|
||||
const app = createApp(App);
|
||||
fr
|
||||
}
|
||||
})
|
||||
|
||||
app.use(i18n);
|
||||
app.use(router);
|
||||
app.use(VueAxios, axios);
|
||||
app.provide("axios", app.config.globalProperties.axios);
|
||||
app.mount("#app");
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(i18n)
|
||||
app.use(router)
|
||||
app.use(VueAxios, axios)
|
||||
app.provide('axios', app.config.globalProperties.axios)
|
||||
app.mount('#app')
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import HomeView from "../views/HomeView.vue";
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: HomeView,
|
||||
},
|
||||
],
|
||||
});
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default router;
|
||||
export default router
|
||||
|
||||
@@ -4,47 +4,36 @@
|
||||
type="text/css"
|
||||
href="https://cdn.jsdelivr.net/npm/geovisio@1.3.1/build/index.css"
|
||||
/>
|
||||
<Header />
|
||||
<main class="entry-page">
|
||||
<section id="viewer" class="entry-viewer"></section>
|
||||
<section class="entry-collection-list">
|
||||
<h2 class="subtitle">Les dernières collections</h2>
|
||||
<CollectionList />
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from "vue";
|
||||
import GeoVisio from "geovisio";
|
||||
import CollectionList from "@/components/CollectionList.vue";
|
||||
import { onMounted } from 'vue'
|
||||
import GeoVisio from 'geovisio'
|
||||
import Header from '@/components/Header.vue'
|
||||
|
||||
onMounted(async () => {
|
||||
new GeoVisio(
|
||||
"viewer", // Div ID
|
||||
await new GeoVisio(
|
||||
'viewer', // Div ID
|
||||
`${import.meta.env.VITE_API_URL}search`, // STAC API search endpoint
|
||||
{
|
||||
map: {
|
||||
startWide: true,
|
||||
},
|
||||
startWide: true
|
||||
}
|
||||
} // Viewer options
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.entry-page {
|
||||
display: flex;
|
||||
padding: 40px;
|
||||
}
|
||||
.entry-viewer {
|
||||
height: 500px;
|
||||
width: 70%;
|
||||
height: calc(100vh - 8rem);
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.entry-collection-list {
|
||||
width: 30%;
|
||||
}
|
||||
.subtitle {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import eslintPlugin from "vite-plugin-eslint";
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import eslintPlugin from 'vite-plugin-eslint'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
server: {
|
||||
host: true,
|
||||
host: true
|
||||
},
|
||||
base: "/",
|
||||
base: '/',
|
||||
plugins: [vue(), eslintPlugin()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ["src/lib/Map.js"],
|
||||
},
|
||||
});
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
17
yarn.lock
@@ -581,6 +581,11 @@
|
||||
"@nodelib/fs.scandir" "2.1.5"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@popperjs/core@^2.11.6":
|
||||
version "2.11.6"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
|
||||
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
|
||||
|
||||
"@rollup/pluginutils@^4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz"
|
||||
@@ -1441,6 +1446,16 @@ boolbase@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz"
|
||||
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
|
||||
|
||||
bootstrap-icons@^1.10.3:
|
||||
version "1.10.3"
|
||||
resolved "https://registry.yarnpkg.com/bootstrap-icons/-/bootstrap-icons-1.10.3.tgz#c587b078ca6743bef4653fe90434b4aebfba53b2"
|
||||
integrity sha512-7Qvj0j0idEm/DdX9Q0CpxAnJYqBCFCiUI6qzSPYfERMcokVuV9Mdm/AJiVZI8+Gawe4h/l6zFcOzvV7oXCZArw==
|
||||
|
||||
bootstrap@^5.2.3:
|
||||
version "5.2.3"
|
||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.2.3.tgz#54739f4414de121b9785c5da3c87b37ff008322b"
|
||||
integrity sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz"
|
||||
@@ -3112,7 +3127,7 @@ geojson-vt@^3.2.1:
|
||||
|
||||
geovisio@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/geovisio/-/geovisio-1.4.0.tgz"
|
||||
resolved "https://registry.yarnpkg.com/geovisio/-/geovisio-1.4.0.tgz#d32cf97d97fe1dec0b5473b30699873c1df63523"
|
||||
integrity sha512-rju/xZLlGlhnzlzt1QbOA8V9eky9XfGbt3EvNzoawzvcFbvd1YJ0eGdJzrM5rm1xXraZkJ9E6UNnSKQUZ6w8oA==
|
||||
dependencies:
|
||||
documentation "^13.2.5"
|
||||
|
||||