Skip to content

feat(geoportal): panorama viewer with street-view navigation - #718

Open
helllth wants to merge 61 commits into
devfrom
feature/0711-add-panorama-viewer
Open

feat(geoportal): panorama viewer with street-view navigation#718
helllth wants to merge 61 commits into
devfrom
feature/0711-add-panorama-viewer

Conversation

@helllth

@helllth helllth commented Jun 25, 2026

Copy link
Copy Markdown
Member
deploy: ["geoportal"]

helllth added 21 commits June 22, 2026 14:34
Demonstrate a panorama "photobox" driven by a `panorama` field emitted from
the vector-style infoBoxMapping, independent of the react-cismap photo
lightbox (no react-cismap changes).

- PanoramaPreview: inline draggable 360° preview in the infobox (250x160),
  with an expand control
- PanoramaLightBox: fullscreen pannellum viewer portaled to document.body so
  it renders above the map; zoom and fullscreen controls hidden, mobile
  device-orientation (compass) control kept; close via close button / Esc
- wire the panorama branch into geoportal FeatureInfoBox and the shared
  portals FeatureInfobox
- add pannellum 2.5.7 dependency and a minimal type shim

Note: includes temporary [PANORAMA] debug logging to verify the field handover.
PanoramaPreview:
- full width on small screens / fixed thumbnail on desktop, using an explicit
  pixel width (the infobox places each secondary element in a shrink-to-fit
  cell, so a percentage width collapses to zero)
- init pannellum once and call viewer.resize() on width change to refit without
  reloading the image
- clean dark loading placeholder and an enlarged loading spinner
- 5px gap to the infobox header, matching the photo preview

PanoramaLightBox:
- render via a portal to document.body so it appears above the map
- hide zoom and fullscreen controls, keep the mobile device-orientation control
The responsive-design refactor extracted the preview's inline styles into
PanoramaPreview.css but dropped the expand button rule. Without absolute
positioning the button fell into normal flow below the full-height viewer
and was clipped by the container's overflow: hidden, so the fullscreen
control was invisible. Re-add the .carma-panorama-preview__expand rule.
…ction

The map's selection-arrow now tracks the live look-direction inside the 360°
panorama viewer (Street-View style), instead of only showing the photo's fixed
capture heading.

- PanoramaPreview / PanoramaLightBox emit an onYawChange(yaw) callback, driven
  by a requestAnimationFrame loop polling viewer.getYaw() (pannellum has no
  continuous view-change event). The callback is held in a ref so it never
  re-inits the viewer / reloads the image.
- FeatureInfoBox resolves the selected layer's maplibre map via getMaplibreMaps
  (keyed by selectedFeature.id) and sets selection-arrow's icon-rotate to
  heading + yaw. Since that layer only renders the selected feature, the
  layer-wide constant rotates just the visible arrow; the data-driven
  ['get','heading'] is restored on deselect/unmount.
- Only one viewer drives at a time: the inline preview yields to the fullscreen
  lightbox while it is open.
- Tunable PANORAMA_YAW_SIGN / PANORAMA_YAW_OFFSET constants for calibration.
Temporary [PANORAMA] debug log of the raw native maplibre features (e.hits) in
onSelectionChanged, before carma maps them into the redux selectedFeature, plus
a window.__panoramaHits handle for interactive inspection. Strip before merge.
Add Street-View-style navigation between panoramas plus the viewer-orientation
handling needed to make it usable:

- PanoramaLightBox: accept hotspots (new PanoramaHotspot type) and render them as
  clickable arrow-discs (PanoramaLightBox.css); clicking one hops to a
  neighbouring pano.
- FeatureInfoBox: find surrounding panos at runtime via querySourceFeatures
  using each feature's proj_x/proj_y/proj_z, place a hotspot per neighbour at its
  bearing with pitch refined from horizontal distance + altitude difference, and
  declutter by distance + angular separation. Navigation rebuilds the neighbour's
  carma feature via createFeature and selects it, so viewer, infobox and map
  arrow all follow the tour.
- Keep the map selection highlight a pure function of the redux selection
  (covers programmatic hops) and snap the arrow to the new heading on selection
  to avoid a stale-direction jump.
- Open the viewer facing forward (initialYaw 180; the image centre faces the back
  of the survey car) and gate yaw reporting on isLoaded() so the arrow does not
  briefly flip 180° before pannellum applies the initial yaw.
The selection-arrow's icon-rotate was overridden with a precomputed scalar
(selectedHeading + yaw). On selection change the cismap path highlights the new
feature one frame before the snap effect runs, so for that frame the arrow used
the previous feature's heading — a full heading error (~90° at a T-junction).

Write icon-rotate as an expression on the feature's own heading
(['+', ['get','heading'], yaw]) in the live follow, the on-select snap and the
reset. The visible (selected) arrow is then always rotated by its own heading,
so it is correct the instant the selection moves; only the small yaw delta can
lag, and that is zero when facing forward.
Pannellum shows a spinner and a load-progress box while a panorama
loads. For a cached or fast-decoding image this only flashes briefly,
which reads as a glitch. Hide the load indicator (pannellum sets its
display inline, so !important is required) and keep a dark #2a2a2a
placeholder to cover the short decode time, in both the inline preview
and the fullscreen lightbox. Tag the lightbox viewer container with a
carma-panorama-lightbox__viewer class so the CSS can target it.
Make the panorama tour work against a fully client-side geojson style
(e.g. oelberg_panorama/styleG.json) in addition to the vector-tile
style:

- resolvePanoSource now falls back to a geojson source (which has no
  source-layer) when no vector source is present, so hotspots, the
  selection highlight and tour navigation resolve correctly for both
  style types. Feature ids are used as-is: vector tiles bake id=fid and
  the geojson dataset carries id=fid natively, so the style must not set
  generateId (which would override the data's ids).
- Preload the reachable neighbours' panorama images on selection (Image
  + decode, held in a ref so the in-flight downloads are not GC'd) so a
  tour hop is near-instant. The hotspot memo now also surfaces the kept
  neighbours' file names; each neighbour URL is derived from the current
  panorama URL by swapping the file name.
- Guard the selection-arrow cleanup against an already-removed layer:
  getLayer/setLayoutProperty throw once the layer's map is torn down,
  which crashed when removing a layer while a panorama was selected.
Pannellum rewrites each hotspot host element's inline transform every
frame to reposition it as the user pans. The hotspot carried its own
transition: transform, so it animated toward each new position instead
of snapping there, making the arrows swim/lag and "settle" when panning
stopped. Move the disc and its hover-scale onto a ::before pseudo-element
(which pannellum never touches) and leave the host element transform-free,
so it stays locked to its bearing while hover still animates smoothly.
Each navigation hotspot now points toward the panorama it leads to,
Street-View style, instead of always pointing up. pannellum's
createTooltipFunc gives a handle to each rendered hotspot element; the
per-frame yaw poll sets a --pano-hotspot-dir CSS variable on each to the
on-screen angle from view-centre to that neighbour (its panorama yaw
minus the current view yaw), and the chevron is rotated by it. A
neighbour you face points up; one off to the side points that way.
Load a pannellum multiresolution panorama (tiled cube faces) when the
selected feature carries a panoramaMultiResConfig URL, falling back to
the equirectangular jpg otherwise. Multires paints a low-res cube
immediately and refines, so first view is far faster than pulling a
single large equirectangular image.

- new resolvePanoramaScene() fetches the generated config.json and gives
  it a basePath (its own directory) so pannellum can resolve the relative
  tile paths, which the generated config omits; on any failure (e.g. a
  pano not yet tiled -> 404) it falls back to the equirectangular image.
- PanoramaPreview and PanoramaLightBox take a multiResConfigUrl prop and
  resolve the scene asynchronously (guarded against teardown mid-fetch).
- FeatureInfoBox passes properties.panoramaMultiResConfig to both.
The lightbox used to destroy and recreate the whole pannellum WebGL
viewer on every tour hop, paying context teardown/setup plus a full
decode each time. Init the viewer once in tour mode (default + scenes)
and, on a hop, addScene + loadScene to crossfade to the new pano,
reusing the WebGL context; the fade also masks the decode.

- The viewer is created for the first scene and destroyed only on
  unmount (separate []-effect); the src/multiResConfigUrl effect now
  either creates it (first scene) or addScene+loadScene (subsequent).
- The yaw poll runs for the viewer's whole life across scene loads; the
  current scene's hotspot elements are swapped into a ref each hop so
  arrow re-aiming tracks whatever is on screen.
- Scenes are still built by resolvePanoramaScene, so multires and the
  jpg fallback work per scene.
- PannellumViewer gains addScene/loadScene; window.pannellum uses it.
Free pannellum's arrow keys in the fullscreen viewer
(disableKeyboardCtrl) and bind Up/Down to a tour hop: Up jumps to the
neighbour nearest the current view direction, Down to the one behind it.

- PanoramaLightBox takes onNext/onPrevious and fires them on ArrowUp/
  ArrowDown (preventing page scroll); dragging still pans.
- FeatureInfoBox captures the live view yaw from the viewer poll and
  picks the neighbour whose bearing is nearest the view (Up) or its
  opposite (Down), reusing the existing hotspot hop (loadScene
  crossfade). Only fires within a 90° hemisphere of the target, so Up
  never jumps to a pano clearly behind you.
- discover neighbours from the baked `nb` tile property (viewport-independent),
  falling back to querySourceFeatures where the tiles do not carry it yet
- render neighbours as faint, ground-pinned rings that recede down the street
  (per-cross ground tilt from pitch + distance scale), replacing the chevrons
- click anywhere jumps to the nearest cross; hover highlights that same cross;
  capture-phase listeners so the click beats pannellum's canvas drag handler
- keep arrow-key tour navigation; dev [PANORAMA] neighbour-source log remains
  for now (strip before merge)
…ading

- add a ground-pinned cursor arrow to the panorama lightbox: it follows the
  mouse and aims along the nearest cross's street (its heading), using a pinhole
  world->screen projection of the current view so it reads as lying on the road
- read each neighbour's heading from the baked `nb` (5th value) or, when the
  deployed tiles predate that field, from the live map source, so the aim works
  before a redeploy; resolve the heading's 180deg ambiguity toward the target
- add getPitch/getHfov/mouseEventToCoords to the pannellum viewer type

Dev [PANORAMA] logs still present; to be stripped before merge.
When navigating between panoramas in the fullscreen viewer, pan the
Leaflet base map to the pano being jumped to (keeping the current zoom),
so closing fullscreen leaves the just-visited point centered in the map.
The panorama behind the current view was only reachable with the
ArrowDown key or by panning 180°. Add an invisible hit zone pinned to
the bottom centre of the lightbox (over the survey vehicle) that, on
click, calls onPrevious (same as ArrowDown). The zone is not drawn; over
it the native cursor is replaced by a downward chevron drawn as a DOM
element that follows the pointer, matching the ground cursor arrow's
look (an SVG image cursor renders blank in Chrome).
Double the size of both the ground cursor arrow and the back-navigation
chevron (46->92px) and render them at 40% opacity for a subtler look.
Previously every tour hop reset the panorama to the forward (initial)
yaw. Now a hop keeps the user's current view direction and pitch, so
they can follow e.g. a sidewalk from pano to pano (the yaw frame is
street-relative, so the relative angle is preserved). Exception: a direct
click on a cross dot still resets to the forward, level view.
@nx-cloud

nx-cloud Bot commented Jun 25, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit e4ff911

Command Status Duration Result
nx run measurements-playground:build --configur... ✅ Succeeded 3m 22s View ↗
nx run lagis-desktop:build --configuration=prod... ✅ Succeeded 1m 9s View ↗
nx run rainhazardmap-xanten:build --configurati... ✅ Succeeded 1m 23s View ↗
nx run ng-topicmap-playground:build --configura... ✅ Succeeded 4m 23s View ↗
nx run solarpotentialmap-saarlouis:build --conf... ✅ Succeeded 1m View ↗
nx run belis-online:build --configuration=produ... ✅ Succeeded 1m 21s View ↗
nx run potenzialflaechen-online:build --configu... ✅ Succeeded 1m 9s View ↗
nx run rainhazardmap-paderborn:build --configur... ✅ Succeeded 1m 9s View ↗
Additional runs (51) ✅ Succeeded ... View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-07-22 08:55:56 UTC

@github-actions

Copy link
Copy Markdown

🚀 Deployed geoportal

Preview URL: https://cismet.github.io/carma-pr-deployments/718/geoportal/

Based on commit 7e95d6d

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

🚀 Deployed geoportal

Preview URL: https://cismet.github.io/carma-pr-deployments/718/geoportal/

Based on commit 7b969ca

@github-actions

Copy link
Copy Markdown

🚀 Deployed geoportal

Preview URL: https://cismet.github.io/carma-pr-deployments/718/geoportal/

Based on commit a19edef

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants