cesium-properties-popup
v0.2.1
Published
A Svelte component for displaying entity properties on hover or click in Cesium
Readme
Cesium Properties Popup
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
- Basic Usage
- Supported Entity Types
- Layer Control
- Event Callbacks
- Property Selection and Display Types
- Layer-Specific Property Configuration
- Customization Options
- Styling
- Component Architecture
- Type Definitions
- Advanced Usage
- Programmatic Control
- Utility Functions
- Implementation Guidelines
- License
Installation
# npm
npm install cesium-properties-popup
# pnpm
pnpm add cesium-properties-popup
# yarn
yarn add cesium-properties-popupThis library has peer dependencies on cesium and svelte:
# Required peer dependencies
npm install cesium svelteBasic 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 popupsPriority Rules
When using both settings:
includeDataSources(whitelist) - If specified, only DataSources included will show popupsexcludeDataSources(blacklist) - Applied when includeDataSources is not specified
Important Notes
- DataSource
nameproperty 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
String patterns perform exact matching
'layer-name'; // Matches only "layer-name" exactlyRegExp patterns use JavaScript regular expressions
/^prefix-/ // Matches any name starting with "prefix-" /.*-suffix$/ // Matches any name ending with "-suffix" /pattern/i // Case-insensitive matchingMixed 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
showPopupoption - 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:
- Layer-specific config: If
layerPropertyConfigscontains a matching pattern, use that configuration - Default config: If no layer pattern matches, use the
propertiesoption - 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 libraryCustomization 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 entitiesPolylineStrategy: Calculates midpoint for line entitiesPolygonStrategy: 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 150msImplementation 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
When dealing with many entities
// Use property whitelist to limit displayed properties const popupOptions = { properties: ['name', 'type', 'value', 'category'] };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
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>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
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' } ] };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' } ] };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
