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

cesium-properties-popup

v0.2.1

Published

A Svelte component for displaying entity properties on hover or click in Cesium

Readme

Cesium Properties Popup

npm version License: MIT

A modern Svelte 5 component library for displaying entity properties on hover or click in CesiumJS. Supports entity types: Points, Polygons, Lines (Polylines)

demo site: https://d2hgd9m8me42il.cloudfront.net/

Table of Contents

Installation

# npm
npm install cesium-properties-popup

# pnpm
pnpm add cesium-properties-popup

# yarn
yarn add cesium-properties-popup

This library has peer dependencies on cesium and svelte:

# Required peer dependencies
npm install cesium svelte

Basic Usage

<script lang="ts">
	import { onMount } from 'svelte';
	import { browser } from '$app/environment';
	import { EntityPopup } from 'cesium-properties-popup';
	import type * as CesiumType from 'cesium';

	// Cesium module is dynamically imported, so use import type
	let cesium: typeof CesiumType | undefined = $state();
	let viewer: CesiumType.Viewer | undefined = $state();
	let viewerReady = $state(false);

	onMount(async (): Promise<void> => {
		if (!browser) return;

		try {
			// Import Cesium module only in browser
			cesium = (await import('cesium')) as typeof CesiumType;
			await import('cesium/Build/Cesium/Widgets/widgets.css');

			// Get necessary exports from Cesium module
			const { Ion, Viewer: CesiumViewer, Cartesian3 } = cesium;

			// Set access token
			Ion.defaultAccessToken = 'your_cesium_ion_access_token';

			// Initialize Viewer
			viewer = new CesiumViewer('cesiumContainer');

			// Set initial camera position
			viewer.camera.setView({
				destination: Cartesian3.fromDegrees(139.754409, 35.670355, 5000)
			});

			// Indicate that viewer is ready
			viewerReady = true;
		} catch (error) {
			console.error('Failed to initialize Cesium:', error);
		}
	});
</script>

<!-- Container for rendering Cesium -->
<div id="cesiumContainer" class="h-full w-full"></div>

<!-- Display EntityPopup when viewer is ready -->
{#if viewerReady && viewer && cesium}
	<EntityPopup {viewer} {cesium} />
{/if}

The EntityPopup component automatically displays properties of Cesium entities when users hover over them or click on them in the scene.

Supported Entity Types

This library supports various Cesium entity types:

  • Points
  • Lines (Polylines)
  • Polygons

Layer Control

DataSource-based Popup Control

You can control popup display on a per-DataSource (layer) basis. This allows you to show popups only for specific layers or exclude certain layers.

Show Only Specific Layers (Whitelist)

const popup = new EntityPopup({
	target: document.body,
	props: {
		viewer,
		cesium,
		options: {
			// Show popups only for specified DataSource entities
			includeDataSources: ['road-stations', 'railways', 'contents']
		}
	}
});

Exclude Specific Layers (Blacklist)

const popup = new EntityPopup({
	target: document.body,
	props: {
		viewer,
		cesium,
		options: {
			// Hide popups for specified DataSource entities
			excludeDataSources: ['lakes', 'background']
		}
	}
});

Practical Example: Organizing Layers

// Create DataSources with names matching demo layers
const roadStationsDataSource = new Cesium.CustomDataSource('road-stations');
const railwaysDataSource = new Cesium.CustomDataSource('railways');
const lakesDataSource = new Cesium.CustomDataSource('lakes');
const contentsDataSource = new Cesium.CustomDataSource('contents');

viewer.dataSources.add(roadStationsDataSource);
viewer.dataSources.add(railwaysDataSource);
viewer.dataSources.add(lakesDataSource);
viewer.dataSources.add(contentsDataSource);

// Add road station entities (points)
roadStationsDataSource.entities.add({
	position: Cesium.Cartesian3.fromDegrees(139.7, 35.6),
	point: {
		pixelSize: 10,
		color: Cesium.Color.RED,
		heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
	},
	properties: {
		P35_006: 'Tokyo Station',
		P35_003: 'Tokyo',
		P35_004: 'Chiyoda'
	}
});

// Add railway entities (polylines)
railwaysDataSource.entities.add({
	polyline: {
		positions: Cesium.Cartesian3.fromDegreesArray([139.7, 35.6, 139.8, 35.7]),
		width: 3,
		material: Cesium.Color.YELLOW,
		clampToGround: true
	},
	properties: {
		N02_003: 'Yamanote Line',
		N02_004: 'JR East'
	}
});

// Configure popup to exclude lakes layer
const popupOptions = {
	excludeDataSources: ['lakes']
};

Dynamic Layer Control

// Initialize with all layers enabled
let popupOptions = {
	excludeDataSources: []
};

// Function to toggle layer popup display
function toggleLayerPopup(dataSourceName, enabled) {
	if (enabled) {
		// Remove from exclusion list (enable popups)
		const index = popupOptions.excludeDataSources.indexOf(dataSourceName);
		if (index > -1) {
			popupOptions.excludeDataSources.splice(index, 1);
		}
	} else {
		// Add to exclusion list (disable popups)
		if (!popupOptions.excludeDataSources.includes(dataSourceName)) {
			popupOptions.excludeDataSources.push(dataSourceName);
		}
	}

	// Update popup options
	popup.$set({ options: popupOptions });
}

// Example usage with UI checkboxes
toggleLayerPopup('road-stations', false); // Disable road stations popups
toggleLayerPopup('railways', true); // Enable railways popups
toggleLayerPopup('lakes', false); // Disable lakes popups
toggleLayerPopup('contents', true); // Enable contents popups

Priority Rules

When using both settings:

  1. includeDataSources (whitelist) - If specified, only DataSources included will show popups
  2. excludeDataSources (blacklist) - Applied when includeDataSources is not specified

Important Notes

  • DataSource name property is used for identification, so always set names when creating DataSources
  • Entities not belonging to any DataSource (directly under viewer.entities) will show popups by default
  • DataSources without names cannot be controlled

Using Regular Expression Patterns

You can use regular expressions for flexible DataSource filtering:

RegExp Pattern Examples

const popup = new EntityPopup({
	target: document.body,
	props: {
		viewer,
		cesium,
		options: {
			// Mix of exact match and regex patterns
			includeDataSources: [
				'exact-layer-name', // Exact match
				/^road-.*/, // Starts with "road-"
				/.*-stations$/, // Ends with "-stations"
				/railway|railroad/ // Contains "railway" OR "railroad"
			]
		}
	}
});

Practical Regex Examples

// Show popups only for layers starting with "data-"
const popupOptions = {
	includeDataSources: [/^data-/]
};

// Exclude all temporary or test layers
const popupOptions = {
	excludeDataSources: [
		/^temp-/, // Starts with "temp-"
		/^test-/, // Starts with "test-"
		/-draft$/ // Ends with "-draft"
	]
};

// Complex pattern: show only specific prefixes
const popupOptions = {
	includeDataSources: [
		/^(road|rail|building)-/ // Starts with "road-", "rail-", or "building-"
	]
};

Pattern Matching Rules

  1. String patterns perform exact matching

    'layer-name'; // Matches only "layer-name" exactly
  2. RegExp patterns use JavaScript regular expressions

    /^prefix-/    // Matches any name starting with "prefix-"
    /.*-suffix$/  // Matches any name ending with "-suffix"
    /pattern/i    // Case-insensitive matching
  3. Mixed patterns can be used together

    includeDataSources: ['exact-name', /^pattern-.*$/, /.*-other$/];

TypeScript Usage with Patterns

import { EntityPopup } from 'cesium-properties-popup';
import type { DataSourcePattern } from 'cesium-properties-popup';

// Type-safe pattern definition
const includePatterns: DataSourcePattern[] = ['exact-layer', /^prefix-.*/, /.*-suffix$/];

const popupOptions = {
	includeDataSources: includePatterns
};

Event Callbacks

You can use callback functions to execute custom logic when entities are clicked or hovered. The callbacks receive detailed context information including the entity, mouse position, and DataSource name.

Basic Callback Usage

<script lang="ts">
	import { EntityPopup } from 'cesium-properties-popup';
	import type { EntityEventContext } from 'cesium-properties-popup';

	const popupOptions = {
		// Entity click callback
		onEntityClick: (context: EntityEventContext) => {
			console.log('Entity clicked:', context.entity);
			console.log('DataSource:', context.dataSourceName);
			console.log('Position:', context.position);
		},

		// Entity hover callback
		onEntityHover: (context: EntityEventContext) => {
			console.log('Entity hovered:', context.entity);
		},

		// Empty space click callback
		onEmptyClick: (position: { x: number; y: number }) => {
			console.log('Empty space clicked at:', position);
		}
	};
</script>

{#if viewerReady && viewer && cesium}
	<EntityPopup {viewer} {cesium} options={popupOptions} />
{/if}

EntityEventContext Interface

The callback functions receive a context object with the following properties:

interface EntityEventContext {
	/** The entity that was clicked or hovered */
	entity: Cesium.Entity;

	/** Mouse position in screen coordinates (pixels) */
	position: {
		x: number;
		y: number;
	};

	/** Event type ('click' or 'hover') */
	eventType: 'click' | 'hover';

	/** Name of the DataSource that owns the entity (if available) */
	dataSourceName?: string;
}

Controlling Popup Display

You can control whether the popup is displayed independently of callbacks using the showPopup option:

const popupOptions = {
	// Disable automatic popup display
	showPopup: false,

	// Only use callbacks without showing popup
	onEntityClick: (context: EntityEventContext) => {
		// Custom logic, like opening a custom panel
		showCustomPanel(context.entity);
	}
};

Practical Examples

1. Analytics Tracking

const popupOptions = {
	onEntityClick: (context: EntityEventContext) => {
		// Track entity clicks for analytics
		trackEvent('entity_click', {
			entityId: context.entity.id,
			dataSource: context.dataSourceName,
			timestamp: new Date().toISOString()
		});
	}
};

2. Custom Data Loading

const popupOptions = {
	onEntityClick: async (context: EntityEventContext) => {
		// Load additional data when entity is clicked
		const entityId = context.entity.id;
		const additionalData = await fetchDetailedInfo(entityId);

		// Update UI with loaded data
		updateDetailPanel(additionalData);
	}
};

3. Filtering by DataSource

const popupOptions = {
	onEntityClick: (context: EntityEventContext) => {
		// Different behavior based on DataSource
		if (context.dataSourceName === 'buildings') {
			showBuildingDetails(context.entity);
		} else if (context.dataSourceName === 'roads') {
			showRoadInformation(context.entity);
		}
	}
};

4. Conditional Popup Display

let showPopupForEntity = $state(true);

const popupOptions = {
	// Control popup visibility dynamically
	showPopup: showPopupForEntity,

	onEntityClick: (context: EntityEventContext) => {
		// Decide whether to show popup based on entity properties
		const priority = context.entity.properties?.priority?.getValue();
		showPopupForEntity = priority === 'high';

		// Always log the click regardless of popup visibility
		console.log('Clicked entity with priority:', priority);
	}
};

5. Integrating with External UI

import { writable } from 'svelte/store';

const selectedEntityStore = writable<Cesium.Entity | null>(null);

const popupOptions = {
	// Disable built-in popup
	showPopup: false,

	// Use custom UI instead
	onEntityClick: (context: EntityEventContext) => {
		// Update store to trigger custom UI component
		selectedEntityStore.set(context.entity);
	},

	onEmptyClick: () => {
		// Clear selection when clicking empty space
		selectedEntityStore.set(null);
	}
};

6. Error Handling in Async Callbacks

const popupOptions = {
	onEntityClick: async (context: EntityEventContext) => {
		try {
			// Async operation that might fail
			const data = await fetchEntityData(context.entity.id);
			processData(data);
		} catch (error) {
			// Error is automatically logged by the library
			// You can add custom error handling here
			showErrorNotification('Failed to load entity data');
		}
	}
};

7. Hover Preview with Click for Details

let hoveredEntity = $state<Cesium.Entity | null>(null);
let selectedEntity = $state<Cesium.Entity | null>(null);

const popupOptions = {
	// Show popup on hover
	enableHover: true,

	onEntityHover: (context: EntityEventContext) => {
		// Show quick preview on hover
		hoveredEntity = context.entity;
		showQuickPreview(context.entity);
	},

	onEntityClick: (context: EntityEventContext) => {
		// Show detailed view on click
		selectedEntity = context.entity;
		openDetailedView(context.entity);
	}
};

8. Combining Callbacks with Layer Filtering

const popupOptions = {
	// Only show popups for specific layers
	includeDataSources: ['interactive-layer', /^data-.*/],

	// But track clicks on all entities
	onEntityClick: (context: EntityEventContext) => {
		// This callback fires even for excluded DataSources
		// if the entity is clicked (popup just won't show)
		logEntityInteraction(context);
	}
};

Important Notes

  • Callbacks are executed before the popup is displayed
  • Callbacks work independently of the showPopup option
  • Both synchronous and asynchronous (Promise-based) callbacks are supported
  • Errors in callbacks are automatically caught and logged to console
  • Callbacks receive context even when popup display is disabled by layer filters

Property Selection and Display Types

Property Selection with Whitelist Mode

You can control which properties are displayed in the popup and in what order by using the properties option:

<script lang="ts">
	import { EntityPopup } from 'cesium-properties-popup';
	import type { PropertyConfig } from 'cesium-properties-popup';

	// Simple string array - displays properties in the order specified
	const simpleWhitelist = ['name', 'description', 'value'];

	// Advanced configuration with display names and types
	const advancedWhitelist: PropertyConfig[] = [
		// Simple text property with custom display name
		{ name: 'name', displayName: 'Name' },

		// Hyperlink display
		{
			name: 'website',
			displayName: 'Website',
			displayType: 'link'
		},

		// Image display
		{
			name: 'imageUrl',
			displayName: 'Preview',
			displayType: 'image'
		},

		// Email link
		{
			name: 'contact',
			displayName: 'Email',
			displayType: 'email'
		}
	];

	const popupOptions = {
		properties: advancedWhitelist
	};
</script>

{#if viewerReady && viewer && cesium}
	<EntityPopup {viewer} {cesium} options={popupOptions} />
{/if}

Display Types

The library supports several display types for properties:

| Display Type | Description | Example | | ------------ | -------------------- | --------------------------- | | text | Default text display | Plain property value | | link | Clickable hyperlink | Opens in new tab/window | | image | Image display | Shows image with 100% width | | email | Email link | Creates mailto: link |

Example with Mixed Display Types

const propertyConfig: PropertyConfig[] = [
	// Regular text property
	{ name: 'stationName', displayName: 'Station' },

	// Hyperlink that opens in new tab
	{
		name: 'homepageUrl',
		displayName: 'Homepage',
		displayType: 'link'
	},

	// Image that displays at full width
	{
		name: 'photoUrl',
		displayName: 'Photo',
		displayType: 'image'
	},

	// Email address with mailto link
	{
		name: 'contactEmail',
		displayName: 'Contact',
		displayType: 'email'
	}
];

Layer-Specific Property Configuration

Overview

You can configure different property display settings for each layer (DataSource). This allows you to:

  • Display different properties for different layers
  • Insert static text content alongside dynamic properties
  • Use string (exact match) or regular expressions to identify layers
  • Provide default configuration for unspecified layers

Basic Layer-Specific Configuration

import { EntityPopup } from 'cesium-properties-popup';
import type { EntityPopupOptions } from 'cesium-properties-popup';

const options: EntityPopupOptions = {
	layerPropertyConfigs: [
		{
			layerPattern: 'buildings', // Exact match
			properties: ['name', 'height', 'floors']
		},
		{
			layerPattern: 'roads', // Exact match
			properties: ['name', 'type', 'lanes']
		}
	],
	// Default configuration for layers not specified above
	properties: ['name', 'description']
};

Using Static Text Content

You can insert fixed text content into the property list alongside dynamic properties:

const options: EntityPopupOptions = {
	layerPropertyConfigs: [
		{
			layerPattern: 'buildings',
			properties: [
				// Static text at the top
				{ type: 'static', label: 'Category', value: 'Building' },

				// Dynamic properties from entity
				'name',
				{ name: 'height', displayName: 'Height (m)' },
				'floors',

				// Static text with link
				{
					type: 'static',
					label: 'Department',
					value: 'Urban Planning',
					displayType: 'text'
				}
			]
		}
	]
};

Using Regular Expression Patterns

Use regular expressions to match multiple layers with a single pattern:

const options: EntityPopupOptions = {
	layerPropertyConfigs: [
		{
			layerPattern: /^sensor-/, // Layers starting with "sensor-"
			properties: [
				{ type: 'static', label: 'Data Type', value: 'Sensor Data' },
				{ name: 'id', displayName: 'Sensor ID' },
				{ name: 'temperature', displayName: 'Temperature' },
				{ name: 'humidity', displayName: 'Humidity' },
				{
					type: 'static',
					label: 'Support',
					value: 'https://example.com/support',
					displayType: 'link'
				}
			]
		},
		{
			layerPattern: /.*-draft$/, // Layers ending with "-draft"
			properties: [
				{ type: 'static', label: 'Status', value: 'Draft' },
				'name',
				'status',
				'lastModified'
			]
		}
	]
};

Priority and Fallback Behavior

The system uses the following priority order:

  1. Layer-specific config: If layerPropertyConfigs contains a matching pattern, use that configuration
  2. Default config: If no layer pattern matches, use the properties option
  3. All properties: If neither is specified, display all entity properties
const options: EntityPopupOptions = {
	// Specific configurations for certain layers
	layerPropertyConfigs: [
		{
			layerPattern: 'buildings',
			properties: ['name', 'height'] // Only these for buildings
		},
		{
			layerPattern: /^sensor-/,
			properties: ['id', 'value'] // Only these for sensor layers
		}
	],

	// Default for all other layers
	properties: ['name', 'description']

	// Note: If a layer matches neither layerPropertyConfigs nor properties,
	// all of its properties will be displayed
};

Combining with Layer Filters

Layer-specific property configs work seamlessly with includeDataSources and excludeDataSources:

const options: EntityPopupOptions = {
	// Control which layers show popups at all
	includeDataSources: ['buildings', 'roads', /^sensor-/],

	// Configure what each layer displays
	layerPropertyConfigs: [
		{
			layerPattern: 'buildings',
			properties: [
				{ type: 'static', label: 'Category', value: 'Building' },
				'name',
				{ name: 'height', displayName: 'Height (m)' }
			]
		},
		{
			layerPattern: 'roads',
			properties: ['name', 'type', 'lanes']
		},
		{
			layerPattern: /^sensor-/,
			properties: [{ type: 'static', label: 'Data Type', value: 'Sensor' }, 'id', 'value']
		}
	],

	// Default for other included layers (if any)
	properties: ['name']
};

Complete Example with Svelte

<script lang="ts">
	import { onMount } from 'svelte';
	import * as Cesium from 'cesium';
	import { EntityPopup } from 'cesium-properties-popup';
	import type { EntityPopupOptions } from 'cesium-properties-popup';

	let viewer: Cesium.Viewer | undefined = $state(undefined);
	let cesium = Cesium;

	const popupOptions: EntityPopupOptions = {
		layerPropertyConfigs: [
			{
				layerPattern: 'buildings',
				properties: [
					{ type: 'static', label: 'Category', value: 'Building' },
					'name',
					{ name: 'height', displayName: 'Height (m)' },
					'floors'
				]
			},
			{
				layerPattern: /^sensor-/,
				properties: [
					{ type: 'static', label: 'Data Type', value: 'Sensor Data' },
					{ name: 'id', displayName: 'Sensor ID' },
					{ name: 'value', displayName: 'Measured Value' }
				]
			}
		],
		// Default configuration
		properties: ['name', 'description']
	};

	onMount(() => {
		viewer = new Cesium.Viewer('cesiumContainer');

		// Add buildings layer
		const buildingsDataSource = new Cesium.CustomDataSource('buildings');
		buildingsDataSource.entities.add({
			name: 'Tokyo Tower',
			position: Cesium.Cartesian3.fromDegrees(139.7454, 35.6586, 0),
			properties: {
				height: 333,
				floors: 2
			},
			point: {
				pixelSize: 10,
				color: Cesium.Color.RED
			}
		});
		viewer.dataSources.add(buildingsDataSource);

		// Add sensor layer
		const sensorDataSource = new Cesium.CustomDataSource('sensor-temperature');
		sensorDataSource.entities.add({
			name: 'Temperature Sensor #1',
			position: Cesium.Cartesian3.fromDegrees(139.75, 35.66, 0),
			properties: {
				id: 'TEMP-001',
				value: '25.3°C'
			},
			point: {
				pixelSize: 10,
				color: Cesium.Color.BLUE
			}
		});
		viewer.dataSources.add(sensorDataSource);
	});
</script>

<div id="cesiumContainer" class="h-screen w-full"></div>

{#if viewer}
	<EntityPopup {viewer} {cesium} options={popupOptions} />
{/if}

Type Definitions for Layer Configuration

/**
 * Static text content configuration
 */
export interface StaticTextContent {
	type: 'static';
	label: string;
	value: string;
	displayType?: PropertyDisplayType;
}

/**
 * Property item type
 */
export type PropertyItem = PropertyConfig | string | StaticTextContent;

/**
 * Layer-specific property configuration
 */
export interface LayerPropertyConfig {
	layerPattern: DataSourcePattern; // string or RegExp
	properties: PropertyItem[];
}

/**
 * EntityPopup options (updated)
 */
export interface EntityPopupOptions {
	// ... other options ...

	/** Layer-specific property configurations */
	layerPropertyConfigs?: LayerPropertyConfig[];

	/** Default property configuration (fallback) */
	properties?: PropertyItem[];
}

Display Results

When you click or hover over entities from different layers:

Buildings layer:

┌────────────────────────────────┐
│ Category     │ Building        │
│ name         │ Tokyo Tower     │
│ Height (m)   │ 333             │
│ floors       │ 2               │
└────────────────────────────────┘

Sensor layer (matching /^sensor-/):

┌─────────────────────────────────────┐
│ Data Type    │ Sensor Data          │
│ Sensor ID    │ TEMP-001             │
│ Measured Value│ 25.3°C              │
└─────────────────────────────────────┘

Other layers (using default config):

┌────────────────────────────────┐
│ name         │ Example Name    │
│ description  │ Some desc...    │
└────────────────────────────────┘

Development Setup

If you encounter cache-related issues during development or debugging, you can clear the cache and rebuild using the following commands:

# Clear cache & rebuild
npm run rebuild

# Or execute individually
npm run clean   # Clear .svelte-kit and node_modules/.vite caches
npm run build   # Rebuild the library

Customization Options

<script lang="ts">
	import { onMount } from 'svelte';
	import { browser } from '$app/environment';
	import { EntityPopup } from 'cesium-properties-popup';
	import type * as CesiumType from 'cesium';

	let cesium: typeof CesiumType | undefined = $state();
	let viewer: CesiumType.Viewer | undefined = $state();
	let viewerReady = $state(false);

	// Popup options with custom settings
	const popupOptions = {
		// Enable/disable hover behavior (default: true)
		enableHover: true,

		// Property selection and configuration
		properties: [
			{ name: 'name', displayName: 'Name' },
			{ name: 'type', displayName: 'Type' },
			{ name: 'value', displayName: 'Value' }
		],

		// Style options
		styleOptions: {
			width: 400,
			height: 300,
			popupClass: 'my-custom-popup',
			backgroundColor: '#ffffff',
			overflowY: 'auto'
		}
	};

	onMount(async (): Promise<void> => {
		if (!browser) return;
		// Cesium initialization code (see Basic Usage example)
		// ...
	});
</script>

<!-- Container for rendering Cesium -->
<div id="cesiumContainer" class="h-full w-full"></div>

<!-- EntityPopup with custom options -->
{#if viewerReady && viewer && cesium}
	<EntityPopup {viewer} {cesium} options={popupOptions} />
{/if}

Options API

| Option | Type | Default | Description | | ---------------------- | ------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------- | | enableHover | boolean | true | Enable popup display on hover | | properties | PropertyItem[] | undefined | Default properties configuration (fallback when layer pattern doesn't match) | | layerPropertyConfigs | LayerPropertyConfig[] | undefined | Layer-specific property configurations (see Layer-Specific Configuration) | | excludeDataSources | DataSourcePattern[] | undefined | DataSource patterns to exclude from popup display (string or RegExp) | | includeDataSources | DataSourcePattern[] | undefined | DataSource patterns to include for popup display (whitelist) | | showPopup | boolean | true | Whether to automatically show popup (can be controlled independently of callbacks) | | onEntityClick | (context: EntityEventContext) => void | Promise<void> | undefined | Callback executed when an entity is clicked | | onEntityHover | (context: EntityEventContext) => void | Promise<void> | undefined | Callback executed when an entity is hovered | | onEmptyClick | (position: {x: number, y: number}) => void | Promise<void> | undefined | Callback executed when empty space is clicked | | styleOptions | object | {} | Style configuration for the popup |

Style Options

| Option | Type | Description | | ----------------- | ------ | -------------------------------------- | | width | number | Popup width in pixels | | height | number | Popup height in pixels | | popupClass | string | CSS class to apply to popup | | backgroundColor | string | Background color (CSS value) | | overflowY | string | Vertical overflow behavior (CSS value) |


## Styling

You can customize the appearance of the popup using CSS:

```html
<style>
	:global(.my-custom-popup) {
		border-radius: 8px;
		box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
		border-left-color: #ff6347 !important;
	}

	:global(.my-custom-popup h3) {
		color: #2c3e50;
		font-weight: bold;
	}

	:global(.my-custom-popup table) {
		border-collapse: separate;
		border-spacing: 0 2px;
	}
</style>

Component Architecture

This library consists of these main components:

  • EntityPopup: The main component that manages entity popups on a Cesium viewer.
  • PopupPositioner: A component that calculates and updates popup positions, automatically adjusting based on camera movements and entity position changes.
  • PopupContent: A component that displays the popup content, showing entity names, descriptions, and properties.
  • PropertyValue: A component that renders property values according to their display type.

Entity Position Strategy System

The library uses a Strategy Pattern to handle different entity types:

  • PointStrategy: Uses direct position for point entities
  • PolylineStrategy: Calculates midpoint for line entities
  • PolygonStrategy: Calculates center point using bounding sphere for polygon entities

Type Definitions

/**
 * Property display types
 */
export type PropertyDisplayType =
	| 'text' // Regular text (default)
	| 'link' // Hyperlink
	| 'image' // Image
	| 'email'; // Email address

/**
 * DataSource pattern type
 */
export type DataSourcePattern = string | RegExp;

/**
 * Entity event context for callbacks
 */
export interface EntityEventContext {
	/** The entity that triggered the event */
	entity: Cesium.Entity;
	/** Mouse position in screen coordinates */
	position: { x: number; y: number };
	/** Event type */
	eventType: 'click' | 'hover';
	/** DataSource name (if available) */
	dataSourceName?: string;
}

/**
 * Property configuration
 */
export interface PropertyConfig {
	/** Property name */
	name: string;
	/** Display name (optional) */
	displayName?: string;
	/** Display type */
	displayType?: PropertyDisplayType;
}

/**
 * Static text content configuration
 */
export interface StaticTextContent {
	/** Content type identifier */
	type: 'static';
	/** Label to display (left side) */
	label: string;
	/** Value to display (right side) */
	value: string;
	/** Optional: display type for the value */
	displayType?: PropertyDisplayType;
}

/**
 * Property item type
 */
export type PropertyItem = PropertyConfig | string | StaticTextContent;

/**
 * Layer-specific property configuration
 */
export interface LayerPropertyConfig {
	/** Target layer pattern (DataSource name) */
	layerPattern: DataSourcePattern;
	/** Properties to display for this layer */
	properties: PropertyItem[];
}

/**
 * EntityPopup component options
 */
export interface EntityPopupOptions {
	/** Whether to show popup on hover */
	enableHover?: boolean;
	/** Default properties configuration (fallback) */
	properties?: PropertyItem[];
	/** Layer-specific property configurations */
	layerPropertyConfigs?: LayerPropertyConfig[];
	/** DataSource patterns to exclude from popup display (string for exact match, RegExp for pattern matching) */
	excludeDataSources?: DataSourcePattern[];
	/** DataSource patterns to include for popup display (whitelist) */
	includeDataSources?: DataSourcePattern[];
	/** Whether to automatically show popup (default: true) */
	showPopup?: boolean;
	/** Callback when entity is clicked */
	onEntityClick?: (context: EntityEventContext) => void | Promise<void>;
	/** Callback when entity is hovered */
	onEntityHover?: (context: EntityEventContext) => void | Promise<void>;
	/** Callback when empty space is clicked */
	onEmptyClick?: (position: { x: number; y: number }) => void | Promise<void>;
	/** Popup CSS settings */
	styleOptions?: {
		/** Popup width in pixels */
		width?: number;
		/** Popup height in pixels */
		height?: number;
		/** CSS class for the popup */
		popupClass?: string;
		/** Background color (CSS value) */
		backgroundColor?: string;
		/** Vertical overflow behavior (CSS value) */
		overflowY?: string;
	};
}

Advanced Usage

You can use individual components as needed for more custom implementations:

<script lang="ts">
	import { onMount } from 'svelte';
	import { browser } from '$app/environment';
	import { PopupPositioner, PopupContent } from 'cesium-properties-popup';
	import type * as CesiumType from 'cesium';

	let cesium: typeof CesiumType | undefined = $state();
	let viewer: CesiumType.Viewer | undefined = $state();
	let selectedEntity: CesiumType.Entity | undefined = $state();
	let isPopupOpen = $state(false);

	onMount(async (): Promise<void> => {
		if (!browser) return;

		try {
			// Import Cesium module
			cesium = (await import('cesium')) as typeof CesiumType;
			await import('cesium/Build/Cesium/Widgets/widgets.css');

			const { Viewer: CesiumViewer, ScreenSpaceEventHandler, ScreenSpaceEventType } = cesium;

			// Initialize viewer
			viewer = new CesiumViewer('cesiumContainer');

			// Custom event handler setup
			const handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
			handler.setInputAction((click) => {
				const pickedObject = viewer.scene.pick(click.position);
				if (cesium.defined(pickedObject) && pickedObject.id instanceof cesium.Entity) {
					handleEntitySelect(pickedObject.id);
				}
			}, ScreenSpaceEventType.LEFT_CLICK);
		} catch (error) {
			console.error('Failed to initialize Cesium:', error);
		}
	});

	// Custom entity selection logic
	function handleEntitySelect(entity: CesiumType.Entity) {
		selectedEntity = entity;
		isPopupOpen = true;
	}
</script>

<div id="cesiumContainer" class="h-full w-full"></div>

{#if isPopupOpen && selectedEntity && viewer && cesium}
	<PopupPositioner {viewer} {cesium} entity={selectedEntity} {isPopupOpen}>
		<div class="custom-popup">
			<PopupContent entity={selectedEntity} {cesium} />
			<div class="actions">
				<button onclick={() => (isPopupOpen = false)}>Close</button>
			</div>
		</div>
	</PopupPositioner>
{/if}

Programmatic Control

You can programmatically control the popup using the component's public API with bind:this:

<script lang="ts">
	import { EntityPopup } from 'cesium-properties-popup';
	import type { EntityPopupAPI } from 'cesium-properties-popup';
	import type * as Cesium from 'cesium';

	let viewer: Cesium.Viewer;
	let cesium: typeof Cesium;
	let popupApi: EntityPopupAPI;

	// Function to programmatically close the popup
	function closePopup() {
		popupApi.close();
	}

	// Function to open popup for a specific entity
	function openPopupForEntity(entity: Cesium.Entity) {
		popupApi.open(entity);
	}

	// Check if popup is currently open
	function checkPopupStatus() {
		const isOpen = popupApi.isOpen();
		const selectedEntity = popupApi.getSelectedEntity();
		console.log('Popup open:', isOpen);
		console.log('Selected entity:', selectedEntity);
	}
</script>

<!-- Bind the component instance to popupApi -->
<EntityPopup bind:this={popupApi} {viewer} {cesium} />

<button onclick={closePopup}>Close Popup</button>

API Reference

The EntityPopupAPI interface provides the following methods:

| Method | Parameters | Returns | Description | | --------------------- | ----------------------- | ---------------------------- | ------------------------------------------------------------- | | close() | None | void | Closes the popup if it's currently open | | open(entity) | entity: Cesium.Entity | void | Opens the popup for the specified entity | | isOpen() | None | boolean | Returns true if the popup is currently open | | getSelectedEntity() | None | Cesium.Entity \| undefined | Returns the currently selected entity, or undefined if none |

Practical Examples

Close Popup After Custom Action

const popupOptions = {
	onEntityClick: (context: EntityEventContext) => {
		// Perform some action
		console.log('Entity clicked:', context.entity.id);

		// Close popup after 2 seconds
		setTimeout(() => {
			popupApi.close();
		}, 2000);
	}
};

Toggle Popup Programmatically

function togglePopup(entity: Cesium.Entity) {
	if (popupApi.isOpen() && popupApi.getSelectedEntity()?.id === entity.id) {
		popupApi.close();
	} else {
		popupApi.open(entity);
	}
}

External UI Integration

<script lang="ts">
	import { EntityPopup } from 'cesium-properties-popup';
	import type { EntityPopupAPI } from 'cesium-properties-popup';

	let popupApi: EntityPopupAPI;
	let customPanelOpen = $state(false);

	const options = {
		// Disable automatic popup display
		showPopup: false,

		onEntityClick: (context) => {
			// Use your own custom UI instead
			customPanelOpen = true;
			// But still track it internally
			popupApi.open(context.entity);
		}
	};
</script>

<EntityPopup bind:this={popupApi} {viewer} {cesium} {options} />

{#if customPanelOpen}
	<CustomPanel entity={popupApi.getSelectedEntity()} />
{/if}

Utility Functions

You can directly use the utility functions provided by the library:

import {
	formatPropertyValue,
	getPropertyEntries,
	getEntityPosition,
	worldPositionToScreenPosition,
	calculatePopupPosition,
	getDataSourceName,
	getApplicablePropertyConfig,
	matchesPattern,
	defaultSettings
} from 'cesium-properties-popup';

// Format entity property value
const formattedValue = formatPropertyValue(entity.properties.myProperty, cesium);

// Get property entries from an entity for display
const propertyEntries = getPropertyEntries(entity);

// Get entity's 3D position
const position = getEntityPosition(entity, cesium);

// Convert 3D coordinates to screen coordinates
const screenPosition = worldPositionToScreenPosition(position, viewer, cesium);

// Comprehensive function to calculate popup position
const popupPosition = await calculatePopupPosition(entity, viewer, cesium, currentPosition);

// Layer-specific helper functions
const dataSourceName = getDataSourceName(entity); // Get DataSource name from entity
const isMatch = matchesPattern('sensor-01', /^sensor-/); // Check if pattern matches
const applicableConfig = getApplicablePropertyConfig(entity, layerConfigs, defaultProps); // Get applicable property config

// Change default settings (for performance tuning, etc.)
defaultSettings.updateFrequency.cameraChangeThrottle = 150; // Change camera change throttling interval to 150ms

Implementation Guidelines and Best Practices

Working with GeoJSON Data

When loading GeoJSON data with custom properties:

// Load GeoJSON with custom properties
const dataSource = await Cesium.GeoJsonDataSource.load('/data/locations.geojson');
viewer.dataSources.add(dataSource);

// Configure popup to display specific properties
const popupOptions = {
	properties: [
		{ name: 'name', displayName: 'Location Name' },
		{ name: 'website', displayName: 'Website', displayType: 'link' },
		{ name: 'photo', displayName: 'Photo', displayType: 'image' },
		{ name: 'email', displayName: 'Contact', displayType: 'email' }
	]
};

Performance Optimization

  1. When dealing with many entities

    // Use property whitelist to limit displayed properties
    const popupOptions = {
    	properties: ['name', 'type', 'value', 'category']
    };
  2. For entities with complex properties

    // Display only essential properties
    const popupOptions = {
    	properties: [
    		{ name: 'id', displayName: 'ID' },
    		{ name: 'status', displayName: 'Status' },
    		{ name: 'lastUpdated', displayName: 'Last Updated' }
    	]
    };

Visual Customization

  1. Theme-specific styling

    <script lang="ts">
    	import { onMount } from 'svelte';
    	import { browser } from '$app/environment';
    	import { EntityPopup } from 'cesium-properties-popup';
    	import type * as CesiumType from 'cesium';
    
    	let cesium: typeof CesiumType | undefined = $state();
    	let viewer: CesiumType.Viewer | undefined = $state();
    	let viewerReady = $state(false);
    
    	const popupOptions = {
    		styleOptions: {
    			popupClass: 'brand-theme'
    		}
    	};
    
    	onMount(async (): Promise<void> => {
    		// Cesium initialization (see Basic Usage example)
    		// ...
    	});
    </script>
    
    {#if viewerReady && viewer && cesium}
    	<EntityPopup {viewer} {cesium} options={popupOptions} />
    {/if}
    
    <style>
    	:global(.brand-theme) {
    		border-left: 4px solid #3498db;
    		border-radius: 0;
    		font-family: 'Roboto', sans-serif;
    	}
    
    	:global(.brand-theme h3) {
    		background-color: #f5f5f5;
    		padding: 12px;
    		margin: 0;
    	}
    </style>
  2. Situational style changes

    <script lang="ts">
    	import { onMount } from 'svelte';
    	import { browser } from '$app/environment';
    	import { EntityPopup } from 'cesium-properties-popup';
    	import type * as CesiumType from 'cesium';
    
    	let cesium: typeof CesiumType | undefined = $state();
    	let viewer: CesiumType.Viewer | undefined = $state();
    	let viewerReady = $state(false);
    	let theme = $state('light');
    
    	function toggleTheme() {
    		theme = theme === 'light' ? 'dark' : 'light';
    	}
    
    	// Reactive popup options based on theme
    	const popupOptions = $derived({
    		styleOptions: {
    			popupClass: theme === 'light' ? 'light-theme' : 'dark-theme'
    		}
    	});
    
    	onMount(async (): Promise<void> => {
    		// Cesium initialization (see Basic Usage example)
    		// ...
    	});
    </script>
    
    <button onclick={toggleTheme}>Toggle Theme</button>
    
    {#if viewerReady && viewer && cesium}
    	<EntityPopup {viewer} {cesium} options={popupOptions} />
    {/if}
    
    <style>
    	:global(.light-theme) {
    		background: white;
    		color: #333;
    	}
    
    	:global(.dark-theme) {
    		background: #333;
    		color: white;
    	}
    </style>

Use-case Specific Tips

  1. Real estate data display

    const popupOptions = {
    	properties: [
    		{ name: 'address', displayName: 'Address' },
    		{ name: 'price', displayName: 'Price' },
    		{ name: 'area', displayName: 'Area (m²)' },
    		{ name: 'listingUrl', displayName: 'View Listing', displayType: 'link' },
    		{ name: 'photoUrl', displayName: 'Photo', displayType: 'image' }
    	]
    };
  2. Infrastructure monitoring

    const popupOptions = {
    	properties: [
    		{ name: 'stationId', displayName: 'Station ID' },
    		{ name: 'status', displayName: 'Status' },
    		{ name: 'lastInspection', displayName: 'Last Inspection' },
    		{ name: 'manualUrl', displayName: 'Manual', displayType: 'link' },
    		{ name: 'supportEmail', displayName: 'Support', displayType: 'email' }
    	]
    };
  3. Adding data attributions

    <script lang="ts">
    	import { onMount } from 'svelte';
    	import type * as CesiumType from 'cesium';
    
    	let cesium: typeof CesiumType | undefined = $state();
    	let viewer: CesiumType.Viewer | undefined = $state();
    
    	function addDataAttributions(): void {
    		if (!viewer || !cesium) return;
    
    		const { Credit } = cesium;
    
    		const credits = [
    			{
    				text: '<a href="https://example.com/data-source" target="_blank">Data Source Name</a>',
    				showOnScreen: true
    			}
    		];
    
    		credits.forEach((credit) => {
    			viewer.creditDisplay.addStaticCredit(new Credit(credit.text, credit.showOnScreen));
    		});
    	}
    
    	onMount(async (): Promise<void> => {
    		// After viewer initialization
    		addDataAttributions();
    	});
    </script>

License

MIT