add header + gitlab cy

This commit is contained in:
Andreani Jean
2023-01-25 17:10:27 +01:00
parent f1f8501e66
commit b01b38bc57
40 changed files with 208 additions and 1956 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -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
View 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
View File

@@ -0,0 +1,5 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "none"
}

2
geovisio.d.ts vendored
View File

@@ -1 +1 @@
declare module "geovisio";
declare module 'geovisio'

View File

@@ -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

Binary file not shown.

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { RouterView } from "vue-router";
import { RouterView } from 'vue-router'
</script>
<template>

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
src/assets/images/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -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);

View File

@@ -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
View 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
View 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>

View File

@@ -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;

View File

@@ -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);
});
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,2 +0,0 @@
import Viewer from './Viewer';
export default Viewer;

View File

@@ -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();
});
});

View File

@@ -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')

View File

@@ -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

View File

@@ -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>

View File

@@ -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))
}
}
})

View File

@@ -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"