From 558b19f8d7f0de4a949d9d33208f7a08e74febdc Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sun, 30 Mar 2025 19:38:36 +0200 Subject: [PATCH] Feature(360): attempt to fix CSP --- scripts/generateLayouts.ts | 2 +- src/Logic/ImageProviders/ImageProvider.ts | 24 +++++ src/UI/Image/ImagePreview.svelte | 2 +- src/UI/Image/NearbyImages.svelte | 34 ++++-- src/UI/Image/photoSphereViewerWrapper.ts | 125 ++++++++++++++-------- 5 files changed, 132 insertions(+), 55 deletions(-) diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index 79a7469aa..39e55419d 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -426,7 +426,7 @@ class GenerateLayouts extends Script { const csp: Record = { "default-src": "'self'", "child-src": "'self' blob: ", - "img-src": "* data:", // maplibre depends on 'data:' to load + "img-src": "* data: blob:", // maplibre depends on 'data:' to load "report-to": "https://report.mapcomplete.org/csp", "worker-src": "'self' blob:", // Vite somehow loads the worker via a 'blob' "style-src": "'self' 'unsafe-inline'", // unsafe-inline is needed to change the default background pin colours diff --git a/src/Logic/ImageProviders/ImageProvider.ts b/src/Logic/ImageProviders/ImageProvider.ts index 3bb2a9bf0..6cb152772 100644 --- a/src/Logic/ImageProviders/ImageProvider.ts +++ b/src/Logic/ImageProviders/ImageProvider.ts @@ -33,6 +33,30 @@ export interface PanoramaView { pitchOffset?: number } +/** + * The property of "nearbyFeatures" in ImagePreview.svelte. + * These properties declare how they are rendered + */ +export interface HotspotProperties { + /** + * The popup text when hovering + */ + name: string + + /** + * If true: the panorama view will automatically turn towards this object + */ + focus: boolean + /** + * The pitch degrees to display this. + * If "auto": will determine the pitch automatically based on distance + */ + pitch: number | "auto" + + gotoPanorama: Feature + +} + export default abstract class ImageProvider { public abstract readonly defaultKeyPrefixes: string[] diff --git a/src/UI/Image/ImagePreview.svelte b/src/UI/Image/ImagePreview.svelte index 165d3da31..a96a19982 100644 --- a/src/UI/Image/ImagePreview.svelte +++ b/src/UI/Image/ImagePreview.svelte @@ -6,7 +6,7 @@ import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider" import { UIEventSource } from "../../Logic/UIEventSource" import Zoomcontrol from "../Zoomcontrol" - import { getContext, onDestroy } from "svelte" + import { onDestroy } from "svelte" import type { PanoramaView } from "./photoSphereViewerWrapper" import { PhotoSphereViewerWrapper } from "./photoSphereViewerWrapper" diff --git a/src/UI/Image/NearbyImages.svelte b/src/UI/Image/NearbyImages.svelte index 6b31f6a4b..f291bcbe4 100644 --- a/src/UI/Image/NearbyImages.svelte +++ b/src/UI/Image/NearbyImages.svelte @@ -25,6 +25,7 @@ import { BBox } from "../../Logic/BBox" import PanoramaxLink from "../BigComponents/PanoramaxLink.svelte" import { GeoOperations } from "../../Logic/GeoOperations" + import type { PanoramaView } from "../../Logic/ImageProviders/ImageProvider" export let tags: UIEventSource export let state: SpecialVisualizationState @@ -55,14 +56,16 @@ let asFeatures = result.map((p4cs) => p4cs.map( (p4c) => - >{ + >{ type: "Feature", geometry: { type: "Point", coordinates: [p4c.coordinates.lng, p4c.coordinates.lat] }, - properties: { + properties: { id: p4c.pictureUrl, + url: p4c.pictureUrl, + northOffset: p4c.direction, rotation: p4c.direction, spherical: p4c.details.isSpherical ? "yes" : "no" } @@ -145,15 +148,24 @@ highlighted.set(feature.properties.id) } }) - let nearbyFeatures: Feature[] = [{ - type: "Feature", - geometry: { type: "Point", coordinates: GeoOperations.centerpointCoordinates(feature) }, - properties: { - name: layer.title?.GetRenderValue(feature.properties).Subs(feature.properties).txt, - focus: true - } - } - ] + let nearbyFeatures: Store = asFeatures.map(nearbyPoints => { + return [{ + type: "Feature", + geometry: { type: "Point", coordinates: GeoOperations.centerpointCoordinates(feature) }, + properties: { + name: layer.title?.GetRenderValue(feature.properties).Subs(feature.properties).txt, + focus: true + } + }, ...nearbyPoints.filter(p => p.properties.spherical === "yes").map(f => ({ + ...f, properties: { + name: "Nearby panorama", + pitch: "auto", + type: "scene", + gotoPanorama: f + } + })) + ] + }) onDestroy( tags.addCallbackAndRunD((tags) => { diff --git a/src/UI/Image/photoSphereViewerWrapper.ts b/src/UI/Image/photoSphereViewerWrapper.ts index ad71fb39f..02c25fae9 100644 --- a/src/UI/Image/photoSphereViewerWrapper.ts +++ b/src/UI/Image/photoSphereViewerWrapper.ts @@ -1,65 +1,106 @@ import "pannellum" -import { Feature, Point } from "geojson" +import { Feature, Geometry, Point } from "geojson" import { GeoOperations } from "../../Logic/GeoOperations" -import { PanoramaView } from "../../Logic/ImageProviders/ImageProvider" +import { HotspotProperties, PanoramaView } from "../../Logic/ImageProviders/ImageProvider" export class PhotoSphereViewerWrapper { - private readonly imageInfo: Feature + private imageInfo: Feature private readonly viewer: Pannellum.Viewer + private nearbyFeatures: Feature[] = [] - - constructor(container: HTMLElement, imageInfo: Feature, nearbyFeatures?: Feature[]) { + constructor(container: HTMLElement, imageInfo: Feature, nearbyFeatures?: Feature[]) { this.imageInfo = imageInfo - this.viewer = pannellum.viewer(container, { - type: "equirectangular", - hfov: 110, - panorama: imageInfo.properties.url, - autoLoad: true, - hotSpots: [], - compass: true, - showControls: false, - northOffset: imageInfo.properties.northOffset, - horizonPitch: imageInfo.properties.pitchOffset - }) - - /* for (let i = 0; i < 360; i += 45) { - - viewer.addHotSpot({ - type: "info", - yaw: i, - text: "YAW " + i - }) - } - - console.log("North offset:", imageInfo.properties.northOffset) - viewer.addHotSpot({ - type: "info", - yaw: -northOffs, - text: "Supposedely north " - })*/ + this.viewer = pannellum.viewer(container, + { + default: { + firstScene: imageInfo.properties.url, + sceneFadeDuration: 250 + }, + scenes: { + [imageInfo.properties.url]: + { + type: "equirectangular", + hfov: 110, + panorama: imageInfo.properties.url, + autoLoad: true, + hotSpots: [], + sceneFadeDuration: 250, + compass: true, + showControls: false, + northOffset: imageInfo.properties.northOffset, + horizonPitch: imageInfo.properties.pitchOffset + } + } + } + ) this.setNearbyFeatures(nearbyFeatures) } - public setNearbyFeatures(nearbyFeatures: Feature[]) { + public calculatePitch(feature: Feature): number { + const coors = this.imageInfo.geometry.coordinates + const distance = GeoOperations.distanceBetween( + <[number, number]>coors, GeoOperations.centerpointCoordinates(feature) + ) + + // In: -pi/2 up to pi/2 + const alpha = Math.atan(distance / 4) // in radians + const degrees = alpha * 360 / (2 * Math.PI) + return -degrees + } + + private setPanorama(imageInfo: Feature) { + if (this.viewer?.getScene() === imageInfo?.properties?.url) { + // Already the current scene + return + } + this.clearHotspots() + this.imageInfo = imageInfo + this.viewer.addScene(imageInfo.properties.url, { + panorama: imageInfo.properties.url, + northOffset: imageInfo.properties.northOffset, + type: "equirectangular" + }) + + this.viewer.loadScene(imageInfo.properties.url, 0, imageInfo.properties.northOffset) + this.setNearbyFeatures(this.nearbyFeatures) + } + + private clearHotspots() { + const hotspots = this.viewer.getConfig()["scenes"][this.imageInfo.properties.url].hotSpots ?? [] + for (const hotspot of hotspots) { + this.viewer.removeHotSpot(hotspot?.id, this.imageInfo.properties.url) + } + } + + public setNearbyFeatures(nearbyFeatures: Feature[]) { const imageInfo = this.imageInfo const northOffs = imageInfo.properties.northOffset - - const hotspots = this.viewer.getConfig().hotSpots ?? [] - for (const hotspot of hotspots) { - this.viewer.removeHotSpot(hotspot.id) - } - // this.viewer.removeHotSpot() + this.nearbyFeatures = nearbyFeatures + this.clearHotspots() for (const f of nearbyFeatures ?? []) { + if (f.properties.gotoPanorama?.properties?.url === this.imageInfo.properties.url) { + continue // This is the current panorama, no need to show it + } const yaw = GeoOperations.bearing(imageInfo, GeoOperations.centerpoint(f)) + let pitch = 0 + if (f.properties.pitch === "auto") { + pitch = this.calculatePitch(f) + } else if (!isNaN(f.properties.pitch)) { + pitch = f.properties.pitch + } this.viewer.addHotSpot({ - type: "info", + type: f.properties.gotoPanorama !== undefined ? "scene" : "info", yaw: (yaw - northOffs) % 360, - text: f.properties.name - }) + pitch, + text: f.properties.name, + clickHandlerFunc: () => { + this.setPanorama(f.properties.gotoPanorama) + } + }, this.imageInfo.properties.url) if (f.properties.focus) { this.viewer.setYaw(yaw - northOffs) }