npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

spatiall-studio-sdk

v0.2.5

Published

TypeScript SDK for integrating a 3D/panorama viewer for Spatiall Studio data into any web-based project.

Readme

Spatiall Viewer SDK

A lightweight SDK that provides an easy-to-use interface for integrating a 3D/panorama viewer for Spatiall Studio data into any web-based project.

Overview

SpatiallViewer is a lightweight JavaScript SDK that:

  • Automatically loads the underlying viewer engine
  • Provides a simple class-based API
  • Works with any JavaScript framework (React, Vue, Next.js, Vanilla JS, etc.)
  • Uses ES modules for modern JavaScript integration

Installation

From npm

bun add spatiall-studio-sdk
# or: npm install spatiall-studio-sdk

Then import it by package name:

import { SpatiallViewer } from 'spatiall-studio-sdk';

Using a Local Build (bun link)

To develop against an unpublished local build, link the SDK in two steps.

First, register the SDK as a global link. Run this in the SDK repo — it builds dist/ and then registers the package globally:

# In the spatiall-viewer SDK repo
bun run link

Then link it into your own project:

# In your project
bun link spatiall-studio-sdk

Now import as normal:

import { SpatiallViewer } from 'spatiall-studio-sdk';

Changes to the SDK source are picked up after rebuilding it with bun run build in the SDK repo. For the full local-dev workflow, see DEVELOPMENT.md in the source repository.

How to Use

In client applications, import the SDK from the package name. The dist/ folder is only used by the internal example inside this repository.

1. Include in Your HTML

The SDK uses ES modules. Create your HTML with a module script:

<!DOCTYPE html>
<html>
  <head>
    <title>My Project</title>
  </head>
  <body>
    <div id="viewer"></div>

    <!-- Load your app as an ES module -->
    <script type="module" src="app.js"></script>
  </body>
</html>

2. Import and Initialize

For an example integration, see /example.

TypeScript:

import { SpatiallViewer, type InitializeOptions } from 'spatiall-studio-sdk';

document.addEventListener('DOMContentLoaded', async () => {
  const viewer = new SpatiallViewer({
    apiKey: 'your-api-key',
    targetElement: document.getElementById('viewer') ?? document.body,
  });

  // Loads the viewer engine. The viewer login itself happens on the first
  // navigation, once a coordinate is known (see note below).
  await viewer.initialize();

  // Resolves the publication for the coordinate, logs into the publication
  // with the returned token, and navigates to the photo location.
  await viewer.setFocusPosition(6.5963340441074845, 53.085994556165765);

  viewer.on('NAVIGATION_CHANGED', (data) => {
    console.log('Moved to', data.x, data.y);
  });
});

Alternatively, the coordinates can be passed directly into initialize to resolve, log in, and navigate to a starting photo on load:

import { SpatiallViewer, type InitializeOptions } from 'spatiall-studio-sdk';

document.addEventListener('DOMContentLoaded', async () => {
  const viewer = new SpatiallViewer({
    apiKey: 'your-api-key',
    targetElement: document.getElementById('viewer') ?? document.body,
  });

  const options: InitializeOptions = {
    initialX: 6.5963340441074845,
    initialY: 53.085994556165765,
  };

  await viewer.initialize(options);
});

Or plain JavaScript (the published types still provide editor IntelliSense):

import { SpatiallViewer } from 'spatiall-studio-sdk';

const viewer = new SpatiallViewer({
  apiKey: 'your-api-key',
  targetElement: document.getElementById('viewer'),
});

await viewer.initialize({
  initialX: 6.5963340441074845,
  initialY: 53.085994556165765,
});

If you call initialize() without initial coordinates, the viewer login is deferred to the first goTo/setFocusPosition. Each navigation re-resolves; crossing into a different publication transparently re-logs into it.

Exported Types

import type {
  SpatiallViewerOptions,
  InitializeOptions,
  NavigationChangedPayload,
  ViewDirectionChangedPayload,
  SpatiallViewerEventMap,
  MeasurementFeature,
  FetchPhotoLocationsOptions,
  PhotoLocation,
  PhotoLocationCollection,
  PhotoLocationProps,
} from 'spatiall-studio-sdk';

Framework Integration

React Example

import { useEffect, useRef } from 'react';
import { SpatiallViewer } from 'spatiall-studio-sdk';

export function MapViewer() {
  const divRef = useRef(null);

  useEffect(() => {
    let viewer = null;

    const initViewer = async () => {
      viewer = new SpatiallViewer({
        apiKey: 'your-api-key',
        targetElement: divRef.current,
      });

      await viewer.initialize();
      await viewer.setFocusPosition(6.5963340441074845, 53.085994556165765);
    };

    initViewer();

    return () => {
      if (viewer) viewer.destroy();
    };
  }, []);

  return <div ref={divRef} style={{ width: '100%', height: '100vh' }} />;
}

Next.js Example

'use client';

import { useEffect, useRef } from 'react';

export default function MapViewer() {
  const divRef = useRef(null);

  useEffect(() => {
    let viewer = null;

    const initViewer = async () => {
      // Dynamic import for client-side only
      const { SpatiallViewer } = await import('spatiall-studio-sdk');

      viewer = new SpatiallViewer({
        apiKey: 'your-api-key',
        targetElement: divRef.current,
      });

      await viewer.initialize();
      await viewer.setFocusPosition(6.5963340441074845, 53.085994556165765);
    };

    initViewer();

    return () => {
      if (viewer) viewer.destroy();
    };
  }, []);

  return <div ref={divRef} style={{ width: '100%', height: '100vh' }} />;
}

API Reference

Constructor

new SpatiallViewer(options);

Options:

  • apiKey (string, required): Organization API key. Sent to the resolve endpoint as Authorization: Api-Key <apiKey>
  • targetElement (HTMLElement, optional): DOM element where the viewer will be rendered. Defaults to document.body if not specified
  • applicationName (string, optional): Custom application name. Defaults to "Spatiall Viewer"
  • defaultCrs (string, optional): Coordinate reference system this viewer instance uses whenever no other one is given — both for x/y arguments you pass in and for the coordinates the viewer reports back in its events (default: '4326'); Supported srids: 4326; 28992.

Set defaultCrs once and the whole instance speaks that CRS:

const viewer = new SpatiallViewer({ apiKey, targetElement, defaultCrs: '28992' });
await viewer.initialize();

// No crs argument needed — 28992 is the default now.
await viewer.setFocusPosition(233413, 581099);

viewer.on('NAVIGATION_CHANGED', ({ x, y, crs }) => {
  console.log(x, y, crs); // 233413 581099 '28992'
});

Methods

initialize(options)

Asynchronously initializes the viewer. This is the main entry point.

await viewer.initialize({
  initialX: 6.5963340441074845, // Optional
  initialY: 53.085994556165765, // Optional
  initialCrs: '4326', // Optional, defaults to the instance's `defaultCrs`
});

Parameters:

  • options (Object, optional)
    • initialX (number): Initial X coordinate to navigate to
    • initialY (number): Initial Y coordinate to navigate to
    • initialCrs (string): Coordinate reference system of initialX / initialY only — it does not change the CRS of anything else. Defaults to the instance's defaultCrs (itself '4326' unless set on the constructor); Supported srids: 4326; 28992.

goTo(x, y, [crs])

Navigate to a specific location.

await viewer.goTo(6.5963340441074845, 53.085994556165765, '4326');

Parameters:

  • x (number): X coordinate
  • y (number): Y coordinate
  • crs (string, optional): Coordinate reference system (defaults to the instance's defaultCrs, itself '4326' unless set)

addOverlay(geojson, name, [sourceSrs], color)

Draw a vector overlay layer from GeoJSON. The layer becomes a named overlay resource in the viewer, so calling addOverlay again with the same name replaces the previous geometry rather than stacking a second copy.

const geojson = {
  type: 'FeatureCollection',
  features: [
    {
      type: 'Feature',
      properties: {},
      geometry: {
        type: 'Polygon',
        coordinates: [
          [
            [6.5963, 53.0859],
            [6.5965, 53.0859],
            [6.5965, 53.0861],
            [6.5963, 53.0861],
            [6.5963, 53.0859],
          ],
        ],
      },
    },
  ],
};

await viewer.addOverlay(geojson, 'my-layer', '4326', '#cc0000');

Parameters:

  • geojson (GeoJSON, required): A GeoJSON Geometry, Feature, or FeatureCollection. Coordinates are interpreted in sourceSrs.
  • name (string, required): Unique layer name. This is also the overlay resource name used by removeOverlay, so reusing a name replaces that layer.
  • sourceSrs (string, optional): EPSG code of the GeoJSON coordinates (e.g. '4326' for lon/lat). Defaults to '28992' (RD / Rijksdriehoek).
  • color (string, required): Layer color as a hex value (#rgb or #rrggbb). Used for a solid outline and a semi-transparent polygon fill.

Notes:

  • The viewer must be initialized first; calling addOverlay before initialize() throws.
  • The call waits for the viewer to be ready and the workspace to open before drawing, so it is safe to call right after initialize().

setFocusPosition(x, y, [crs], [year])

Navigate to a specific location.

await viewer.setFocusPosition(6.5963340441074845, 53.085994556165765, '4326');
// Open the 2024 panorama at the location:
await viewer.setFocusPosition(6.5963340441074845, 53.085994556165765, '4326', 2024);

Parameters:

  • x (number): X coordinate
  • y (number): Y coordinate
  • crs (string, optional): Coordinate reference system (defaults to the instance's defaultCrs, itself '4326' unless set)
  • year (number, optional): Capture year of the imagery to show at the location. How the viewer gets there, and what it shows when that year has no imagery nearby, depends on the viewer implementation.

removeOverlay(name)

Remove a previously drawn overlay layer by name.

await viewer.removeOverlay('my-layer');

Parameters:

  • name (string, required): The layer name passed to addOverlay.

If no layer with that name exists, the call is a no-op.

fetchPhotoLocations(options)

Fetch photo capture locations within a map viewport as GeoJSON, so you can show them on your own map (Mapbox, MapLibre, Leaflet, OpenLayers, …) and let users click one to open that photo in the viewer.

Returns a GeoJSON FeatureCollection of Point features ([lon, lat]), each with PhotoLocationProps: { id, utcTimestamp, year, pan, tilt, roll }.

const photos = await viewer.fetchPhotoLocations({
  bbox: [4.8, 52.7, 4.83, 52.72], // current map view: [minLon, minLat, maxLon, maxLat]
  year: 2023, // optional: a single campaign year
});

Parameters:

  • options (Object, required)
    • bbox ([minLon, minLat, maxLon, maxLat], required): the current map view, in WGS84 (EPSG:4326).
    • year (number, optional): restrict to one recording campaign year.
    • limit (number, optional): max features to return (default 5000).
    • signal (AbortSignal, optional): cancel an in-flight request, e.g. when the map moves again.

Supported CRS:

  • WGS84 (EPSG:4326)

The dataset is very large, so always pass the current bbox and re-fetch when the map moves. A point carries no photo URL — open its photo by passing its coordinates to setFocusPosition:

// Reload photo locations whenever the map stops moving. Read the current
// view bounds in the form of:
// [minLon, minLat, maxLon, maxLat].
map.on('moveend', async () => {
  const bbox = [minLon, minLat, maxLon, maxLat];
  const photos = await viewer.fetchPhotoLocations({ bbox });
  map.getSource('photos').setData(photos); // a GeoJSON source you added once
});

// Click a point -> open its photo in the viewer.
map.on('click', 'photos', (e) => {
  const [lon, lat] = e.features[0].geometry.coordinates;
  viewer.setFocusPosition(lon, lat);
});

on(event, callback)

Subscribe to a viewer event. The callback receives that event's payload (see Events below).

viewer.on('NAVIGATION_CHANGED', ({ x, y }) => {
  console.log('Moved to', x, y);
});

off(event, callback)

Remove a previously registered handler. Pass the same function reference used with on.

viewer.off('NAVIGATION_CHANGED', handler);

Parameters:

  • event (string): Event name
  • callback (function): The handler reference to remove

destroy()

Destroy the viewer and clean up resources.

viewer.destroy();

Events

Subscribe with on(event, callback) and remove handlers with off(event, callback). Payload types are described by the exported SpatiallViewerEventMap.

NAVIGATION_CHANGED

Fires when the focus position changes, including on every goTo / setFocusPosition.

  • Payload: { x: number, y: number, crs: string } — x/y are expressed in the defaultCrs given to the constructor ('4326' when none was), and crs states which CRS that is. If the viewer cannot reproject into defaultCrs, the coordinate is reported as-is and crs names the CRS it really is in, so the payload is always self-describing.

VIEWDIRECTION_CHANGED

Fires when the camera orientation changes.

  • Payload: { yaw: number, pitch: number } — both angles in degrees.
    • yaw is the compass heading, in the range [0, 360): 0 is north, 90 east, 180 south, 270 west. It never returns exactly 360.
    • pitch is the vertical angle, in the range [-90, 90]: 0 is level with the horizon, positive values look up, negative values look down.

MEASUREMENT_CREATED

Fires when the user completes a measurement; in-progress measurements are not emitted.

  • Payload: MeasurementFeature — a 3D GeoJSON Feature whose properties.mode is 'point', 'line', or 'polygon' and whose coordinates are [longitude, latitude, height].

YEAR_CHANGED

Fires when the capture year of the panorama shown by the viewer changes, including once at startup with the year of the first photo opened.

  • Payload: { year: number | null } — the capture year of the panorama currently shown, or null when it can't be determined. The event only fires when the year actually changes, so navigating between photos of the same year stays quiet.

Notes

  • The SDK automatically handles loading the viewer engine from the server
  • Make sure CORS is properly configured if serving from different domains
  • Your integration should include a way to set the api key by the user: Do not ship your own api key into the integration.
  • The SDK creates all necessary DOM elements internally - users only need to provide a target container

Migration Guide

Navigation, event subscription, and teardown are otherwise unchanged, so most integrations are a small edit.

At a glance

| Area | POC build | This release | | -------------- | ------------------------------------------------------------------ | ------------------------------------------------------ | | Distribution | Copy sdk/spatiall-viewer.js into your project | Install the spatiall-studio-sdk package | | Import | import { SpatiallViewer } from '/path/to/sdk/spatiall-viewer.js' | import { SpatiallViewer } from 'spatiall-studio-sdk' | | Authentication | username + password | apiKey | | Publication | Hard-coded in SDK_CONFIG | Resolved per coordinate from your API key | | initialize() | Logs in and renders immediately | Loads the engine; renders on first navigation | | Language | Plain JavaScript | TypeScript (ships type definitions) |

1. Install the package instead of copying a file

The SDK is no longer a file you vendor into your project. Install it (see Installation above) and import it by package name:

- import { SpatiallViewer } from '/path/to/sdk/spatiall-viewer.js';
+ import { SpatiallViewer } from 'spatiall-studio-sdk';

2. Replace username / password with apiKey

Credential-based login has been removed. The constructor now takes a single apiKey:

  const viewer = new SpatiallViewer({
-   username: 'your-username',
-   password: 'your-password',
+   apiKey: 'your-api-key',
    targetElement: document.getElementById('viewer'),
  });

Constructing without an apiKey now throws SpatiallViewer requires an apiKey (previously SpatiallViewer requires username and password).

Provide a way for the end user to supply the key — do not ship your own API key in the integration.

3. Stop configuring the host and publication

In the POC you pointed the SDK at a customer/environment by editing ORBIT_HOST and PUBLICATION_NAME in SDK_CONFIG. That step is gone, and the viewer is no longer tied to a single publication for the whole session:

If your integration edited SDK_CONFIG, remove that entirely.

4. initialize() no longer logs in on its own

Because a publication can only be resolved once a coordinate is known, initialize() now just loads the engine. It does not log in or render anything by itself. (It also no longer waits on the POC's fixed 2-second startup delay.)

To reproduce the POC's "renders immediately" behavior, pass initial coordinates so the SDK resolves, logs in, and navigates in one step:

await viewer.initialize({
  initialX: 6.5963340441074845,
  initialY: 53.085994556165765,
});

If you call initialize() without coordinates, nothing is shown until your first goTo() / setFocusPosition(), at which point login happens lazily. See the note under Import and Initialize above.

5. goTo() will become setFocusPosition().

goTo() is deprecated; please replace with setFocusPosition(). For now, both methods will work.

Contributing / Development

To develop on the SDK itself (build, lint, run the example), see DEVELOPMENT.md in the source repository.