diff --git a/.github/workflows/trigger-website.yml b/.github/workflows/trigger-website.yml new file mode 100644 index 00000000..5208fb7e --- /dev/null +++ b/.github/workflows/trigger-website.yml @@ -0,0 +1,21 @@ +name: Trigger Website Build + +on: + release: + types: [published] + +jobs: + trigger: + runs-on: ubuntu-latest + steps: + - name: Trigger ODINv2.Website build + # NOTE: Cross-repo dispatch requires a Personal Access Token (PAT) with + # "repo" scope stored as a repository secret named WEBSITE_DISPATCH_TOKEN. + # The default GITHUB_TOKEN does not have permission to dispatch events + # to other repositories. + run: | + curl -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token ${{ secrets.WEBSITE_DISPATCH_TOKEN }}" \ + https://api.github.com/repos/syncpoint/ODINv2.Website/dispatches \ + -d '{"event_type":"release-published","client_payload":{"tag":"${{ github.event.release.tag_name }}"}}' diff --git a/docs-src/.gitignore b/docs-src/.gitignore new file mode 100644 index 00000000..45ddf0ae --- /dev/null +++ b/docs-src/.gitignore @@ -0,0 +1 @@ +site/ diff --git a/docs-src/docs/features/collaboration.md b/docs-src/docs/features/collaboration.md new file mode 100644 index 00000000..01c84a02 --- /dev/null +++ b/docs-src/docs/features/collaboration.md @@ -0,0 +1,33 @@ +# Real-Time Collaboration + +ODIN supports real-time collaboration through the [Matrix](https://matrix.org) protocol. Multiple ODIN instances can share layers and synchronise changes in real time. + +## How It Works + +Collaboration in ODIN operates at the **layer level**. When you share a layer, it becomes available to other ODIN users connected to the same Matrix server. Changes to features within shared layers are replicated automatically. + +Matrix is a federated, open protocol — you can use public Matrix servers or host your own for full control over your data. + +## Sharing a Layer + +1. Select the layer you want to share in the sidebar +2. Open the **Sharing** panel from the Properties Panel +3. Configure the Matrix server connection +4. Invite collaborators + +## Permissions + +Shared layers support different permission levels: + +| Power Level | Access | +|------------|--------| +| 0 | Read-only access | +| 25 | Read and write (create/edit features) | +| 50 | Change layer name and settings | +| 100 | Full control (creator) | + +A shared layer is **read-only by default**. Permissions for individual users can be changed at any time by the layer owner. + +## Community + +Join the ODIN community on Matrix: [#ODIN.Community:syncpoint.io](https://matrix.to/#/#ODIN.Community:syncpoint.io) diff --git a/docs-src/docs/features/elevation-data.md b/docs-src/docs/features/elevation-data.md new file mode 100644 index 00000000..c6c73c1c --- /dev/null +++ b/docs-src/docs/features/elevation-data.md @@ -0,0 +1,148 @@ +# Elevation Data and Elevation Profile + +ODIN supports RGB-encoded terrain tiles for displaying elevation data. When configured, elevation information is shown at the cursor position and can be used to generate elevation profiles along lines. + +## Setting Up Elevation Data + +Elevation data is provided through tile services that encode elevation values in the RGB channels of each pixel (Mapbox Terrain-RGB format). ODIN supports three tile service types as terrain sources: + +- **XYZ** — direct `{z}/{x}/{y}` tile URL +- **TileJSON** — a single TileJSON endpoint +- **TileJSON Discovery** — a server (e.g. [mbtileserver](https://github.com/consbio/mbtileserver)) that exposes multiple tilesets, any of which can individually be marked as terrain + +### Step 1: Add a Terrain Tile Service + +1. Press `Ctrl+N` followed by `T` to create a new tile service +2. In the **URL** field, enter the address of your terrain tile server +3. Press `Tab` or click outside the field to confirm + +ODIN automatically detects the service type from the URL response. + +### Step 2: Mark as Terrain Data + +**For XYZ and TileJSON services:** + +1. In the tile service properties, enable the **RGB-encoded terrain data** checkbox +2. This tells ODIN to interpret the tile pixels as elevation values instead of displaying them as a visible map layer + +**For TileJSON Discovery services:** + +A discovery endpoint may expose multiple tilesets — some may be regular map tiles, others may contain elevation data. Terrain is configured per tileset: + +1. In the layer list, select the tileset that contains elevation data +2. Enable the **RGB-encoded terrain data** checkbox +3. Repeat for any additional terrain tilesets + +Each discovered tileset can be independently marked as terrain or left as a regular map layer. + +The terrain layer will be active but invisible on the map. Elevation values are decoded using the Mapbox Terrain-RGB formula: + +``` +elevation = -10000 + (R * 65536 + G * 256 + B) * 0.1 +``` + +### Step 3: Verify + +Once configured, the current elevation is displayed in the on-screen display (bottom-right corner) as you move the cursor over the map. + +## Elevation Profile + +The Elevation Profile tool generates a chart showing elevation (Y-axis) vs. distance (X-axis) along a line. It works independently of the current map viewport — even parts of the line that extend beyond the visible area are sampled correctly. + +### Creating an Elevation Profile + +There are two ways to create an elevation profile: + +#### Option A: From a Selected Feature + +1. Select an existing feature with a LineString geometry on the map +2. Open the measure dropdown in the toolbar (ruler icon) +3. Click **Elevation Profile** +4. The profile chart appears at the bottom of the screen + +#### Option B: By Drawing a Line + +1. Make sure no LineString feature is selected +2. Open the measure dropdown and click **Elevation Profile** +3. Draw a line on the map by clicking to place points +4. Double-click to finish the line +5. The profile chart appears at the bottom of the screen + +### Reading the Profile Chart + +The chart displays: + +- **X-axis**: Distance along the line (in meters or kilometers) +- **Y-axis**: Elevation (in meters), auto-scaled to the data range +- **Orange filled area**: The elevation curve along the line +- **Dashed vertical lines**: Segment boundaries (see below) + +#### Header Statistics + +The header bar shows summary statistics for the profile: + +| Statistic | Description | +|-----------|-------------| +| **Min** | Lowest elevation along the line | +| **Max** | Highest elevation along the line | +| **Dist** | Total geodesic length of the line | +| **↑** | Total ascent (cumulative elevation gain) | +| **↓** | Total descent (cumulative elevation loss) | + +#### Segment Markers + +When the profile is based on a line with multiple segments (more than two vertices), dashed vertical lines appear in the chart at the distance where each segment boundary (vertex) is located. These markers help you correlate specific sections of the profile with the corresponding segments of the line on the map. For example, if your line follows a road with several waypoints, each marker shows where one segment ends and the next begins. + +#### Hover Interaction + +- Move the mouse over the chart to see a crosshair with the exact elevation and distance at that position +- A corresponding orange marker appears on the map, showing the geographic location of the hovered point +- Moving the mouse off the chart removes the marker + +### Editing the Profile Line + +#### Drawn Lines + +Lines drawn with the Elevation Profile tool are immediately editable: + +- **Move a vertex**: Click and drag an existing vertex +- **Add a vertex**: Click on a segment between vertices +- **Remove a vertex**: Alt-click (Option-click on macOS) on a vertex + +The elevation profile recalculates automatically after each edit. + +#### Selected Features + +When the profile is based on a selected feature from the map, edits made to that feature through ODIN's standard modify tool are also reflected in the profile. The chart updates automatically when the geometry changes. + +### Closing the Profile + +Click the **X** button in the profile panel header. This also removes the line highlight and hover marker from the map. + +## Tips + +- The Elevation Profile button in the toolbar is only enabled when at least one terrain tile service is configured +- Disabling the last terrain source automatically closes any open elevation profile +- You can create multiple profiles in sequence — each new profile replaces the previous one +- For best results, use a terrain tile service that covers the area of your line +- Terrain tiles are cached in memory (up to 200 tiles), so revisiting the same area is fast +- The tool automatically uses the highest available zoom level of the terrain service, regardless of how far you are zoomed in on the map + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| Elevation Profile is greyed out in the menu | No terrain tile service is configured. Add a tile service (XYZ, TileJSON, or TileJSON Discovery) and enable "RGB-encoded terrain data" | +| Profile chart is empty or shows gaps | The line extends beyond the terrain tile coverage area. Gaps indicate coordinates where no elevation data is available | +| No elevation shown at cursor position | Verify the terrain tile service is correctly configured and the server is accessible | +| Profile doesn't update after editing | Edits trigger a debounced recalculation (300ms delay). Wait briefly for the profile to refresh | + +## Terrain Tile Sources + +Any tile server providing Mapbox Terrain-RGB encoded tiles is compatible. Supported configurations: + +- **Direct XYZ URL** — standard `{z}/{x}/{y}` tile endpoint +- **TileJSON endpoint** — a URL serving a [TileJSON](https://github.com/mapbox/tilejson-spec) document with a `tiles` array +- **TileJSON Discovery server** — a server like [mbtileserver](https://github.com/consbio/mbtileserver) that lists multiple tilesets; individual tilesets can be marked as terrain independently + +In all cases, tiles must be served with CORS headers (`Access-Control-Allow-Origin`) enabled. diff --git a/docs-src/docs/features/mgrs-graticule.md b/docs-src/docs/features/mgrs-graticule.md new file mode 100644 index 00000000..f6ac91c6 --- /dev/null +++ b/docs-src/docs/features/mgrs-graticule.md @@ -0,0 +1,47 @@ +# MGRS Graticule + +ODIN provides a Military Grid Reference System (MGRS) coordinate grid overlay for the map. The grid is rendered as a vector layer and adapts its level of detail based on the current zoom level. + +## Enabling the MGRS Grid + +1. Open the **View** menu +2. Select **Graticules → MGRS** + +The MGRS grid overlay appears on the map. WGS84 and MGRS graticules are mutually exclusive — enabling one disables the other. + +## Grid Levels + +The graticule renders three levels of detail based on the map resolution: + +| Level | Grid Spacing | Visible When | Content | +|-------|-------------|--------------|---------| +| Grid Zone Designation (GZD) | 6° × 8° | Always | Zone boundaries and labels (e.g. `33U`) | +| 100 km Squares | 100 km | Resolution ≤ ~1200 m/px | Grid lines and two-letter square identifiers (e.g. `WP`) | +| 10 km Squares | 10 km | Resolution ≤ ~120 m/px | Fine grid lines with numeric labels | + +## About MGRS + +MGRS is based on the Universal Transverse Mercator (UTM) projection. UTM divides the world into 60 zones (each 6° wide) and lettered latitude bands (each 8° tall, from 80°S to 84°N). Within each zone, locations are identified by 100 km square letters and numeric easting/northing offsets. + +### Example MGRS Coordinate + +``` +33U WP 12345 67890 +``` + +- `33U` — Grid Zone Designation (zone 33, band U) +- `WP` — 100 km square identifier +- `12345 67890` — Easting and northing within the square (1 m precision) + +## Styling + +- **GZD boundaries** — Thicker stroke with zone labels at the centre of each visible zone +- **100 km grid lines** — Medium stroke with square letter labels +- **10 km grid lines** — Thin stroke with numeric labels +- All lines use a neutral colour to avoid interfering with map content + +## Limitations + +- UTM special zones (Norway, Svalbard) use standard 6° boundaries +- Polar regions (UPS) are not supported +- Grid levels finer than 10 km (1 km, 100 m) are not yet available diff --git a/docs-src/docs/features/shapes.md b/docs-src/docs/features/shapes.md new file mode 100644 index 00000000..d2881f15 --- /dev/null +++ b/docs-src/docs/features/shapes.md @@ -0,0 +1,133 @@ +# Shapes — Lines, Polygons, and Text + +ODIN supports custom shapes that are not part of the military symbology standard. +These are plain geometric features — lines, polygons, and text labels — that you can +freely place on the map and style to your liking. + +## Drawing Shapes + +All shape tools are available from the **Shapes dropdown** in the toolbar +(the icon with the geometric shape outline). + +![Shapes dropdown menu](img/shapes-dropdown.jpg) +*The Shapes dropdown offers three drawing tools: Line, Polygon, and Text.* + +### Drawing a Line + +1. Click the **Shapes** dropdown in the toolbar. +2. Select **Draw Line**. +3. Click on the map to place each vertex of the line. +4. **Double-click** to finish the line. + +The line appears with a default black stroke (width 2). + +### Drawing a Polygon + +1. Click the **Shapes** dropdown in the toolbar. +2. Select **Draw Polygon**. +3. Click on the map to place each vertex of the polygon. +4. **Double-click** to close and finish the polygon. + +The polygon appears with a black outline and no fill by default. + +### Placing Text + +1. Click the **Shapes** dropdown in the toolbar. +2. Select **Place Text**. +3. Click on the map where you want the text to appear. + +A text label reading "Text" is placed at the clicked position. +You can immediately edit it in the properties panel. + +## Editing Shapes + +Select any shape on the map or in the sidebar to open its **Properties Panel**. + +### Line Properties + +| Property | Description | +|---|---| +| **Line Color** | Color of the line stroke. Choose from the palette or leave undefined. | +| **Line Width** | Thickness from S (1px) to XXXL (6px). | +| **Line Style** | Solid, Dashed, or Dotted. | + +### Polygon Properties + +Polygons have all the line properties above, plus: + +| Property | Description | +|---|---| +| **Fill Color** | Interior fill color. Leave undefined for no fill. | +| **Fill Opacity** | Transparency of the fill, from 0 (transparent) to 1 (opaque). | + +### Text Properties + +The text properties panel has two sections: + +#### Text Input + +A text area where you can type or paste your content. +A simple **Markdown** dialect is supported: + +``` +# Heading +## Subheading +- Bullet point +* Another bullet point +Plain text on multiple lines +``` + +- `#` creates a large, bold heading +- `##` creates a medium, bold subheading +- `-` or `*` at the start of a line creates a bullet list item +- Everything else is rendered as plain text + +![Text shape with markdown](img/shapes-text-markdown.jpg) +*A text shape using markdown for structured annotations. The properties panel on the right shows the text input and style options.* + +#### Style Options + +| Property | Description | +|---|---| +| **Text Color** | Color of the text. Default: black. | +| **Background Color** | Color of the text box background. Default: white. | +| **Background Opacity** | Transparency of the background, from 0 (fully transparent) to 1 (opaque). Default: 0.8. | +| **Font Size** | Base font size from 10px to 32px. Headings scale proportionally. | +| **Rotation** | Rotate the text label from 0° to 359°. Use the slider for fine control. | +| **Text Scale** | Click **"Use current view as full size"** to set the current zoom level as the reference. The text will appear at full size at this zoom level and shrink proportionally when zoomed out. | + +## Zoom Behavior (Text) + +Text labels scale with the map zoom level: + +- At the **reference zoom** (set when the text was created, or adjusted via the button), + the text appears at full size. +- When **zooming out**, the text shrinks proportionally so it does not cover the map. +- The text never shrinks below 15% of its original size. +- When **zooming in** beyond the reference level, the text stays at full size + (it does not grow larger). + +This ensures text labels are readable at the zoom level they were designed for, +without cluttering the map at overview zoom levels. + +## Sidebar Tags + +Shapes are tagged in the sidebar for easy filtering: + +- **SHAPE** — all shape features +- **LINE** — line shapes +- **POLYGON** — polygon shapes +- **TEXT** — text shapes + +Use the tag search to quickly filter for specific shape types. + +## Tips + +- **Text as map annotations:** Use text shapes to add context to your map — + unit names, phase labels, terrain notes, or any free-form information. +- **Transparent backgrounds:** Set background opacity to 0 for text that + floats directly on the map without a box. +- **Rotated labels:** Use rotation to align text with roads, borders, or + other linear features on the map. +- **Markdown headings:** Use `#` headings for titles and `-` lists for + structured annotations like task organizations or key terrain lists. diff --git a/docs-src/docs/features/symbology.md b/docs-src/docs/features/symbology.md new file mode 100644 index 00000000..ae894080 --- /dev/null +++ b/docs-src/docs/features/symbology.md @@ -0,0 +1,56 @@ +# Military Symbology + +ODIN supports the MIL-STD-2525C standard for military symbol identification codes (SIDC). Symbols can be placed on the map as point features within layers. + +## Placing Symbols + +1. Press ++ctrl+n++ to open the symbol search +2. Type a symbol name (e.g., "Infantry", "Headquarters", "Armor") +3. Select the desired symbol from the results +4. Click on the map to place the symbol + +The symbol appears on the map with the standard military icon and any configured modifiers. + +## Symbol Properties + +Select a symbol to view and edit its properties in the Properties Panel: + +| Property | Description | +|----------|-------------| +| **SIDC** | The 15-character Symbol Identification Code | +| **Designator (t)** | Unit designator text displayed below the symbol | +| **Additional Info (t1)** | Additional information text | +| **Name** | Display name shown in the sidebar | + +## Affiliation + +The second character of the SIDC determines the affiliation: + +| Code | Affiliation | Colour | +|------|-------------|--------| +| F | Friendly | Blue | +| H | Hostile | Red | +| N | Neutral | Green | +| U | Unknown | Yellow | + +## Common SIDC Examples + +| SIDC | Description | +|------|-------------| +| `SFGPUCI----D---` | Friendly Infantry Unit | +| `SFGPUCIZ---D---` | Friendly Mechanised Infantry | +| `SFGPUCA----D---` | Friendly Armour Unit | +| `SFGPUH----H---` | Friendly Headquarters | +| `SHGPUCI----D---` | Hostile Infantry Unit | +| `GFGPGLB----K---` | Boundary Line | +| `GFGPGAA----K---` | Assembly Area | + +## Colour Scheme + +ODIN supports three colour schemes for military symbols: + +- **Dark** — Standard dark colours +- **Medium** — Medium saturation +- **Light** — Light/pastel colours + +The colour scheme can be configured globally, per layer, or per feature. Feature styles override layer styles, which override global styles. diff --git a/docs-src/docs/features/tile-services.md b/docs-src/docs/features/tile-services.md new file mode 100644 index 00000000..efa209e1 --- /dev/null +++ b/docs-src/docs/features/tile-services.md @@ -0,0 +1,45 @@ +# Tile Services + +Tile services provide background map imagery and elevation data for ODIN. You can configure multiple tile services and switch between them. + +## Supported Service Types + +| Type | Description | +|------|-------------| +| **XYZ** | Standard `{z}/{x}/{y}` tile URL | +| **TileJSON** | Single TileJSON endpoint | +| **TileJSON Discovery** | Server exposing multiple tilesets (e.g. [mbtileserver](https://github.com/consbio/mbtileserver)) | + +## Adding a Tile Service + +1. Press ++ctrl+n++ followed by ++t++ to create a new tile service +2. Enter the tile server URL in the **URL** field +3. Press ++tab++ or click outside the field to confirm + +ODIN automatically detects the service type from the URL response. + +## TileJSON Discovery + +When using a TileJSON Discovery server: + +1. ODIN detects the available tilesets and displays them as a list +2. Each tileset has a checkbox — enable the ones you want to use +3. Selected tilesets appear in the **Background Maps** control + +This is ideal for self-hosted setups using [mbtileserver](https://github.com/consbio/mbtileserver) with `.mbtiles` files. + +## Terrain Data + +Tile services can provide RGB-encoded elevation data (Mapbox Terrain-RGB format) instead of visible map imagery. See [Elevation Data](elevation-data.md) for setup instructions. + +When a tile service is configured as terrain, a system tag (`TERRAIN`) appears in the sidebar to visually distinguish it from regular map layers. This tag cannot be removed by the user. + +## Self-Hosting Maps + +For offline or air-gapped environments, you can host your own tile server: + +1. Obtain `.mbtiles` files for your area of interest +2. Run [mbtileserver](https://github.com/consbio/mbtileserver) on your local machine or network +3. Add the server URL to ODIN as a TileJSON Discovery service + +This gives you full operational independence without requiring an internet connection. diff --git a/docs-src/docs/getting-started/core-concepts.md b/docs-src/docs/getting-started/core-concepts.md new file mode 100644 index 00000000..ef3e4ff1 --- /dev/null +++ b/docs-src/docs/getting-started/core-concepts.md @@ -0,0 +1,58 @@ +# Core Concepts + +This page introduces the fundamental concepts in ODIN. + +## Projects + +A **project** is the top-level container for all your work. Each project is stored locally and contains layers, features, tile services, bookmarks, and settings. You can have multiple projects and switch between them. + +## Layers + +**Layers** are the primary organisational unit. Each layer is a container that groups features (symbols, graphics, shapes) on the map. + +- **Thematic separation** — Create layers for friendly forces, hostile forces, logistics, boundaries, etc. +- **Visibility** — Toggle layers on and off to control what is shown on the map +- **Locking** — Lock a layer to prevent accidental edits +- **Sharing** — Share individual layers with other ODIN users via Matrix + +## Features + +A **feature** is any object placed on the map. Features belong to a layer and have: + +- **Geometry** — The geographic shape (point, line, polygon) +- **Properties** — Attributes such as the symbol code (SIDC), name, and designator +- **Style** — Visual appearance (colours, line width, fill) +- **Tags** — Searchable labels for filtering and organisation + +### Feature Types + +| Type | Description | +|------|-------------| +| Military symbols | MIL-STD-2525C point symbols (units, equipment, installations) | +| Tactical graphics | Standardised lines and areas (boundaries, phase lines, assembly areas) | +| Shapes | Custom lines, polygons, and text labels | +| Markers | Simple point markers | + +## Tags + +**Tags** are free-form labels that can be attached to layers and features. Use them to categorise and filter your data. Tags are searchable from the sidebar. + +Examples: `#PRIORITY`, `#PHASE-1`, `#LOGISTICS`, `#ARTILLERY` + +## Tile Services + +**Tile services** provide background map imagery. ODIN supports: + +- **XYZ** — Standard `{z}/{x}/{y}` tile URLs +- **TileJSON** — Single TileJSON endpoints +- **TileJSON Discovery** — Servers like [mbtileserver](https://github.com/consbio/mbtileserver) that expose multiple tilesets + +Tile services can also provide RGB-encoded elevation data for terrain analysis. + +## Bookmarks + +**Bookmarks** save map view positions (centre, zoom, rotation) for quick navigation. Use them to jump between areas of interest on the map. + +## Links + +**Links** attach external resources to layers or features. They can point to URLs or local files, allowing you to reference documents, briefings, or web resources directly from the map. diff --git a/docs-src/docs/getting-started/installation.md b/docs-src/docs/getting-started/installation.md new file mode 100644 index 00000000..1fb84cbb --- /dev/null +++ b/docs-src/docs/getting-started/installation.md @@ -0,0 +1,59 @@ +# Installation + +ODIN runs on Windows, macOS, and Linux. No registration or account is required. + +## Download + +Get the latest release from the [GitHub Releases](https://github.com/syncpoint/ODINv2/releases) page. + +### Windows + +Download the `.exe` installer from the releases page. Run the installer and follow the prompts. Requires Windows 10 or later. + +### macOS + +Download the `.dmg` file from the releases page. Open the disk image and drag ODIN to your Applications folder. Requires macOS 10.15 (Catalina) or later. + +### Linux + +=== "Snap Store (recommended)" + + Install from the Snap Store for automatic updates: + + ```bash + sudo snap install odin-v2 + ``` + + [![Get it from the Snap Store](https://snapcraft.io/en/light/install.svg)](https://snapcraft.io/odin-v2) + +=== "AppImage" + + Download the `.AppImage` file from the releases page: + + ```bash + chmod +x ODIN-*.AppImage + ./ODIN-*.AppImage + ``` + +## First Launch + +On first launch, ODIN creates a new empty project. You can immediately: + +1. Add map tile services for background maps +2. Create layers and place military symbols +3. Draw shapes and annotations +4. Configure collaboration via Matrix + +## Migrating from ODINv1 + +If you have an existing ODINv1 installation, ODIN v2 will automatically migrate all your projects on first start. You can continue using ODINv1 in parallel, but changes made in ODINv1 after the initial migration will not be synchronised. + +To import individual ODINv1 layer files (`.json`), drag and drop them onto the ODIN v2 map at any time. + +## Self-Update + +ODIN checks for updates automatically. To disable self-update, set the environment variable: + +```bash +ODIN_SELF_UPDATE=0 +``` diff --git a/docs-src/docs/getting-started/overview.md b/docs-src/docs/getting-started/overview.md new file mode 100644 index 00000000..dc8d154c --- /dev/null +++ b/docs-src/docs/getting-started/overview.md @@ -0,0 +1,54 @@ +# Overview + +ODIN is an open-source Command and Control Information System (C2IS) built for tactical mapping, situational awareness, and distributed coordination. + +## Key Capabilities + +| Capability | Description | +|-----------|-------------| +| **Military Symbology** | MIL-STD-2525C symbols with full modifier support | +| **Tactical Graphics** | Boundaries, areas, lines, and control measures | +| **Custom Shapes** | Lines, polygons, and text annotations with styling | +| **Collaboration** | Real-time layer sharing via the Matrix protocol | +| **Offline Operation** | Self-hosted map tiles, search, and elevation data | +| **Elevation Analysis** | RGB-encoded terrain tiles with elevation profiles | +| **External Integration** | NIDO WebSocket API and live SSE data sources | +| **MGRS Graticule** | Military Grid Reference System overlay | +| **Cross-Platform** | Windows, macOS, and Linux (Electron) | + +## Architecture + +ODIN is an Electron desktop application. Data is stored locally in each project and can optionally be replicated to other ODIN instances via Matrix servers. + +``` +┌─────────────────────────────────────────┐ +│ ODIN Desktop │ +│ ┌──────────┐ ┌──────────┐ ┌───────┐ │ +│ │ Map │ │ Sidebar │ │Toolbar│ │ +│ │(OpenLayers)│ │(Layers/ │ │ │ │ +│ │ │ │ Features)│ │ │ │ +│ └────┬─────┘ └────┬─────┘ └───┬───┘ │ +│ └──────┬───────┘ │ │ +│ ▼ │ │ +│ ┌─────────────────┐ │ │ +│ │ Project Store │◄─────────┘ │ +│ └────────┬────────┘ │ +│ │ │ +│ ┌─────────┼──────────┐ │ +│ ▼ ▼ ▼ │ +│ [Matrix] [NIDO WS] [SSE Live] │ +└─────────────────────────────────────────┘ +``` + +- **Matrix** — Federated protocol for layer-level replication across instances +- **NIDO** — WebSocket API for full read/write access from external applications +- **SSE Live** — Server-Sent Events for real-time tracking data (read-only) + +## Technology Stack + +- **Runtime**: Electron (Node.js + Chromium) +- **Map Engine**: OpenLayers +- **Symbology**: MIL-STD-2525C renderer +- **Replication**: Matrix protocol +- **Persistence**: Local project files +- **Package**: Snap, AppImage, DMG, NSIS installer diff --git a/docs-src/docs/index.md b/docs-src/docs/index.md new file mode 100644 index 00000000..283c7e96 --- /dev/null +++ b/docs-src/docs/index.md @@ -0,0 +1,33 @@ +# ODIN Documentation + +Welcome to the official documentation for **ODIN** — the Open Source Command & Control Information System. + +ODIN is a free, cross-platform desktop application for tactical mapping and situational awareness. It supports MIL-STD-2525C military symbology, real-time collaboration via the [Matrix](https://matrix.org) protocol, and works fully offline. + +## Quick Links + +
+ +- :material-download: **[Installation](getting-started/installation.md)** — Download and install ODIN on Windows, macOS, or Linux +- :material-book-open-variant: **[Core Concepts](getting-started/core-concepts.md)** — Layers, features, tags, and the basics +- :material-shape: **[Shapes](features/shapes.md)** — Lines, polygons, and text annotations +- :material-api: **[NIDO API](integration/nido.md)** — Integrate external systems via WebSocket +- :material-access-point: **[Live Data Sources](integration/live-data-sources.md)** — Real-time tracking via SSE + +
+ +## What is ODIN? + +ODIN is designed for professionals who need reliable tactical mapping without vendor lock-in: + +- **Offline-first** — Self-host map tiles and search services for air-gapped environments +- **MIL-STD-2525C** — Full military symbology support with drag-and-drop placement +- **Real-time collaboration** — Share layers across teams using the Matrix ecosystem +- **Extensible** — Connect external systems via the NIDO WebSocket API or live data feeds +- **Open source** — Licensed under AGPL-3.0, fully auditable + +## Getting Help + +- [GitHub Issues](https://github.com/syncpoint/ODINv2/issues) — Report bugs or request features +- [Matrix Chat](https://matrix.to/#/#ODIN.Community:syncpoint.io) — Join the community +- [Email](mailto:office@syncpoint.io) — Contact Syncpoint GmbH diff --git a/docs-src/docs/integration/live-data-sources.md b/docs-src/docs/integration/live-data-sources.md new file mode 100644 index 00000000..4eeed304 --- /dev/null +++ b/docs-src/docs/integration/live-data-sources.md @@ -0,0 +1,225 @@ +# Live Data Source + +The Live Data Source feature allows ODIN to receive and display real-time geospatial data from external systems via Server-Sent Events (SSE). + +## Use Cases + +- **Real-time tracking**: Display moving assets such as vehicles, aircraft, or personnel on the map +- **Sensor data visualization**: Show live sensor readings with geographic positions +- **Situational awareness**: Integrate external data feeds into your operational picture +- **IoT integration**: Display data from IoT devices with location information + +## Creating a Live Data Source + +1. Click the **+** button in the toolbar +2. Select **Create Live Data Source** +3. Configure the connection parameters: + - **Name**: A descriptive name for the data source + - **URL**: The SSE endpoint URL + - **Event Type**: The SSE event name to listen for (default: `message`) + - **Update Interval**: Rate limiting in milliseconds (default: 100ms) + - **Track features by ID**: How to handle incoming features (see below) +4. Enable the checkbox to connect + +### Feature Tracking Modes + +The **Track features by ID** option controls how incoming features are processed: + +**Enabled (default)**: Features are tracked by their `id` field. When a feature with the same ID arrives, it updates the existing feature (position, properties). This is ideal for tracking moving assets where each asset has a unique identifier. + +**Disabled**: All features are replaced on each update. The previous features are cleared and new features are added. This mode is suitable for data sources that: +- Don't provide feature IDs +- Send complete snapshots of all features on each update +- Have features that don't need individual tracking + +## SSE Endpoint Requirements + +The SSE endpoint must comply with the [Server-Sent Events specification](https://html.spec.whatwg.org/multipage/server-sent-events.html). + +### HTTP Response Headers + +``` +Content-Type: text/event-stream +Cache-Control: no-cache +Connection: keep-alive +``` + +### CORS + +If the SSE endpoint is on a different origin than ODIN, it must include appropriate CORS headers: + +``` +Access-Control-Allow-Origin: * +``` + +### Event Format + +Events should be sent in the standard SSE format: + +``` +event: +data: + +``` + +Note: Each event must end with two newlines. + +If using the default event type (`message`), the `event:` line can be omitted: + +``` +data: + +``` + +## Data Format + +The data payload must be valid GeoJSON. Two formats are supported: + +### Single Feature + +```json +{ + "type": "Feature", + "id": "unit-001", + "geometry": { + "type": "Point", + "coordinates": [16.3738, 48.2082] + }, + "properties": { + "sidc": "SFGPUCI----D", + "name": "Alpha Unit" + } +} +``` + +### Feature Collection + +```json +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": "unit-001", + "geometry": { + "type": "Point", + "coordinates": [16.3738, 48.2082] + }, + "properties": { + "sidc": "SFGPUCI----D", + "name": "Alpha Unit" + } + }, + { + "type": "Feature", + "id": "unit-002", + "geometry": { + "type": "Point", + "coordinates": [16.3850, 48.2100] + }, + "properties": { + "sidc": "SFGPUCI----D", + "name": "Bravo Unit" + } + } + ] +} +``` + +### Required Fields + +| Field | Description | +|-------|-------------| +| `type` | Must be `"Feature"` or `"FeatureCollection"` | +| `id` | Unique identifier for the feature. Required when "Track features by ID" is enabled. Used to update existing features. | +| `geometry` | GeoJSON geometry object | +| `geometry.type` | Geometry type (e.g., `"Point"`, `"LineString"`, `"Polygon"`) | +| `geometry.coordinates` | Coordinates in the configured projection (default: EPSG:4326 / WGS84) | + +### Optional Fields + +| Field | Description | +|-------|-------------| +| `properties.sidc` | MIL-STD-2525C symbol identification code for military symbology | +| `properties.name` | Display name for the feature | +| `properties.*` | Any additional properties for styling or information | + +### Coordinate System + +By default, coordinates are expected in **EPSG:4326 (WGS84)** format: +- Longitude first, then latitude: `[longitude, latitude]` +- Example: `[16.3738, 48.2082]` for Vienna, Austria + +The data projection can be configured in the Live Data Source settings if your endpoint uses a different coordinate system. + +## Example SSE Server (Node.js) + +```javascript +const http = require('http'); + +const server = http.createServer((req, res) => { + if (req.url === '/api/stream') { + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'Access-Control-Allow-Origin': '*' + }); + + // Send initial data + const sendUpdate = () => { + const data = { + type: 'Feature', + id: 'track-001', + geometry: { + type: 'Point', + coordinates: [16.37 + Math.random() * 0.1, 48.20 + Math.random() * 0.1] + }, + properties: { + sidc: 'SFGPUCI----D', + name: 'Moving Unit' + } + }; + + res.write(`data: ${JSON.stringify(data)}\n\n`); + }; + + // Send updates every second + const interval = setInterval(sendUpdate, 1000); + sendUpdate(); + + req.on('close', () => { + clearInterval(interval); + }); + } else { + res.writeHead(404); + res.end(); + } +}); + +server.listen(3000, () => { + console.log('SSE server running on http://localhost:3000/api/stream'); +}); +``` + +## Troubleshooting + +### Connection Issues + +- Verify the URL is correct and accessible +- Check browser developer tools for CORS errors +- Ensure the server sends proper SSE headers + +### Features Not Appearing + +- Verify the JSON payload is valid +- If "Track features by ID" is enabled, check that the `id` field is present on each feature +- If your data source doesn't provide IDs, disable "Track features by ID" +- Ensure coordinates are in the correct order (longitude, latitude) +- Verify the event type matches the configured value + +### Features Not Updating + +- Ensure "Track features by ID" is enabled +- Ensure each update uses the same `id` for features that should be updated +- Check the update interval setting - very low values may cause performance issues diff --git a/docs-src/docs/integration/nido.md b/docs-src/docs/integration/nido.md new file mode 100644 index 00000000..6c9a0192 --- /dev/null +++ b/docs-src/docs/integration/nido.md @@ -0,0 +1,1249 @@ +# NIDO - External Integration API + +NIDO (ODIN reversed) is a WebSocket-based API that enables external applications to integrate with ODIN in real-time. Connected tools can receive live updates when data changes and send commands to create, modify, or delete project data. + +## Table of Contents + +- [Security Warning](#security-warning) +- [Overview](#overview) +- [Connection Management](#connection-management) + - [Establishing a Connection](#establishing-a-connection) + - [Connection States](#connection-states) + - [Troubleshooting](#troubleshooting) +- [Protocol Reference](#protocol-reference) + - [Message Format](#message-format) + - [Incoming Messages (Server → ODIN)](#incoming-messages-server--odin) + - [Outgoing Messages (ODIN → Server)](#outgoing-messages-odin--server) +- [Data Operations](#data-operations) + - [Layers](#layers) + - [Features](#features) + - [Markers](#markers) + - [Bookmarks](#bookmarks) + - [Tags](#tags) + - [Links](#links) + - [Visibility (Hidden)](#visibility-hidden) + - [Locking](#locking) + - [Styles](#styles) +- [View Control](#view-control) +- [Coordinate System](#coordinate-system) +- [Complete Workflow Example](#complete-workflow-example) +- [Test Server](#test-server) + +--- + +## Security Warning + +> **WARNING: NIDO allows external applications to read and modify ALL data in your project.** +> +> - Any connected server can **read** all layers, features, markers, and other project data +> - Any connected server can **create, modify, or delete** data in your project +> - Changes made via NIDO are **immediate and may be difficult to undo** +> - There is **no authentication** built into the NIDO protocol itself +> +> **Only connect to WebSocket servers that you fully trust.** +> +> If you are unsure about a server's trustworthiness, do not enable the connection. + +--- + +## Overview + +NIDO provides bidirectional communication between ODIN and external applications: + +**ODIN sends to external server:** +- Connection information (project ID, name, ODIN version) +- Real-time batch events when data changes +- Responses to queries and commands + +**External server can send to ODIN:** +- Commands to create, update, or delete data +- Queries to retrieve data by prefix +- View commands to control the map + +### Use Cases + +- **Digital Twin**: Mirror ODIN data in an external system +- **Automation**: Programmatically create or update features +- **Integration**: Connect ODIN to other C2 systems or data sources +- **Analysis**: Export data to external analytics tools in real-time + +--- + +## Connection Management + +### Establishing a Connection + +1. Click the **LAN icon** in the ODIN toolbar (left side) +2. Enter the WebSocket URL of your server (e.g., `ws://localhost:9000`) +3. Check **Enable connection** + +The connection status badge indicates: +- **Green**: Connected successfully +- **Red**: Enabled but connection failed +- **Gray**: Disabled + +### Connection States + +When ODIN connects, it sends a `connected` message: + +```json +{ + "type": "connected", + "payload": { + "projectId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "projectName": "My Project", + "odinVersion": "3.1.0", + "clientId": "unique-client-uuid" + } +} +``` + +ODIN automatically attempts to reconnect with exponential backoff if the connection is lost. + +### Troubleshooting + +| Problem | Possible Cause | Solution | +|---------|----------------|----------| +| Red badge, no connection | Server not running | Start your WebSocket server | +| Red badge, connection refused | Wrong URL or port | Verify the WebSocket URL | +| Red badge, connection drops | Network issues | Check network connectivity | +| No data received | Not subscribed to events | Ensure server handles `batch` messages | +| Commands fail | Invalid key format | Check key prefix is valid | +| Coordinates wrong | Projection mismatch | NIDO uses EPSG:4326 (lon/lat) | + +--- + +## Protocol Reference + +### Message Format + +All messages are JSON objects with a `type` field. Commands and queries include an `id` field for correlating responses. + +### Incoming Messages (Server → ODIN) + +#### Command + +Execute data operations (put, del, batch). + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "layer:uuid-here", + "value": { "name": "My Layer" } + } +} +``` + +#### Query + +Retrieve data by key prefix. + +```json +{ + "type": "query", + "id": "qry-1", + "payload": { + "prefix": "layer:" + } +} +``` + +#### View + +Control the map view. + +```json +{ + "type": "view", + "id": "view-1", + "payload": { + "action": "flyto", + "center": [16.37, 48.21] + } +} +``` + +### Outgoing Messages (ODIN → Server) + +#### connected + +Sent when connection is established. + +#### batch + +Sent when data changes in ODIN. + +```json +{ + "type": "batch", + "payload": { + "operations": [ + { "type": "put", "key": "feature:...", "value": {...} }, + { "type": "del", "key": "feature:..." } + ], + "timestamp": 1699876543210 + } +} +``` + +#### command:response + +```json +{ + "type": "command:response", + "id": "cmd-1", + "success": true +} +``` + +#### query:response + +```json +{ + "type": "query:response", + "id": "qry-1", + "success": true, + "payload": { + "tuples": [ + ["layer:uuid-1", { "name": "Layer 1" }], + ["layer:uuid-2", { "name": "Layer 2" }] + ] + } +} +``` + +#### view:response + +```json +{ + "type": "view:response", + "id": "view-1", + "success": true, + "payload": { + "center": [16.37, 48.21], + "zoom": 12, + "resolution": 38.21, + "rotation": 0 + } +} +``` + +#### error + +```json +{ + "type": "error", + "id": "cmd-1", + "error": { + "code": "INVALID_KEY", + "message": "Invalid key: invalid:prefix" + } +} +``` + +Error codes: `INVALID_MESSAGE`, `INVALID_KEY`, `INVALID_ACTION`, `COMMAND_FAILED`, `QUERY_FAILED` + +--- + +## Data Operations + +### Valid Key Prefixes + +| Prefix | Description | Requires Geometry | +|--------|-------------|-------------------| +| `layer:` | Map layers | No | +| `feature:` | Features within layers | Yes | +| `marker:` | Map markers | Yes | +| `bookmark:` | Saved map views | No | +| `place:` | Search result places | No | +| `link+` | Links between entities | No | +| `tags+` | Tags for entities | No | +| `hidden+` | Visibility flags | No | +| `locked+` | Lock flags | No | +| `style+` | Style overrides | No | + +--- + +### Layers + +Layers are containers for features. + +**Key format:** `layer:{uuid}` + +**Structure:** +```json +{ + "name": "Layer Name" +} +``` + +#### Create a Layer + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "layer:550e8400-e29b-41d4-a716-446655440001", + "value": { + "name": "Operations Layer" + } + } +} +``` + +#### Query All Layers + +```json +{ + "type": "query", + "id": "qry-1", + "payload": { "prefix": "layer:" } +} +``` + +#### Delete a Layer + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "del", + "key": "layer:550e8400-e29b-41d4-a716-446655440001" + } +} +``` + +--- + +### Features + +Features are military symbols or geometric shapes within a layer. + +**Key format:** `feature:{layer-uuid}/{feature-uuid}` + +**Structure:** +```json +{ + "type": "Feature", + "name": "Optional display name", + "geometry": { + "type": "Point", + "coordinates": [16.37, 48.21] + }, + "properties": { + "sidc": "SFGPUCI----D---", + "t": "Designator text", + "t1": "Additional info" + } +} +``` + +#### Geometry Types + +- `Point` - Single coordinate +- `LineString` - Array of coordinates +- `Polygon` - Array of coordinate rings +- `MultiPoint` - Multiple points +- `GeometryCollection` - Mixed geometries + +#### Create a Feature (Infantry Unit) + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "feature:550e8400-e29b-41d4-a716-446655440001/660e8400-e29b-41d4-a716-446655440002", + "value": { + "type": "Feature", + "name": "Alpha Company", + "geometry": { + "type": "Point", + "coordinates": [16.3738, 48.2082] + }, + "properties": { + "sidc": "SFGPUCI----D---", + "t": "A/1-1" + } + } + } +} +``` + +#### Create a Line Feature (Boundary) + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "feature:550e8400-e29b-41d4-a716-446655440001/770e8400-e29b-41d4-a716-446655440003", + "value": { + "type": "Feature", + "name": "Phase Line Alpha", + "geometry": { + "type": "LineString", + "coordinates": [ + [16.35, 48.20], + [16.38, 48.21], + [16.40, 48.19] + ] + }, + "properties": { + "sidc": "GFGPGLB----K---" + } + } + } +} +``` + +#### Create a Polygon Feature (Area) + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "feature:550e8400-e29b-41d4-a716-446655440001/880e8400-e29b-41d4-a716-446655440004", + "value": { + "type": "Feature", + "name": "Assembly Area", + "geometry": { + "type": "Polygon", + "coordinates": [[ + [16.35, 48.20], + [16.40, 48.20], + [16.40, 48.22], + [16.35, 48.22], + [16.35, 48.20] + ]] + }, + "properties": { + "sidc": "GFGPGAA----K---" + } + } + } +} +``` + +#### Query Features in a Layer + +```json +{ + "type": "query", + "id": "qry-1", + "payload": { "prefix": "feature:550e8400-e29b-41d4-a716-446655440001/" } +} +``` + +#### Update a Feature + +Send a `put` command with the same key and updated value: + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "feature:550e8400-e29b-41d4-a716-446655440001/660e8400-e29b-41d4-a716-446655440002", + "value": { + "type": "Feature", + "name": "Alpha Company (Renamed)", + "geometry": { + "type": "Point", + "coordinates": [16.38, 48.22] + }, + "properties": { + "sidc": "SFGPUCI----D---", + "t": "A/1-1", + "t1": "On objective" + } + } + } +} +``` + +#### Custom SVG Icons + +For point features, you can render custom SVG graphics instead of military symbols by providing an `svg` property in the feature properties. This is useful for displaying custom markers, sensor data, or any graphical representation that doesn't fit the military symbology standard. + +**Properties:** +- `svg` - The SVG markup as a string (required for custom rendering) +- `icon-scale` - Scale factor for the icon (default: 1) +- `icon-anchor` - Anchor point as `[x, y]` where values are 0-1 representing the fraction of the icon size (default: `[0.5, 0.5]` for center) +- `icon-rotate` - Rotation angle in degrees, clockwise (default: 0) + +When an `svg` property is present, it takes precedence over any `sidc` code. + +**Example: Simple Circle Marker** + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "feature:550e8400-e29b-41d4-a716-446655440001/custom-svg-1", + "value": { + "type": "Feature", + "name": "Sensor Location", + "geometry": { + "type": "Point", + "coordinates": [16.37, 48.21] + }, + "properties": { + "svg": "", + "icon-scale": 1 + } + } + } +} +``` + +**Example: Custom Icon with Bottom Anchor** + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "feature:550e8400-e29b-41d4-a716-446655440001/custom-svg-2", + "value": { + "type": "Feature", + "name": "Pin Marker", + "geometry": { + "type": "Point", + "coordinates": [16.38, 48.22] + }, + "properties": { + "svg": "", + "icon-scale": 1.5, + "icon-anchor": [0.5, 1] + } + } + } +} +``` + +**Example: Sensor Status with Dynamic Color** + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "feature:550e8400-e29b-41d4-a716-446655440001/sensor-status", + "value": { + "type": "Feature", + "name": "Temperature Sensor #5", + "geometry": { + "type": "Point", + "coordinates": [16.39, 48.20] + }, + "properties": { + "svg": "OK", + "icon-scale": 1 + } + } + } +} +``` + +> **Note:** Ensure your SVG includes the `xmlns='http://www.w3.org/2000/svg'` attribute for proper rendering. The SVG content should be a complete, self-contained SVG document. + +--- + +### Markers + +Markers are simple point indicators on the map. + +**Key format:** `marker:{uuid}` + +**Structure:** +```json +{ + "name": "Marker Name", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [16.37, 48.21] + } +} +``` + +#### Create a Marker + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "marker:990e8400-e29b-41d4-a716-446655440005", + "value": { + "name": "Rally Point", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [16.40, 48.25] + } + } + } +} +``` + +--- + +### Bookmarks + +Bookmarks save map view positions for quick navigation. + +**Key format:** `bookmark:{uuid}` + +**Structure:** +```json +{ + "name": "Bookmark Name", + "center": [16.37, 48.21], + "zoom": 12, + "resolution": 38.21, + "rotation": 0 +} +``` + +#### Create a Bookmark + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "bookmark:aa0e8400-e29b-41d4-a716-446655440006", + "value": { + "name": "Objective Area", + "center": [16.37, 48.21], + "zoom": 14, + "resolution": 9.55, + "rotation": 0 + } + } +} +``` + +--- + +### Tags + +Tags are labels attached to layers, features, or other entities. + +**Key format:** `tags+{entity-key}` + +**Structure:** Array of tag strings +```json +["tag1", "tag2", "PRIORITY"] +``` + +#### Add Tags to a Layer + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "tags+layer:550e8400-e29b-41d4-a716-446655440001", + "value": ["OPERATIONS", "FRIENDLY", "ACTIVE"] + } +} +``` + +#### Add Tags to a Feature + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "tags+feature:550e8400-e29b-41d4-a716-446655440001/660e8400-e29b-41d4-a716-446655440002", + "value": ["PRIORITY", "MANEUVER"] + } +} +``` + +#### Remove Tags + +Delete the tags entry to remove all tags: + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "del", + "key": "tags+feature:550e8400-e29b-41d4-a716-446655440001/660e8400-e29b-41d4-a716-446655440002" + } +} +``` + +Or update with a reduced list: + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "tags+feature:...", + "value": ["PRIORITY"] + } +} +``` + +#### Query All Tags + +```json +{ + "type": "query", + "id": "qry-1", + "payload": { "prefix": "tags+" } +} +``` + +--- + +### Links + +Links attach external resources (URLs or files) to layers or features. + +**Key format:** `link+{entity-key}/{link-uuid}` + +**Structure:** +```json +{ + "name": "Link description", + "url": "https://example.com or file:///path/to/file" +} +``` + +#### Link to an External Website + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "link+layer:550e8400-e29b-41d4-a716-446655440001/bb0e8400-e29b-41d4-a716-446655440007", + "value": { + "name": "ODIN Documentation", + "url": "https://odin.syncpoint.io" + } + } +} +``` + +#### Link to a Local File + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "link+feature:550e8400-e29b-41d4-a716-446655440001/660e8400-e29b-41d4-a716-446655440002/cc0e8400-e29b-41d4-a716-446655440008", + "value": { + "name": "Mission Briefing", + "url": "file:///Users/commander/Documents/briefing.pdf" + } + } +} +``` + +#### Query Links for an Entity + +```json +{ + "type": "query", + "id": "qry-1", + "payload": { "prefix": "link+layer:550e8400-e29b-41d4-a716-446655440001/" } +} +``` + +--- + +### Visibility (Hidden) + +Control whether layers or features are visible on the map. + +**Key format:** `hidden+{entity-key}` + +**Structure:** `true` (hidden) or delete to show + +#### Hide a Layer + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "hidden+layer:550e8400-e29b-41d4-a716-446655440001", + "value": true + } +} +``` + +#### Show a Layer (Remove Hidden Flag) + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "del", + "key": "hidden+layer:550e8400-e29b-41d4-a716-446655440001" + } +} +``` + +#### Query Hidden State + +```json +{ + "type": "query", + "id": "qry-1", + "payload": { "prefix": "hidden+" } +} +``` + +--- + +### Locking + +Prevent accidental edits to layers or features. + +**Key format:** `locked+{entity-key}` + +**Structure:** `true` (locked) or delete to unlock + +#### Lock a Layer + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "locked+layer:550e8400-e29b-41d4-a716-446655440001", + "value": true + } +} +``` + +#### Unlock a Layer + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "del", + "key": "locked+layer:550e8400-e29b-41d4-a716-446655440001" + } +} +``` + +#### Lock a Feature + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "locked+feature:550e8400-e29b-41d4-a716-446655440001/660e8400-e29b-41d4-a716-446655440002", + "value": true + } +} +``` + +--- + +### Styles + +Override default styling for layers or features. + +**Key format:** `style+{entity-key}` + +**Structure:** Style object with optional properties + +#### Set Layer Style + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "style+layer:550e8400-e29b-41d4-a716-446655440001", + "value": { + "stroke-color": "#ff0000", + "stroke-width": 3, + "fill-color": "rgba(255, 0, 0, 0.2)" + } + } +} +``` + +#### Reset Style to Default + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "del", + "key": "style+layer:550e8400-e29b-41d4-a716-446655440001" + } +} +``` + +--- + +## View Control + +Control the map view programmatically. + +### Fly to Location + +Animates the map to center on a location. + +```json +{ + "type": "view", + "id": "view-1", + "payload": { + "action": "flyto", + "center": [16.37, 48.21] + } +} +``` + +### Get Current View + +Retrieve the current map view state. + +```json +{ + "type": "view", + "id": "view-2", + "payload": { + "action": "get" + } +} +``` + +**Response:** +```json +{ + "type": "view:response", + "id": "view-2", + "success": true, + "payload": { + "center": [16.37, 48.21], + "zoom": 12, + "resolution": 38.21, + "rotation": 0 + } +} +``` + +--- + +## Coordinate System + +**All coordinates in the NIDO API use EPSG:4326 (WGS84) format:** +- Format: `[longitude, latitude]` +- Longitude: -180 to 180 (East is positive) +- Latitude: -90 to 90 (North is positive) + +**Example:** Vienna, Austria = `[16.3738, 48.2082]` + +ODIN internally uses EPSG:3857 (Web Mercator), but all NIDO communications are automatically converted. + +--- + +## Complete Workflow Example + +This example demonstrates a complete workflow: creating a layer, adding features, tagging them, and managing visibility. + +### Step 1: Create a Layer + +```json +{ + "type": "command", + "id": "cmd-1", + "payload": { + "action": "put", + "key": "layer:11111111-1111-1111-1111-111111111111", + "value": { "name": "Blue Force Tracking" } + } +} +``` + +### Step 2: Add Features + +```json +{ + "type": "command", + "id": "cmd-2", + "payload": { + "action": "batch", + "operations": [ + { + "type": "put", + "key": "feature:11111111-1111-1111-1111-111111111111/aaaa-1111", + "value": { + "type": "Feature", + "name": "HQ Element", + "geometry": { "type": "Point", "coordinates": [16.37, 48.20] }, + "properties": { "sidc": "SFGPUH----H---" } + } + }, + { + "type": "put", + "key": "feature:11111111-1111-1111-1111-111111111111/aaaa-2222", + "value": { + "type": "Feature", + "name": "Alpha Team", + "geometry": { "type": "Point", "coordinates": [16.38, 48.21] }, + "properties": { "sidc": "SFGPUCI----D---", "t": "A" } + } + }, + { + "type": "put", + "key": "feature:11111111-1111-1111-1111-111111111111/aaaa-3333", + "value": { + "type": "Feature", + "name": "Bravo Team", + "geometry": { "type": "Point", "coordinates": [16.39, 48.22] }, + "properties": { "sidc": "SFGPUCI----D---", "t": "B" } + } + } + ] + } +} +``` + +### Step 3: Add Tags + +```json +{ + "type": "command", + "id": "cmd-3", + "payload": { + "action": "batch", + "operations": [ + { + "type": "put", + "key": "tags+layer:11111111-1111-1111-1111-111111111111", + "value": ["BLUE", "TRACKING", "ACTIVE"] + }, + { + "type": "put", + "key": "tags+feature:11111111-1111-1111-1111-111111111111/aaaa-1111", + "value": ["HQ", "COMMAND"] + } + ] + } +} +``` + +### Step 4: Lock the HQ Element + +```json +{ + "type": "command", + "id": "cmd-4", + "payload": { + "action": "put", + "key": "locked+feature:11111111-1111-1111-1111-111111111111/aaaa-1111", + "value": true + } +} +``` + +### Step 5: Hide Bravo Team + +```json +{ + "type": "command", + "id": "cmd-5", + "payload": { + "action": "put", + "key": "hidden+feature:11111111-1111-1111-1111-111111111111/aaaa-3333", + "value": true + } +} +``` + +### Step 6: Update Alpha Team Position + +```json +{ + "type": "command", + "id": "cmd-6", + "payload": { + "action": "put", + "key": "feature:11111111-1111-1111-1111-111111111111/aaaa-2222", + "value": { + "type": "Feature", + "name": "Alpha Team", + "geometry": { "type": "Point", "coordinates": [16.40, 48.23] }, + "properties": { "sidc": "SFGPUCI----D---", "t": "A" } + } + } +} +``` + +### Step 7: Fly to the Layer Area + +```json +{ + "type": "view", + "id": "view-1", + "payload": { + "action": "flyto", + "center": [16.38, 48.21] + } +} +``` + +### Step 8: Remove a Tag from the Layer + +```json +{ + "type": "command", + "id": "cmd-7", + "payload": { + "action": "put", + "key": "tags+layer:11111111-1111-1111-1111-111111111111", + "value": ["BLUE", "TRACKING"] + } +} +``` + +### Step 9: Show Bravo Team Again + +```json +{ + "type": "command", + "id": "cmd-8", + "payload": { + "action": "del", + "key": "hidden+feature:11111111-1111-1111-1111-111111111111/aaaa-3333" + } +} +``` + +### Step 10: Unlock HQ Element + +```json +{ + "type": "command", + "id": "cmd-9", + "payload": { + "action": "del", + "key": "locked+feature:11111111-1111-1111-1111-111111111111/aaaa-1111" + } +} +``` + +--- + +## Test Server + +ODIN includes a test server for development and testing at `tools/nido-test-server.js`. + +### Starting the Server + +```bash +node tools/nido-test-server.js [port] +``` + +Default port is 9000. + +### Available Commands + +| Command | Description | Example | +|---------|-------------|---------| +| `query ` | Query data by prefix | `query layer:` | +| `put ` | Create or update data | `put layer:test {"name":"Test"}` | +| `del ` | Delete data | `del layer:test` | +| `flyto ` | Fly to coordinates | `flyto 16.37 48.21` | +| `getview` | Get current view state | `getview` | +| `quit` | Exit server | `quit` | + +### Example Session + +``` +$ node tools/nido-test-server.js + +🚀 NIDO Test Server running on ws://localhost:9000 + +Waiting for ODIN to connect... + +✅ ODIN connected! + +[12:34:56] 📡 CONNECTED + Project: My Project (a1b2c3d4-...) + ODIN Version: 3.1.0 + Client ID: xyz-123-... + +> query layer: +📤 Sent query for prefix: layer: + +[12:34:58] 📋 Query qry-1 result: 2 items + layer:abc-123: {"name":"Operations"} + layer:def-456: {"name":"Intelligence"} + +> flyto 16.37 48.21 +📤 Sent view command: flyto + +[12:35:02] ✅ Command view-1 succeeded + +> quit +Goodbye! +``` + +--- + +## Appendix: SIDC Codes + +Features use Symbol Identification Codes (SIDC) based on MIL-STD-2525C. Common examples: + +| SIDC | Description | +|------|-------------| +| `SFGPUCI----D---` | Friendly Infantry Unit | +| `SFGPUCIZ---D---` | Friendly Mechanized Infantry | +| `SFGPUCA----D---` | Friendly Armor Unit | +| `SFGPUH----H---` | Friendly Headquarters | +| `SHGPUCI----D---` | Hostile Infantry Unit | +| `GFGPGLB----K---` | Boundary Line | +| `GFGPGAA----K---` | Assembly Area | + +For a complete reference, see the MIL-STD-2525C specification. diff --git a/docs-src/docs/reference/configuration.md b/docs-src/docs/reference/configuration.md new file mode 100644 index 00000000..ddcd632b --- /dev/null +++ b/docs-src/docs/reference/configuration.md @@ -0,0 +1,28 @@ +# Configuration + +ODIN can be configured through environment variables. + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `ODIN_SELF_UPDATE` | `1` | Set to `0` to disable automatic update checks | +| `NOMINATIM_URL` | `https://nominatim.openstreetmap.org/search` | URL of the Nominatim server for place search. Set this when using a self-hosted Nominatim instance in offline environments. | + +## Self-Hosted Search + +If you operate ODIN without internet access, you can host your own [Nominatim](https://nominatim.org/release-docs/latest/admin/Installation/) server for the "Search for places" feature: + +```bash +export NOMINATIM_URL=http://your-server:8080/search +``` + +## Self-Hosted Map Tiles + +For offline map tiles, use [mbtileserver](https://github.com/consbio/mbtileserver): + +1. Download `.mbtiles` files for your region +2. Start mbtileserver: `mbtileserver --dir /path/to/tiles` +3. Add the server URL in ODIN as a TileJSON Discovery service + +See [Tile Services](../features/tile-services.md) for detailed instructions. diff --git a/docs-src/docs/reference/keyboard-shortcuts.md b/docs-src/docs/reference/keyboard-shortcuts.md new file mode 100644 index 00000000..0eb2bade --- /dev/null +++ b/docs-src/docs/reference/keyboard-shortcuts.md @@ -0,0 +1,35 @@ +# Keyboard Shortcuts + +## General + +| Shortcut | Action | +|----------|--------| +| ++ctrl+n++ | Create new item (followed by type key) | +| ++ctrl+n++ then ++t++ | Create new tile service | +| ++ctrl+z++ | Undo | +| ++ctrl+shift+z++ | Redo | +| ++ctrl+c++ | Copy selected features | +| ++ctrl+v++ | Paste features | +| ++delete++ | Delete selected features | + +## Map Navigation + +| Shortcut | Action | +|----------|--------| +| Scroll wheel | Zoom in/out | +| Click + drag | Pan the map | +| ++shift++ + drag | Zoom to rectangle | + +## Drawing + +| Shortcut | Action | +|----------|--------| +| Single click | Place vertex | +| Double click | Finish drawing | +| ++alt++ + click vertex | Remove vertex (during editing) | + +## Search + +| Shortcut | Action | +|----------|--------| +| ++ctrl+f++ | Search for places (Nominatim) | diff --git a/docs-src/mkdocs.yml b/docs-src/mkdocs.yml new file mode 100644 index 00000000..2d3297f9 --- /dev/null +++ b/docs-src/mkdocs.yml @@ -0,0 +1,84 @@ +site_name: ODIN Documentation +site_description: Documentation for ODIN — Open Source Command & Control Information System +site_url: https://odin.syncpoint.io/docs/ +repo_url: https://github.com/syncpoint/ODINv2 +repo_name: syncpoint/ODINv2 +edit_uri: edit/main/docs-src/docs/ + +theme: + name: material + palette: + - scheme: default + primary: indigo + accent: cyan + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - scheme: slate + primary: indigo + accent: cyan + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.tabs + - navigation.tabs.sticky + - navigation.sections + - navigation.expand + - navigation.top + - search.suggest + - search.highlight + - content.code.copy + - content.action.edit + icon: + repo: fontawesome/brands/github + font: + text: Inter + code: Roboto Mono + +plugins: + - search + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - tables + - attr_list + - md_in_html + - toc: + permalink: true + +nav: + - Home: index.md + - Getting Started: + - Overview: getting-started/overview.md + - Installation: getting-started/installation.md + - Core Concepts: getting-started/core-concepts.md + - Features: + - Military Symbology: features/symbology.md + - Shapes: features/shapes.md + - MGRS Graticule: features/mgrs-graticule.md + - Elevation Data: features/elevation-data.md + - Tile Services: features/tile-services.md + - Collaboration: features/collaboration.md + - Integration: + - NIDO API: integration/nido.md + - Live Data Sources: integration/live-data-sources.md + - Reference: + - Keyboard Shortcuts: reference/keyboard-shortcuts.md + - Configuration: reference/configuration.md + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/syncpoint/ODINv2 + - icon: fontawesome/brands/mastodon + link: https://matrix.to/#/#ODIN.Community:syncpoint.io + +copyright: Copyright © Syncpoint GmbH — Licensed under AGPL-3.0