# @edgepdf/viewer-js

A powerful, self-contained JavaScript library for viewing large PDF documents in the browser. Provides smooth navigation, zoom controls, and interactive marker management without requiring any external dependencies.

## Installation

```bash
npm install @edgepdf/viewer-js
# or
pnpm add @edgepdf/viewer-js
# or
yarn add @edgepdf/viewer-js
```

**Note:** All dependencies are bundled with this library. **CSS styles are automatically injected** when you import the library - no manual CSS import needed!

## Features

- ⚡ **Zero Configuration**: Works out of the box with sensible defaults
- 📦 **Self-Contained**: All dependencies bundled - no external CSS or scripts needed
- 🎯 **Precise Control**: Programmatic zoom, pan, and focus capabilities
- 📍 **Rich Markers**: Support for draggable, editable markers with tooltips and custom icons
- 🔄 **Data Management**: Import/export marker data for persistence
- 🎨 **Customizable**: Flexible API for styling and behavior customization
- 📱 **Touch-Friendly**: Full support for touch gestures on mobile devices
- ♿ **Accessible**: Keyboard navigation and ARIA labels included

## Quick Start

### Basic Example

```typescript
import { EdgePdfViewer } from '@edgepdf/viewer-js';
import type { ViewerConfig } from '@edgepdf/types';

// Get container
const container = document.getElementById('map-container');

// Configure viewer
const config: ViewerConfig = {
  tileUrl: 'https://example.com/tiles/{z}/{x}/{y}.png',
  imageInfo: {
    width: 2000,
    height: 3000,
    tileSize: 256,
    maxZoom: 5,
    minZoom: 0,
  },
};

// Create and initialize viewer
const viewer = new EdgePdfViewer({ container, config });
viewer.initialize();

// Access marker manager
const markerManager = viewer.getMarkerManager();
if (markerManager) {
  // Create a marker
  const marker = markerManager.createMarker({
    position: [1500, 1000], // [y, x] coordinates
    label: 'My Marker',
  });
}

// Cleanup when done
viewer.dispose();
```

### With Custom Map Options

```typescript
import { EdgePdfViewer } from '@edgepdf/viewer-js';
import type { ViewerConfig, MapOptions } from '@edgepdf/types';

const config: ViewerConfig = {
  tileUrl: 'https://example.com/tiles/{z}/{x}/{y}.png',
  imageInfo: {
    width: 2000,
    height: 3000,
    tileSize: 256,
    maxZoom: 5,
    minZoom: 0,
  },
};

const mapOptions: MapOptions = {
  center: [1500, 1000], // [y, x] in pixel coordinates
  zoom: 2,
  minZoom: 0,
  maxZoom: 5,
};

const viewer = new EdgePdfViewer({
  container: document.getElementById('map-container'),
  config,
  mapOptions,
});

viewer.initialize();
```

## API Reference

### EdgePdfViewer

Main class for initializing and managing interactive map instances for PDF viewing.

#### Constructor

```typescript
new EdgePdfViewer(options: {
  container: HTMLElement;
  config: ViewerConfig;
  mapOptions?: MapOptions;
})
```

**Parameters:**

- `container` - HTML element that will contain the map
- `config` - Viewer configuration with tile URL and image info
- `mapOptions` - Optional map options (center, zoom, etc.)

**Throws:**

- `Error` if container is not provided or invalid
- `Error` if config is not provided or invalid
- `Error` if tileUrl is missing
- `Error` if imageInfo is missing

#### Methods

##### `initialize(): void`

Initializes the viewer and prepares it for displaying the PDF document. Call this method after creating the viewer instance.

**Throws:**

- `Error` if initialization fails

##### `getMap(): L.Map | null`

Gets the map instance.

**Returns:** The map instance, or `null` if not initialized

##### `isInitialized(): boolean`

Checks if the map is initialized.

**Returns:** `true` if map is initialized, `false` otherwise

##### `getTileLayerManager(): TileLayerManager | null`

Gets the tile layer manager instance.

**Returns:** The tile layer manager, or `null` if not created

##### `getCoordinateMapper(): CoordinateMapper | null`

Gets the coordinate mapper instance.

**Returns:** The coordinate mapper, or `null` if not created

##### `getZoomController(): ZoomController | null`

Gets the zoom controller instance.

**Returns:** The zoom controller, or `null` if not created

##### `getMarkerManager(): MarkerManager | null`

Gets the marker manager instance.

**Returns:** The marker manager, or `null` if not created

##### `focusMarker(markerOrId: string | Marker, options?: FocusOptions): boolean`

Focuses on a marker by panning and zooming to its position.

**Parameters:**

- `markerOrId` - Marker ID string or Marker object
- `options` - Optional focus options
  - `zoom?: number` - Target zoom level (uses marker's zoom or default if not provided)
  - `animate?: boolean` - Whether to animate the transition (default: true)
  - `duration?: number` - Animation duration in seconds (default: 0.5)
  - `offsetLeft?: number` - Pixel offset to move focus marker to the left (default: 0)
  - `offsetRight?: number` - Pixel offset to move focus marker to the right (default: 0)
  - `offsetTop?: number` - Pixel offset to move focus marker upward (default: 0)
  - `offsetBottom?: number` - Pixel offset to move focus marker downward (default: 0)

**Returns:** `true` if focus was successful, `false` if marker not found or viewer not initialized

**Example:**

```typescript
// Focus with offset to account for overlay
viewer.focusMarker('marker-123', { offsetLeft: 100, offsetTop: 50 });
```

##### `dispose(): void`

Disposes of the map instance and cleans up resources. This should be called when the viewer is no longer needed to prevent memory leaks and event listener issues.

---

### MarkerManager

Manages markers on the interactive map. Access via `viewer.getMarkerManager()`.

#### Methods

##### `createMarker(options: CreateMarkerOptions): Marker`

Creates a new marker from coordinates.

**Parameters:**

- `options.position?: ViewerCoords` - Map coordinates [y, x]
- `options.imageCoords?: ImageCoords` - Image pixel coordinates {x, y}
- `options.label?: string` - Marker label/tooltip text
- `options.description?: string` - Marker description
- `options.href?: string` - Link URL (optional)
- `options.target?: string` - Link target (optional)
- `options.showLabel?: boolean` - Show label/tooltip
- `options.id?: string` - Custom marker ID (auto-generated if not provided)
- `options.iconType?: 'pin-gray' | 'pin-yellow'` - Icon type for the marker
- `options.referenceId?: string` - Reference ID for linking to external systems
- `options.draggable?: boolean` - Enable dragging for this specific marker
- `options.editable?: boolean` - Enable editing for this specific marker
- `options.deletable?: boolean` - Enable deleting for this specific marker

**Returns:** The created marker

**Throws:**

- `Error` if neither position nor imageCoords is provided
- `Error` if coordinates are invalid or out of bounds

##### `getMarker(id: string): Marker | null`

Gets a marker by ID.

**Returns:** The marker, or `null` if not found

##### `getAllMarkers(): Marker[]`

Gets all markers.

**Returns:** Array of all markers

##### `getMarkerCount(): number`

Gets the total number of markers.

**Returns:** Number of markers

##### `hasMarker(id: string): boolean`

Checks if a marker exists.

**Returns:** `true` if marker exists, `false` otherwise

##### `updateMarker(id: string, updates: Partial<Marker>): boolean`

Updates a marker's properties.

**Parameters:**

- `id` - Marker ID
- `updates` - Partial marker object with properties to update

**Returns:** `true` if marker was updated, `false` if not found

##### `updateMarkerPosition(id: string, position: ViewerCoords): boolean`

Updates a marker's position.

**Parameters:**

- `id` - Marker ID
- `position` - New map coordinates [y, x]

**Returns:** `true` if marker was updated, `false` if not found

##### `updateMarkerIcon(id: string, iconType: 'pin-gray' | 'pin-yellow'): boolean`

Updates the icon type for a specific marker.

**Returns:** `true` if marker was updated, `false` if not found

##### `updateAllMarkerIcons(iconType: 'pin-gray' | 'pin-yellow'): void`

Updates all markers to use a new icon type.

##### `removeMarker(id: string): boolean`

Removes a marker by ID.

**Returns:** `true` if marker was removed, `false` if not found

##### `removeAllMarkers(): void`

Removes all markers.

##### `deleteMarker(id: string): boolean`

Deletes a marker by ID (triggers delete event).

**Returns:** `true` if marker was deleted, `false` if not found

##### `exportMarkers(): MarkerData`

Exports all markers as JSON data.

**Returns:** MarkerData object containing all markers and metadata

##### `importMarkers(data: MarkerData, options?: ImportOptions): ImportResult`

Imports markers from MarkerData.

**Parameters:**

- `data` - MarkerData object to import
- `options` - Import options
  - `clearExisting?: boolean` - If true, removes all existing markers before import (default: false)
  - `validateCoordinates?: boolean` - If true, validates coordinates are within bounds (default: true)

**Returns:** Import result with success status and details

##### `setInteractionConfig(config: Partial<MarkerInteractionConfig>): void`

Sets marker interaction configuration.

**Parameters:**

- `config.draggable?: boolean` - Enable dragging markers
- `config.selectable?: boolean` - Enable selecting markers
- `config.showTooltips?: boolean` - Show tooltips on hover
- `config.showPopups?: boolean` - Show popups on click
- `config.multiSelect?: boolean` - Allow multiple marker selection
- `config.showEditButton?: boolean` - Show edit button in popups
- `config.showDeleteButton?: boolean` - Show delete button in popups
- `config.onEdit?: (marker: Marker) => Promise<Marker | null>` - Custom edit handler
- `config.onDelete?: (marker: Marker) => Promise<boolean>` - Custom delete handler
- `config.onActiveMarkerChange?: (marker: Marker | null) => void` - Active marker change callback

##### `getInteractionConfig(): MarkerInteractionConfig`

Gets the current interaction configuration.

**Returns:** Current interaction configuration

##### `selectMarker(id: string): boolean`

Selects a marker.

**Returns:** `true` if marker was selected, `false` if not found

##### `deselectMarker(id: string): boolean`

Deselects a marker.

**Returns:** `true` if marker was deselected, `false` if not found

##### `deselectAllMarkers(): void`

Deselects all markers.

##### `getSelectionState(): MarkerSelectionState`

Gets the current selection state.

**Returns:** Selection state object with selected IDs and count

##### `isMarkerSelected(id: string): boolean`

Checks if a marker is selected.

**Returns:** `true` if marker is selected, `false` otherwise

##### `setActiveMarker(id: string | null): boolean`

Sets the active marker (for popup management).

**Returns:** `true` if active marker was set, `false` if marker not found

##### `getActiveMarker(): Marker | null`

Gets the currently active marker.

**Returns:** The active marker, or `null` if no marker is active

##### `focusMarker(markerOrId: string | Marker, options?: FocusOptions): boolean`

Focuses on a marker by panning and zooming to its position.

**Parameters:**

- `markerOrId` - Marker ID string or Marker object
- `options` - Optional focus options
  - `zoom?: number` - Target zoom level
  - `animate?: boolean` - Whether to animate (default: true)
  - `duration?: number` - Animation duration in seconds (default: 0.5)
  - `offsetLeft?: number` - Pixel offset to move focus marker to the left (default: 0)
  - `offsetRight?: number` - Pixel offset to move focus marker to the right (default: 0)
  - `offsetTop?: number` - Pixel offset to move focus marker upward (default: 0)
  - `offsetBottom?: number` - Pixel offset to move focus marker downward (default: 0)

**Returns:** `true` if focus was successful, `false` if marker not found

**Example:**

```typescript
// Focus with offset to account for overlay
markerManager.focusMarker('marker-123', { offsetLeft: 100, offsetTop: 50 });
```

##### `setDefaultIconType(iconType: 'pin-gray' | 'pin-yellow'): void`

Sets the default icon type for new markers.

##### `getDefaultIconType(): 'pin-gray' | 'pin-yellow'`

Gets the current default icon type.

**Returns:** Current default icon type

##### `setIconBasePath(basePath: string): void`

Sets the base path for marker icons.

**Example:**

```typescript
// Use library's built-in icons (default)
markerManager.setIconBasePath('./images/');

// Use custom icons from public folder
markerManager.setIconBasePath('/');

// Use icons from a CDN
markerManager.setIconBasePath('https://cdn.example.com/icons/');
```

##### `getIconBasePath(): string`

Gets the current base path for marker icons.

**Returns:** The current icon base path

##### `on(eventType: MarkerEventType, callback: (event: MarkerEvent) => void): () => void`

Registers an event listener.

**Parameters:**

- `eventType` - Event type: 'click' | 'dragstart' | 'drag' | 'dragend' | 'edit' | 'delete'
- `callback` - Callback function

**Returns:** Unsubscribe function

**Example:**

```typescript
const unsubscribe = markerManager.on('click', (event) => {
  console.log('Marker clicked:', event.marker.id);
});

// Later, to unsubscribe:
unsubscribe();
```

##### `off(eventType: MarkerEventType, callback: (event: MarkerEvent) => void): void`

Unregisters an event listener.

##### `removeAllListeners(eventType?: MarkerEventType): void`

Removes all event listeners for a specific event type, or all events if no type is provided.

##### `dispose(): void`

Disposes of the marker manager and cleans up resources.

---

### ZoomController

Manages zoom state and operations. Access via `viewer.getZoomController()`.

#### Methods

##### `zoomIn(options?: { animate?: boolean }): boolean`

Zooms in by one level.

**Parameters:**

- `options.animate?: boolean` - Whether to animate the zoom (default: true)

**Returns:** `true` if zoomed in, `false` if already at max zoom

##### `zoomOut(options?: { animate?: boolean }): boolean`

Zooms out by one level.

**Parameters:**

- `options.animate?: boolean` - Whether to animate the zoom (default: true)

**Returns:** `true` if zoomed out, `false` if already at min zoom

##### `setZoom(zoom: number, options?: { animate?: boolean }): void`

Sets a specific zoom level.

**Parameters:**

- `zoom` - Target zoom level
- `options.animate?: boolean` - Whether to animate the zoom (default: true)

**Throws:**

- `Error` if zoom is not a finite number

##### `getZoom(): number`

Gets the current zoom level.

**Returns:** Current zoom level

##### `getZoomState(): ZoomState`

Gets the current zoom state.

**Returns:** Zoom state object with currentZoom, minZoom, maxZoom

##### `getMinZoom(): number`

Gets the minimum zoom level.

**Returns:** Minimum zoom level

##### `getMaxZoom(): number`

Gets the maximum zoom level.

**Returns:** Maximum zoom level

##### `canZoomIn(): boolean`

Checks if can zoom in.

**Returns:** `true` if can zoom in, `false` otherwise

##### `canZoomOut(): boolean`

Checks if can zoom out.

**Returns:** `true` if can zoom out, `false` otherwise

##### `onZoomChange(listener: (state: ZoomState) => void): () => void`

Registers a zoom change listener.

**Returns:** Unsubscribe function

##### `removeAllListeners(): void`

Removes all zoom change listeners.

##### `dispose(): void`

Disposes of the zoom controller and cleans up resources.

---

### Utility Functions

#### `createZoomControls(container: HTMLElement, zoomController: ZoomController, options?: ZoomControlsOptions): HTMLElement`

Creates zoom control buttons (zoom in/out) in the specified container.

**Parameters:**

- `container` - Container element
- `zoomController` - Zoom controller instance
- `options` - Optional configuration
  - `position?: ZoomControlsPosition` - Position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' (default: 'top-right')

**Returns:** The created controls element

---

## Configuration

### ViewerConfig

```typescript
interface ViewerConfig {
  tileUrl: string; // Tile URL template with {z}, {x}, {y} placeholders
  imageInfo: ImageInfo; // Image metadata
  onPinsUpdate?: (pins: Marker[]) => void; // Optional callback for marker updates
  originalImagePath?: string; // Optional original image path
}
```

### ImageInfo

```typescript
interface ImageInfo {
  width: number; // Original image width in pixels
  height: number; // Original image height in pixels
  tileSize: number; // Tile size in pixels (default: 256)
  maxZoom: number; // Maximum zoom level
  minZoom: number; // Minimum zoom level
}
```

### MapOptions

```typescript
interface MapOptions {
  center?: [number, number]; // Initial center coordinates [y, x]
  zoom?: number; // Initial zoom level
  minZoom?: number; // Minimum zoom level
  maxZoom?: number; // Maximum zoom level
  bounds?: {
    southwest: [number, number];
    northeast: [number, number];
  };
}
```

## Marker Icons

The library includes built-in pin icons that are automatically available in the `dist/images/` folder:

- `pin.png` - Default pin icon
- `pin-gray.png` - Gray pin icon
- `pin-yellow.png` - Yellow pin icon
- `pin-gray-selected.png` - Gray pin (selected state)
- `pin-yellow-selected.png` - Yellow pin (selected state)

### Using Built-in Icons

By default, the `MarkerManager` uses `./images/` as the icon base path. To use the built-in icons:

```typescript
const viewer = new EdgePdfViewer({ container, config });
viewer.initialize();

const markerManager = viewer.getMarkerManager();
// Icons are automatically loaded from the library's dist/images/ folder
// No configuration needed if serving from the library location
```

### Customizing Icons

To use custom icons, copy the icon files from `node_modules/@edgepdf/viewer-js/dist/images/` to your public folder, customize them, and set the icon base path:

```typescript
const markerManager = viewer.getMarkerManager();

// Option 1: Serve from your public folder (recommended for customization)
markerManager.setIconBasePath('/'); // Icons in public/ folder

// Option 2: Use a CDN
markerManager.setIconBasePath('https://cdn.example.com/icons/');

// Option 3: Use relative path
markerManager.setIconBasePath('./assets/icons/');
```

**Note:** Icon files must be named exactly as listed above (e.g., `pin.png`, `pin-gray.png`, etc.) for the library to find them.

### Available Icon Types

```typescript
type IconType = 'pin-gray' | 'pin-yellow';
```

## Coordinate System

The viewer uses a simple pixel-based coordinate system for positioning markers:

- **Format**: `[y, x]` - array with vertical position first, horizontal position second
- **Range**: X from 0 to image width, Y from 0 to image height
- **Origin**: Top-left corner at `[0, 0]`

**Note**: You can also use image pixel coordinates `{x, y}` when creating markers - the viewer will automatically convert them to the appropriate format.

## Examples

### Working with Markers

```typescript
const viewer = new EdgePdfViewer({ container, config });
viewer.initialize();

const markerManager = viewer.getMarkerManager();
if (!markerManager) return;

// Create a marker
const marker = markerManager.createMarker({
  position: [1500, 1000],
  label: 'My Marker',
  description: 'This is a marker description',
  iconType: 'pin-yellow',
});

// Update marker
markerManager.updateMarker(marker.id, {
  label: 'Updated Label',
  description: 'Updated description',
});

// Listen to marker events
markerManager.on('click', (event) => {
  console.log('Marker clicked:', event.marker);
});

markerManager.on('dragend', (event) => {
  console.log('Marker dragged to:', event.marker.position);
});

// Export markers
const markerData = markerManager.exportMarkers();
console.log(JSON.stringify(markerData, null, 2));

// Import markers
markerManager.importMarkers(markerData, {
  clearExisting: true,
  validateCoordinates: true,
});
```

### Working with Zoom

```typescript
const viewer = new EdgePdfViewer({ container, config });
viewer.initialize();

const zoomController = viewer.getZoomController();
if (!zoomController) return;

// Zoom in
zoomController.zoomIn();

// Zoom out
zoomController.zoomOut();

// Set specific zoom level
zoomController.setZoom(3);

// Get zoom state
const zoomState = zoomController.getZoomState();
console.log('Current zoom:', zoomState.currentZoom);
console.log('Min zoom:', zoomState.minZoom);
console.log('Max zoom:', zoomState.maxZoom);

// Listen to zoom changes
zoomController.onZoomChange((state) => {
  console.log('Zoom changed to:', state.currentZoom);
});
```
