# Nextspace Developer Documentation Every documentation page, concatenated. Urls are resolved for this account. # Your organization (/home) # Your organization Have a Nextspace account ID? Enter it below to load the related API Base URL. This is not your User ID but the ID of your organization's account. Don't have one? [Sign up](https://nextspace.com) here. Account ID `https://your-account-id.api.nextspace.host` # Get started with.. Something missing? Contact us so we know what parts of our documentation to improve. [HTTP API](/apientities)[Web development](/webgettingstarted)[Python development](/pythongettingstarted)[Mobile development](/reactnativeexpo)[Custom Plugins](/webnavigatorplugins)[Embed widgets](https://embed.nextspace-dev.net) # The basics of Nextspace Entity Type Describe what your 'things' are and what properties they should have using Entity Types. The description of the properties is called the "Data Schema". ![](/res/home/section_entity_type.png) Entity Make a record of whatever Entity Types you've defined. This can be anything from a Building, Person or Work Order. After creation, you can associate documents, media, external links and more with these records. ![](/res/home/section_entity.png) Nextspace applications let you view and edit your data in 2D, 3D, tabular form, and charts as a few examples. Create your own applications or custom-code Plugins using the Nextspace API, libraries, embedded widgets to fit your needs. ![](/res/home/section_visualization.png) Project Views and Bookmarks Create a Project View to manage what data you want to work with, Menu Items provide a switchboard-like interface to turn things on and off. Use Bookmarks to create snapshots of your scenes so you can annotated and share them. The legacy terminology for Bookmarks is Slides. You will notice this when working our API and libraries. ![](/res/home/section_project_views.png) Tilesets Optimize loading performance of large sets of data through Tilesets. They bundle data based on their geospacial positions and detail to efficiently stream what's needed based on the viewport. ![](/res/home/section_tileset.png) --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/home # Terminology (/terminology) # Terminology To avoid confusion in other sections of the documentation, we will define some common terms here. Account ID Also referred to as Client Account ID, this is the identifier for your client account. It is not your personal user account that you use to login. To figure out what your account ID is, you can refer to your Nextspace account links. For example, if your account link is: `https://my-account.nextspace.host/` then your account ID is `my-account`. Session ID Also referred to as ssid, this is the identifier for your session. Send it as a bearer token to authenticate requests to the Nextspace API: `Authorization: Bearer `. The same header also accepts an access token. Environment Also referred to as Env, this is the identifier used to differentiate between different versions of the Nextspace services. The most common two you will work with are `UAT` and `PROD`. As of writing this, we recommend using `UAT` for development as production is significantly out of date. `DEV` and `STG` are for Nextspace developers only. Please ignore them. Entity Entities are the core of Nextspace. An Entity is a record that can represent anything based on your own level of categorization, attribution, and tagging. Examples of Entity records can be a certain building, a person, or a vehicle. Below is a minimal example of an Entity record. ```typescript interface IEntity { // Bruce is a code-word for the Nextspace API. // These are internal fields. Bruce: { ID: string, "EntityType.ID": string, "CreatedBy.User.ID": string, "Layer.ID": number[], Created: string; } ``` Entity Type Entity Types are used to categorize and describe Entities. For example you make a generic category 'Building' and describe what attributes you'd like any building in your account to have. Attributes such as 'Name', 'Address', 'Owner', etc. Tileset Tilesets can be thought of as 'optimized for rendering' graphics. They are most commonly used in Nextspace to render CAD, BIM, Point cloud, terrain, and other types of data. Project Views and Bookmarks Project Views are saved configurations for what is available to be rendered by a user. For example you may import a few files into your account, then setup a Project View to showcase or manage one or many of those imports. Project Views contain Bookmarks, these are saved Views of the data. Think of them like powerpoint slides that show where to look and what data is enabled. Menu Item A Menu Item is an 'action' that a Project View can perform. For example the action might be to load a Point Cloud, or to render buildings around the view-port. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/terminology # Attribution (/attribution) # Attribution Before publishing any applications please review the following attribution requirements for Nextspace and any sub-libraries that you use. Nextspace If you use the Nextspace API, or develop using Nextspace web libraries you must include a form of attribution that befits the use. If you use the Nextspace web library to render on a map then you must include **one** of the following logos on the viewer: [Download icon pack](/res/attribution/nextspace-logos-small.zip) Clicking the logo should reveal a link or directly link the user to: `https://nextspace.com`. If your application does not include a map, then you must include **one** of the following logos within your credits or attribution section: [Download icon pack](/res/attribution/nextspace-logos.zip) Alongside the logo you must include a link to the Nextspace website: `https://nextspace.com`. Cesium If you use Cesium World Terrain, or any Cesium Ion assets, then you must follow Cesium's attribution policies. You can read them here: [Cesium attribution](https://cesium.com/platform/cesium-ion/) Google If you use the Google 3D buildings Menu Item through the Nextspace web libraries then you must follow Google's attribution policies. You can read them here: [Google attribution](https://developers.google.com/maps/documentation/places/web-service/policies) --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/attribution # BRZ: Bruce Archive Format (/formatbrz) # BRZ: Bruce Archive Format A `.brz` file is a Nextspace archive format that packages an Assembly. It is a hierarchy of Entities at a single root Entity node for export, import, and interchange between systems. It carries the Entity attribute values, parent-child hierarchy with transforms, and embedded 3D geometry (GLB). BRZ files are produced by an [Export BRZ](/apiexportbrz) job and consumed by the [Import Assembly](/apiimportassembly) endpoint. ## Container format A BRZ file is a standard ZIP archive containing exactly one entry: a SQLite database named `model.db`. A ".db" file renamed to ".brz" is not considered valid. ```text archive.brz └── model.db ``` ## Complete schema The following is the exact creation script and schema explanation to use for new BRZ files. Copy-paste this to initialise a valid v7 database before inserting any data. ```sql PRAGMA application_id = 1112687682; PRAGMA user_version = 7; -- Attribute definitions. -- One row per distinct attribute that Entities in this archive can carry. CREATE TABLE Attribute ( id INTEGER PRIMARY KEY, -- Dot-separated grouping path, e.g. "dimensions.physical". -- "Bruce" is reserved for internal use. Never create custom attributes under it. category TEXT, name TEXT, -- 0=STRING 1=NUMERIC 2=BOOLEAN 3=TIMESTAMP 4=DATE dataType INT, -- Optional regex validation pattern. NULL or empty if no validation is needed. pattern TEXT, displayName TEXT, description TEXT ); -- One row per Entity. -- Keep hierarchy minimal. Prefer direct geometry-bearing Entities for key parts (e.g. DOOR, WALL_EAST, NAS). -- Avoid unnecessary wrapper or grouping nodes. -- It is preferred for leaf-nodes to have geometry, and parent nodes to be mostly organizational. CREATE TABLE Entity ( -- Local auto-increment. Used for all FK references within this file only. internalID INTEGER PRIMARY KEY, name TEXT, -- 0 = regular Entity instance, 1 = prototype definition (deprecated, don't create new prototypes). prototype BOOLEAN, -- UUID from the source system. Used for identity matching after an export from Nextspace. -- Leave as NULL for generated files that aren't coming from Nextspace. ID TEXT, -- References "EntityType.internalid". -- Leave as NULL if no defined Entity Types are being brought in. typeid INTEGER, -- Optional geohash string for spatial indexing. -- Leave as NULL if not applicable. geohash TEXT, -- Optional WGS84 geographic position. -- Leave as NULL if not applicable (ie: Entities are relatively positioned within the Assembly). longitude DOUBLE PRECISION, latitude DOUBLE PRECISION, altitude DOUBLE PRECISION ); -- Attribute values for Entities. One row per attribute value per Entity. -- Populate only the value column that matches the attribute dataType; leave the others NULL. -- STRING/BOOLEAN/TIMESTAMP/DATE values under ~240 chars -> StringValue. -- STRING values over ~240 chars -> LongTextValue. -- NUMERIC values (and booleans as 0.0/1.0) -> NumericValue. CREATE TABLE EntityAttribute ( -- References "Entity.internalID". entityID BIGINT, -- References "Attribute.id". AttributeID BIGINT, StringValue TEXT, LongTextValue TEXT, NumericValue DOUBLE PRECISION ); -- One row per GLB geometry asset. -- The binary data must be zlib-compressed (RFC 1950); the importer always decompresses before use. -- GLB geometry MUST use Y-up (the glTF standard). ParentRelationship transforms must also be in Y-up to match. -- See the "GLB requirements" section below for mandatory content rules. CREATE TABLE Geometry ( id INTEGER PRIMARY KEY, -- e.g. "model/gltf-binary" type TEXT, -- UUID from the source system. -- Leave as NULL for generated files that aren't coming from Nextspace. persistentID TEXT, -- Deprecated field. -- Always set this to NULL for generated files as it'll prioritize checking filepath over data. filepath TEXT, -- Vertex / face / line counts. Set to 0 if unknown. nverts INT, nfaces INT, nlines INT, -- Axis-aligned bounding box of the geometry in its local space. minx DOUBLE PRECISION, miny DOUBLE PRECISION, minz DOUBLE PRECISION, maxx DOUBLE PRECISION, maxy DOUBLE PRECISION, maxz DOUBLE PRECISION, -- Uncompressed byte size of the GLB. size DOUBLE PRECISION, -- CRC32 of the uncompressed GLB bytes. Used by the server for deduplication. hash INTEGER, -- Hash algorithm version. Always 1 (CRC32). hashVersion SMALLINT, -- Zlib-compressed GLB bytes (RFC 1950: 2-byte zlib header + DEFLATE payload + 4-byte checksum). -- Must not be stored uncompressed or as raw DEFLATE without the zlib wrapper. -- In Python use: zlib.compress(glb_bytes). -- In Node.js use: zlib.deflateSync(glbBuffer) (use deflateSync, NOT deflateRawSync). data VARBINARY ); -- Defines the parent-child hierarchy and the local transform of each child. -- Every non-root Entity must have exactly one row here. -- Shallow hierarchies are recommended. If a part is in the BOM, attach geometry to that Entity directly. CREATE TABLE ParentRelationship ( -- Both reference "Entity.internalID". parentID BIGINT, childID BIGINT, -- Zero-based ordering index among siblings. childNumber INT, -- Local translation in Y-up space (matching the GLB coordinate system). -- e.g. px = right, py = up, pz = toward viewer. px DOUBLE PRECISION, py DOUBLE PRECISION, pz DOUBLE PRECISION, -- Local rotation as a unit quaternion. Identity = (0, 0, 0, 1). qx DOUBLE PRECISION, qy DOUBLE PRECISION, qz DOUBLE PRECISION, qw DOUBLE PRECISION, -- Local scale per axis. Identity = (1, 1, 1). sx DOUBLE PRECISION, sy DOUBLE PRECISION, sz DOUBLE PRECISION ); -- Links instance Entities to their prototype. -- Only needed when multiple Entities share the same geometric definition. -- Deprecated and not needed for newly generated files. CREATE TABLE PrototypeRelationship ( -- Both reference "Entity.internalID". instanceID BIGINT, prototypeID BIGINT ); -- Optional material references for a geometry row. -- Can be left empty for generated files. -- Simple explicit materials are recommended over vertex colors alone. -- Window/glazing (and similar) materials should be mostly transparent. CREATE TABLE GeometryMaterial ( id INTEGER PRIMARY KEY, -- References "Geometry.id". geometryID BIGINT, -- Material slot path within the GLB. path TEXT, materialReference TEXT ); -- Alternative to embedding geometry in "Geometry.data": points to a downloadable URL instead. -- Only populate this if "Geometry.data" is NULL (e.g. for very large assets). -- Can be left empty for generated files that embed geometry directly. CREATE TABLE GeometryRemoteURL ( -- References "Geometry.id". geometryID BIGINT PRIMARY KEY, url TEXT ); -- Not read by the current importer. Can be left empty. CREATE TABLE EntityRemoteURL ( -- References "Entity.internalID". entityID BIGINT PRIMARY KEY, url TEXT ); -- Metadata about the account that exported this file. -- Leave empty if generating files outside of Nextspace. CREATE TABLE AccountSettings ( accountID TEXT, accountName TEXT, -- e.g. "prod", "uat", "dev". environment TEXT ); -- Generic key/value store. -- Leave empty if generating files outside of Nextspace. CREATE TABLE Parameter ( name TEXT PRIMARY KEY, value TEXT ); -- Defines the root Entity of the assembly. -- At least one row is required; the importer uses this to locate the top of the hierarchy. CREATE TABLE Component ( id INTEGER PRIMARY KEY, name TEXT, -- References "Entity.internalID" of the root entity. rootEntityID BIGINT ); -- Associates Entities with geometry at specific levels of detail. -- Add one row per Entity that has geometry. -- LODs should be marked with LODCategory = "GLB" and LODGroup = "DEFAULT". CREATE TABLE EntityLOD ( id INTEGER PRIMARY KEY, -- References "Entity.internalID". entityID BIGINT, -- References "Geometry.id". geometryID BIGINT, -- 0 = highest detail. level INT, -- "GLB" for primary geometry. "BOUNDS" for a bounding-box proxy mesh. LODCategory TEXT, -- Optional grouping label. NULL is fine. LODGroup TEXT ); -- Entity Type definitions used by Entities in this archive. CREATE TABLE EntityType ( -- Local auto-increment. Used for FK references within this file only. internalid INTEGER PRIMARY KEY, -- UUID from the source system. Used for identity matching after an export from Nextspace. -- Leave as NULL for generated files that aren't coming from Nextspace. id TEXT, name TEXT, description TEXT, -- 1 = reject attributes not in this schema on import. 0 = allow extras. isStrictSchema INTEGER ); -- Maps attributes to Entity Types, forming a typed schema. CREATE TABLE SchemaAttribute ( id INTEGER PRIMARY KEY, -- References "EntityType.internalid". typeID INTEGER, -- References "Attribute.id". attributeID INTEGER, -- Display ordering index. attributeNumber INTEGER ); -- Required indexes. CREATE UNIQUE INDEX IDX_Geometry_FilePath ON Geometry (filepath); CREATE UNIQUE INDEX UIDX_Geometry_Material ON GeometryMaterial (geometryID, path); CREATE INDEX IDX_Geometry_Hash ON Geometry (hash); CREATE UNIQUE INDEX IDX_Entity_ID ON Entity (ID); ``` ## Inserting geometry correctly The most common mistake when generating BRZ files is populating the `Geometry.filepath` column. When that column is non-empty the importer tries to read a file from disk instead of reading the embedded blob, which silently drops all geometry. Always set `filepath` to `NULL`. ```sql -- Correct: filepath is NULL, data contains zlib-compressed GLB bytes. INSERT INTO Geometry ( id, type, persistentID, filepath, -- MUST be NULL nverts, nfaces, nlines, minx, miny, minz, maxx, maxy, maxz, size, hash, hashVersion, data ) VALUES ( 1, 'model/gltf-binary', NULL, NULL, -- No filename 24, 12, 0, -1.0, -1.0, 0.0, 1.0, 1.0, 2.5, 2048, 305419896, 1, X'789C...' -- zlib.compress(glb_bytes) ); -- Then link the Entity to this Geometry via EntityLOD: INSERT INTO EntityLOD (id, entityID, geometryID, level, LODCategory, LODGroup) VALUES (1, 2, 1, 0, 'GLB', 'DEFAULT'); ``` ## GLB requirements Each GLB embedded in the Geometry table must satisfy the following rules. Missing any of these will cause import failures, Tileset generation crashes, or models rendering incorrectly. | Requirement | Description | | --- | --- | | At least one material | Every GLB must define at least one material using pbrMetallicRoughness. A simple default is fine (e.g. baseColorFactor [0.8, 0.8, 0.8, 1.0]). GLBs with zero materials will crash the Tileset generator. | | Material reference on every primitive | The material index on each mesh primitive must be set. Omitting it produces undefined behaviour in downstream processing tools. | | Vertex normals (NORMAL) | Every mesh primitive must include a NORMAL accessor alongside POSITION. Without normals, lighting is broken and some processing tools may crash. | | Indexed triangles (mode 4) | Primitives must use indexed triangle mode. Non-indexed geometry or other modes (lines, points) are not reliably supported. | | Y-up orientation | Geometry must be authored in Y-up space per the glTF specification. +X = right, +Y = up, −Z = forward. The Nextspace viewer (CesiumJS) expects Y-up and automatically converts to its internal Z-up scene coordinate system. Geometry authored in Z-up will appear sideways. | | Single-node structure | A single scene containing one root node with one child mesh node is the recommended GLB structure. Multi-node GLBs work but single-node is preferred for stable importer and Tileset generation behaviour. | ### Minimal valid material example ```json // Inside the glTF JSON of each GLB: { "materials": [ { "pbrMetallicRoughness": { "baseColorFactor": [0.8, 0.8, 0.8, 1.0], "metallicFactor": 0.0, "roughnessFactor": 0.9 } } ], "meshes": [ { "primitives": [ { "attributes": { "POSITION": 0, "NORMAL": 1 }, "indices": 2, "material": 0 } ] } ] } ``` ## Positioning and placement The `ParentRelationship` transform (px/py/pz, quaternion, scale) is **relative to the parent Entity**, not world-absolute. The importer converts each row into a 4×4 matrix stored on the child Entity. At render time the viewer computes the world matrix by chaining transforms up the hierarchy: `world = parent.world × child.local`. No collision detection, gravity, or snap-to-surface logic is applied. Objects will float, intersect, or sink if their translations are not computed from their actual geometry bounds. ### Flat vs. nested hierarchies In a **flat hierarchy** (every Entity is a direct child of the root), the root typically has an identity transform, so each child's px/py/pz values are effectively world-space coordinates. In a **nested hierarchy** (e.g. monitor is a child of desk), each child is positioned relative to its parent. A monitor at `py = 0.76` under a desk means "0.76 m above the desk's local origin", not 0.76 m above the world floor. ### Recommended mesh origin convention For generated assets, place each mesh origin at the **centre of its base contact surface** so that `miny = 0` for floor-standing objects. This simplifies placement: in a flat hierarchy, `py` is the world-space Y of the surface the object rests on. ### Placement from bounds To place a child so its base sits on a surface, use the Geometry bounding box: ```text child.py = surface_y - child.miny ``` In a flat hierarchy, `surface_y` is the world-space Y of the support surface. In a nested hierarchy it is the parent-local Y of the support surface (e.g. the parent's `maxy`). If meshes follow the `miny = 0` convention, the formula simplifies to `py = surface_y`. ### Worked example (flat hierarchy) All Entities below are children of the root Entity (1). Because the root has an identity transform, px/py/pz are world positions. ```sql -- Root Entity (1) has identity transform. -- All children are parented directly to it. -- Floor slab: miny = 0, maxy = 0.05. -- Rests at world origin. INSERT INTO ParentRelationship VALUES (1, 2, 0, 0.0, 0.0, 0.0, 0,0,0,1, 1,1,1); -- Desk: miny = 0, maxy = 0.76. -- Rests on the floor surface (floor.maxy = 0.05). INSERT INTO ParentRelationship VALUES (1, 3, 1, 0.0, 0.05, 0.0, 0,0,0,1, 1,1,1); -- Monitor: miny = 0, maxy = 0.41. -- Rests on the desk surface (desk.py + desk.maxy = 0.05 + 0.76 = 0.81). INSERT INTO ParentRelationship VALUES (1, 4, 2, 0.0, 0.81, 0.0, 0,0,0,1, 1,1,1); -- Chair: miny = 0, maxy = 1.0. -- Rests on the floor surface, offset sideways. INSERT INTO ParentRelationship VALUES (1, 5, 3, -0.6, 0.05, 0.4, 0,0,0,1, 1,1,1); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/formatbrz # JSON: Markup (/formatjsonmarkup) # JSON: Markup A Markup is a JSON object stored inside a Bookmark record. It describes a set of 2D screen overlays and 3D world annotations that are rendered on top of the Cesium viewer when the bookmark is activated. Markups are composed of **Divs** (HTML text boxes and shapes), **Lines** (SVG connector / callout lines), and **Cesium objects** (polygons, polylines, circles drawn in 3D space). The most commonly used elements are **Divs** and **Callouts** (a Div + Line pair). Markup files use the file extension ".bmarkup" but are actually JSON text files. They can be exported/imported from the Markup interface in Navigator. ## Top-level structure The root object carries arrays for each element category and records the original screen dimensions the markup was authored against. The renderer scales elements proportionally when the viewer is a different size. ```typescript interface IMarkup { // Array of HTML div overlays (text boxes, shapes, callout boxes). divs?: IDiv[]; // Array of SVG vector elements (lines and free-paint strokes). vectors?: (ILine | IPaint)[]; // Array of 3D Cesium entities (polygons, polylines, circles, etc). cesiumObjects?: (IPolygon | IPolyline | ICircle | IWalkscore)[]; // Original screen dimensions the markup was authored at. // The renderer uses these to scale elements to the current viewer size. width?: number; height?: number; // Default = true. Set to false to hide the entire markup. visible?: boolean; } ``` ## Base element Every markup element (divs, lines, cesium objects) extends a common base that carries an `id` and optional metadata. The `id` must be unique within the markup and is used for cross-referencing (e.g. a line snapping to a div). ```typescript interface IElement { // Unique id of the element within the markup. id: string; // Optional human-readable label shown in editing screens. name?: string; // Default = true. visible?: boolean; // Screen dimensions when this element was originally drawn. // Used with the markup-level width/height to calculate scaling. screenWidth?: number; screenHeight?: number; } ``` ## Div A Div is the primary building block. It renders as an absolutely-positioned HTML box on the viewer overlay. Divs are used for text boxes, info panels, image tiles, and callout labels. ### Sizing The div itself has a fixed pixel `width` and `height`. Inside the div, content is laid out as **rows**. Each row has a `height` expressed as a **percentage** of the div height (all row heights should add up to 100). Within a row, each cell has a `width` expressed as a **percentage** of the row width (all cell widths in a row should add up to 100). For example, two equal columns in a single row would each have `width: 50`. ### Anchoring & positioning The `position` field controls how the div is anchored to the screen. It determines which corner of the viewport the div's coordinates are measured from: | Position | Coordinates | Description | | --- | --- | --- | | TopLeft | left, top | Measured from the top-left corner. This is the default. | | TopRight | right, top | Measured from the top-right corner. | | BottomLeft | left, bottom | Measured from the bottom-left corner. | | BottomRight | right, bottom | Most commonly used anchor. The bottom-right area typically has the most available space. A typical offset is right: 120, bottom: 12. | | "fixed" | | The div is not shifted by UI panel offsets (sidebars, toolbars). | When the viewport resizes, the renderer recalculates the div position relative to its anchor corner so it stays in the intended screen region. Divs anchored to `BottomRight` will stay pinned to the bottom-right regardless of window size. ### Colours All colour fields (`backgroundColor`, `fontColor`, `borderColor`, etc.) accept standard CSS colour strings, hex (`#1d70e7`), `rgba(29,112,231,0.75)`, or named colours. ```typescript // Anchor side enum. enum EAnchorDivSide { TopLeft = "TopLeft", BottomLeft = "BottomLeft", BottomRight = "BottomRight", TopRight = "TopRight" } interface IDiv extends IElement { // "default" for a regular div, "callout" for one anchored in 3D via a line. type?: "default" | "callout"; // Content laid out as rows of cells (see IRow / IRowItem below). contentRows: IRow[]; // CSS box shadow. boxShadow?: IBoxShadow; // Border radius in pixels (or percent if borderUsingPercent is true). borderRadius?: number; borderUsingPercent?: boolean; // CSS border colour string. borderColor?: string; borderWidth?: number; // Position offsets in pixels, interpreted relative to the anchor side. // Use the pair that matches your anchor: left/top, right/top, left/bottom, or right/bottom. left?: number; top?: number; right?: number; bottom?: number; // Fixed pixel dimensions of the div box. width: number; height: number; // Screen anchor. Determines which corner coordinates are relative to. // Default is "TopLeft". Most common for info panels is "BottomRight". position?: EAnchorDivSide | "fixed" | "absolute"; // CSS colour strings for default text styling. backgroundColor?: string; fontColor?: string; fontSize?: number; // Maximum camera distance (meters) at which this div is visible. // Useful for callouts that should disappear when zoomed out. visibleMaxDistance?: number; } ``` ## Row & cell Div content is a list of rows. Each row contains cells. Row heights and cell widths are percentages that should each total 100 within their parent. ```typescript interface IRow { // Cells within this row. items: IRowItem[]; // Height of the row as a percentage of the div height. All rows should sum to 100. height: number; } // Cell content type determines rendering behaviour. enum EItemType { Default = "DEFAULT", // Plain text content. Embed = "EMBED", // Raw HTML content via embedRaw. Icon = "ICON", // Font-awesome icon via icon field. Image = "IMAGE", // Background image. None = "NONE" // No content, shape only. } interface IRowItem { // Plain text to display (auto-cleaned). text?: string; // Raw HTML when contentType is "EMBED". embedRaw?: string; // Font-awesome icon class when contentType is "ICON". icon?: string; // Action to perform when this cell is clicked. action?: IItemAction; // Background image for this cell. bgImage?: { FileID?: string; }; // CSS colour strings. backgroundColor?: string; fontColor?: string; fontSize?: number; // CSS text alignment. textAlign?: string; verticalAlign?: string; // Padding. Prefer innerPadding (CSS shorthand) over the deprecated numeric padding. innerPadding?: string; padding?: number; textShadow?: ITextShadow; // Content type. Defaults to "DEFAULT". contentType?: EItemType; // Width of this cell as a percentage of the row width. // All cells in a row should sum to 100. width?: number; } ``` ## Callout A Callout is not a separate element type, it is a **Div with type: "callout"** paired with a **Line** whose end-snap references the div's `id`. The line's start-snap holds a 3D world position (the anchor point in the scene), and its end-snap attaches to a side of the div. This is what makes the callout "float" in 3D space, the line projects from a real-world coordinate to the screen-space div. Common defaults: the line is typically white with `brushSize: 3` and `angled: true`. The div usually has a semi-transparent background like `rgba(29,112,231,0.75)` with white text. ```typescript // Example: a callout attached to a 3D position. // 1. Create the div with type "callout". const calloutDiv: IDiv = { id: "callout-1", type: "callout", width: 160, height: 40, contentRows: [{ height: 100, items: [{ text: "Pump Station A", width: 100, backgroundColor: "rgba(29,112,231,0.75)", fontColor: "white", fontSize: 13, textAlign: "center", verticalAlign: "center" }] }], borderRadius: 8, borderColor: "white", borderWidth: 1 }; // 2. Create a line whose end-snap references the div. const calloutLine: ILine = { id: "callout-line-1", pathType: "LINE", path: null, transform: null, brushColor: "rgba(255,255,255,0.75)", brushSize: 3, angled: true, snaps: [ { // Start: anchored to a 3D world position. // lat/lon are in degrees, altitude is absolute height in meters. side: 0, // EAnchorLineSide.Start position: { latitude: 40.7128, longitude: -74.0060, altitude: 0 } }, { // End: attached to the left edge of the callout div. side: 1, // EAnchorLineSide.End id: "callout-1", snapPosition: 5 // EAnchorDivSide.CenterLeft } ] }; ``` ## Line A Line is an SVG-based vector rendered on the 2D overlay. Lines can connect two screen positions, two 3D world positions, or snap to divs. When a snap references a div `id`, the line end follows that div as it moves. ```typescript enum ELineHead { None = "NONE", Circle = "CIRCLE", Arrow = "ARROW" } interface ILine extends IElement { pathType: "LINE"; // SVG path string (auto-generated when using snaps). path: string; // SVG transform string. transform: string; // Snap points for each end of the line. snaps: IAnchor[]; // CSS colour of the line. brushColor?: string; // Stroke width in pixels. brushSize?: number; // If true, the line angles towards its destination instead of going straight. angled?: boolean; // Line-head style (arrow, circle, or none). brushHead?: ELineHead; // CSS colour of the outline. outlineColor?: string; } // Determines which side of a div an anchor snaps to. enum EAnchorDivSide { TopLeft = 0, TopRight = 1, BottomLeft = 2, BottomRight = 3, Center = 4, CenterLeft = 5, CenterRight = 6, CenterTop = 7, CenterBottom = 8 } enum EAnchorLineSide { Start = 0, End = 1 } interface IAnchor { // Id of a markup Div this anchor is attached to (makes the line follow the div). id?: string; // Which side of the div to snap to. snapPosition?: EAnchorDivSide; // Which end of the line this anchor represents. side?: EAnchorLineSide; // Visual head style at this end. brushHead?: ELineHead; // CSS fill colour. fill?: string; // Size of the head (arrow/circle). size?: number; // CSS stroke colour. stroke?: string; // 3D world position (Cartesian3 or Cartographic in degrees). position?: { x: number; y: number; z: number }; // 2D screen position. screenPos?: { x: number; y: number }; // If true, position is relative to the markup's parent entity. positionFromEntity?: boolean; } ``` ## Free-paint A free-form SVG stroke drawn on the overlay. Unlike lines, free-paints do not have snap anchors. ```typescript interface IPaint extends IElement { pathType: "PLAIN"; // SVG path string. path: string; transform: string; brushColor?: string; brushSize?: number; brushHead?: "NONE" | "CIRCLE" | "ARROW"; outlineColor?: string; } ``` ## Polygon A filled polygon rendered in 3D world space via Cesium. Positions are Cartesian3 or Cartographic (degrees) coordinates. ```typescript interface IPolygon extends IElement { type: "POLYGON"; // Array of 3D positions defining the polygon boundary. positions: { x: number; y: number; z: number }[]; // CSS colour of the fill. color?: string; // CSS colour of the outline. outlineColor?: string; // Outline width in pixels. outlineWidth?: number; // Extrusion height in meters (0 for flat). extrudedHeight?: number; // "BOTH" overlaps terrain and 3D models. "TERRAIN" overlaps terrain only. classificationType?: "BOTH" | "TERRAIN"; // Number of Chaikin smoothing passes. 0 = no smoothing. smoothen?: number; } ``` ## Polyline A line rendered in 3D world space via Cesium. Similar to polygon but renders as a stroke. ```typescript interface IPolyline extends IElement { type: "POLYLINE"; positions: { x: number; y: number; z: number }[]; color?: string; // Width in pixels. outlineWidth?: number; smoothen?: number; } ``` ## Circle A filled circle rendered in 3D world space via Cesium. ```typescript interface ICircle extends IElement { type: "CIRCLE"; // Center position in 3D world space. position: { x: number; y: number; z: number }; // Radius in meters. radius: number; color?: string; // Outline width in meters. outlineWidth?: number; // Extrusion height in meters. extrudedHeight?: number; outlineExtrusion?: number; outlineColor?: string; } ``` ## Cell actions Each cell in a div row can have an optional `action` that fires when clicked. ```typescript enum EActionType { None = "NONE", // Open a URL. Can open in new tab. OpenLink = "LINK", // Allow this cell to be used as a drag handle for the div. Draggable = "DRAGABBLE", // Close the markup. Close = "CLOSE", // Activate another Bookmark in the same Project View. OpenBookmark = "BOOKMARK" } interface IItemAction { type: EActionType; params: IActionLinkParams | IActionBookmarkParams | {}; } interface IActionLinkParams { URL?: string; openInNewTab?: boolean; closeMarkupOnOpen?: boolean; } interface IActionBookmarkParams { bookmarkID?: string; // Styling applied to the cell when the target Bookmark is active. activeBackgroundColor?: string; activeFontColor?: string; } ``` ## Example: Colour legend A common use case is a draggable colour legend anchored to the bottom-right of the viewer. The example below shows a single div with a title row and six category rows, each pairing a label cell with a colour swatch cell. ```json { "width": 1920, "height": 1008, "visible": true, "cesiumObjects": [], "vectors": [], "divs": [ { "id": "legend-001", "type": "default", "position": "BottomRight", "right": 120, "bottom": 15, "width": 200, "height": 280, "borderRadius": 8, "borderWidth": 0, "borderColor": "black", "borderUsingPercent": false, "screenWidth": 1920, "screenHeight": 1008, "boxShadow": { "horizontal": 2, "vertical": 2, "blur": 5, "spread": 3, "color": "rgba(0, 0, 0, 0.25)" }, "contentRows": [ { "height": 16, "items": [ { "width": 100, "contentType": "DEFAULT", "text": "Colour Legend", "textAlign": "center", "verticalAlign": "center", "fontSize": 13, "fontColor": "white", "backgroundColor": "rgba(1, 39, 73, 1)", "innerPadding": "5px 5px 5px 5px", "action": { "type": "DRAGABBLE", "params": {} } } ] }, { "height": 14, "items": [ { "width": 60, "contentType": "DEFAULT", "text": "Category A", "textAlign": "center", "verticalAlign": "center", "fontSize": 13, "fontColor": "black", "backgroundColor": "white", "innerPadding": "5px 5px 5px 5px" }, { "width": 40, "contentType": "DEFAULT", "text": "", "textAlign": "center", "verticalAlign": "center", "fontSize": 13, "fontColor": "black", "backgroundColor": "rgba(0, 184, 217, 1)", "innerPadding": "5px 5px 5px 5px" } ] }, { "height": 14, "items": [ { "width": 60, "contentType": "DEFAULT", "text": "Category B", "textAlign": "center", "verticalAlign": "center", "fontSize": 13, "fontColor": "black", "backgroundColor": "white", "innerPadding": "5px 5px 5px 5px" }, { "width": 40, "contentType": "DEFAULT", "text": "", "textAlign": "center", "verticalAlign": "center", "fontSize": 13, "fontColor": "black", "backgroundColor": "rgba(0, 200, 83, 1)", "innerPadding": "5px 5px 5px 5px" } ] }, { "height": 14, "items": [ { "width": 60, "contentType": "DEFAULT", "text": "Category C", "textAlign": "center", "verticalAlign": "center", "fontSize": 13, "fontColor": "black", "backgroundColor": "white", "innerPadding": "5px 5px 5px 5px" }, { "width": 40, "contentType": "DEFAULT", "text": "", "textAlign": "center", "verticalAlign": "center", "fontSize": 13, "fontColor": "black", "backgroundColor": "rgba(170, 0, 255, 1)", "innerPadding": "5px 5px 5px 5px" } ] }, { "height": 14, "items": [ { "width": 60, "contentType": "DEFAULT", "text": "Category D", "textAlign": "center", "verticalAlign": "center", "fontSize": 13, "fontColor": "black", "backgroundColor": "white", "innerPadding": "5px 5px 5px 5px" }, { "width": 40, "contentType": "DEFAULT", "text": "", "textAlign": "center", "verticalAlign": "center", "fontSize": 13, "fontColor": "black", "backgroundColor": "rgba(255, 171, 0, 1)", "innerPadding": "5px 5px 5px 5px" } ] }, { "height": 14, "items": [ { "width": 60, "contentType": "DEFAULT", "text": "Category E", "textAlign": "center", "verticalAlign": "center", "fontSize": 13, "fontColor": "black", "backgroundColor": "white", "innerPadding": "5px 5px 5px 5px" }, { "width": 40, "contentType": "DEFAULT", "text": "", "textAlign": "center", "verticalAlign": "center", "fontSize": 13, "fontColor": "black", "backgroundColor": "rgba(41, 121, 255, 1)", "innerPadding": "5px 5px 5px 5px" } ] }, { "height": 14, "items": [ { "width": 60, "contentType": "DEFAULT", "text": "Category F", "textAlign": "center", "verticalAlign": "center", "fontSize": 13, "fontColor": "black", "backgroundColor": "white", "innerPadding": "5px 5px 5px 5px" }, { "width": 40, "contentType": "DEFAULT", "text": "", "textAlign": "center", "verticalAlign": "center", "fontSize": 13, "fontColor": "black", "backgroundColor": "rgba(100, 221, 23, 1)", "innerPadding": "5px 5px 5px 5px" } ] } ] } ] } ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/formatjsonmarkup # OpenAPI documentation (/apiswagger) The complete Bruce API reference for this account is served as an OpenAPI 3 document. Fetch it directly instead of reading this page: https://{accountId}.api.nextspace-uat.net/api/swagger/json Every path in that document is relative to `https://{accountId}.api.nextspace.host`, which is also its `servers` entry. The human-facing version of this page renders that same document with Swagger UI. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apiswagger # Authentication (/apiauth) # Authentication The most basic form of authentication is to perform a user login HTTPS request and receive a token in the response. Send that token as a bearer token in the `Authorization` header on every request to the Nextspace API: ```bash Authorization: Bearer ``` The same header accepts a session token from a user login and a long-lived access token, so code that authenticates a request does not need to know which kind of token it holds. ### Login ```http POST https://guardian.nextspace.host/login ``` **Requires:** Nothing **Request body** ```typescript interface IPostBody { // Client Account ID. // This is your organization's ID. account: string; // Logic can be either your User ID, username, or email address. login: string; // Your user's password. password: string; } ``` **Example request body** ```json { "account": "my_account_id", "login": "jdoe", "password": "password123" } ``` **Response** ```typescript interface IResponse { // This is your token. // Send it as "Authorization: Bearer " on any request you make to the Nextspace API. ID: string; // User record associated with the login. User: { // The user's ID. ID: string; // The user's username. Login?: string; // The user's email address. Email?: string; // The user's full name. Fullname?: string; } } ``` **Javascript example** ```javascript const url = "https://guardian.nextspace.host/login"; const method = "post"; const token = "your-token"; const body = { "account": "my_account_id", "login": "jdoe", "password": "password123" }; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` To invalidate the token, perform a logout HTTPS request authenticated with that same token. ### Logout ```http POST https://guardian.nextspace.host/logout ``` **Requires:** - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". **Javascript example** ```javascript const url = "https://guardian.nextspace.host/logout"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apiauth # Entities (/apientities) # Entities Entities are the core of Nextspace. An Entity is a record that can represent anything based on your own level of categorization, attribution, and tagging. Examples of Entity records can be a certain building, a person, or a vehicle. ## Data model To avoid repeating the data model in the documentation, we'll cover it once here. Please refer to the terminology section for a definition if you're unsure what an Entity is. Below is a minimal example of an Entity record. Your custom attributes will sit alongside the Nextspace ones. ```typescript // @Warning this example is primarily focused at non-assembly Entities. // Assembly Entities have relative positions to their parent. interface IEntity { // Bruce is a code-word for the Nextspace API. // These are internal fields. "Bruce": { "ID": string, "EntityType.ID": string, "CreatedBy.User.ID": string, // Array of Entity Tag IDs. "Layer.ID": number[], // Created/updated date/time in ISO 8601 UTC. "Created": string; "Updated": string; "Location"?: { // Lat/long are in degrees. "latitude": number, "longitude": number // Altitude is in meters. // The Entity's Style will dictate what this is relative to. // For example the Style may say "relative-to-ground" which would mean- // this altitude value should be added to the ground elevation. "altitude"?: number }, "Boundaries"?: { // Lat/long are in degrees. "maxLatitude": number; "maxLongitude": number; "minLatitude": number; "minLongitude": number; // Altitude is in meters. "maxAltitude"?: number; "minAltitude"?: number; }, "Transform"?: { // H/P/R are in degrees. "heading"?: number, "pitch"?: number, "roll"?: number, // Scale is a multiplier. // This will be multiplied by the Entity Style scale as well. "scale"?: number }, // Outline on what parts of the Entity loaded from where. // Only available when there are >1 sources of data, or if the only source has something of interest. "Outline"?: { // Human readable name of the source. "Source.Name": string; // Indicates if this is the baseline source. // This means that the base list of attributes are sourced from here. // Other sources will be overlaid on top of this one. "Baseline"?: boolean; // Array of attribute paths that were sourced from here. // If Baseline is true, then this isn't specified and all unspecified attributes are sourced from here. "Attribute"?: string[]; // Indicates if this source is editable. // If false, inputs for related attributes will be disabled. "Editable": boolean; // If the record itself or attributes were sourced from here. "Kind": "ATTRIBUTE" | "ENTITY"; // ID of the source, if any. "Source.ID"?: number; // Entity Type source ID, if any. "EntityType.Source.ID"?: number; // Related DateTime if any (for Historic data). "DateTime"?: string; // Related Scenario record if any. "Scenario"?: string | number; // Related linking attribute. // Eg: for historic data this is the date-time key, for sources it's the FK. "ForeignKey"?: string; }[]; }, // There can be any number of custom attributes defined by you. "MyAttribute": string, "MyOtherAttribute": { "MySubAttribute": number } } ``` [Entity data model](https://nextspace-limited.github.io/nextspace-web-bundle/docs/bruce-models/interfaces/Entity.IEntity.html) ## Requesting an Entity record The simplest way to retrieve an Entity record is through an HTTPS GET request. It is best to include a session header as Entities can be restricted through tags. ### Get a single record by ID ```http GET https://{accountId}.api.nextspace.host/v3/entity/{entity_id}?Type={entityTypeId}&$expand=location ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | The ID of the Entity to get. | | `Type={entityTypeId}` | query | no | Optional query parameter to view the Entity under a specific schema. | | `$expand=location` | query | no | (see the documentation page, this description is rich content) | **Response** ```typescript // Response is the same as the Entity definition. // It returns the Entity JSON directly. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/entity/{entity_id}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Requesting list by IDs You can also request a list of Entities by their IDs. ### Get list of records by IDs ```http POST https://{accountId}.api.nextspace.host/v3/getEntities ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "Filter": { ID: { // Array of IDs you'd like to request. in: string[] } }, // Unfortunately, this is required. // Set it to length of ID array, otherwise it will use a server default length. "PageSize": number } ``` **Example request body** ```json { "Filter": { "ID": { "in": [ "some-entity-id-1", "some-entity-id-2" ] } }, "PageSize": 2 } ``` **Response** ```typescript interface IResponse { "Items": IEntity[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/getEntities"; const method = "post"; const token = "your-token"; const body = { "Filter": { "ID": { "in": [ "some-entity-id-1", "some-entity-id-2" ] } }, "PageSize": 2 }; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Requesting list with an attribute query Below is a breakdown of how the basics of attribute queries work when requesting a list of Entities. Supported microservices will apply these filters when requesting external data (for example Maximo) by reversing the data mapping between the two sources. It is recommended to use DataLab for more advanced queries. ### Get list of records with attribute queries ```http POST https://{accountId}.api.nextspace.host/v3/getEntities ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "Filter": { // Attribute path segments are separated by a forward slash. Eg: 'address/city'. "an-attribute-path": { "an-operator": "a-value" } }, "PageSize": number } interface ISamples { "Filter": { // Nested string attribute containing "queen". // This is case-insensitive. "address/street": { "contains": "queen" }, // Nested string attribute equal to "auckland". "address/city": { "=": "auckland" }, // Numeric attribute between 0 and 100. "value-1": { "between": [0, 100] }, // Numeric attribute bigger than 100. "value-2": { ">": 100 }, // Date time attribute equal or after 2024-03-15T02:00:52Z. "founded-date": { // ISO 8601 format. ">=": "2024-03-15T02:00:52Z" }, // Date time attribute between 2024-03-15T02:00:52Z and 2024-03-15T02:00:52Z. "founded-date-2": { // ISO 8601 format. "between": ["2024-03-15T02:00:52Z", "2024-03-15T02:00:52Z"] }, // String attribute starting with "a". "something-1": { "startswith": "a" } // String attribute ending with "b". "something-2": { "endswith": "b" } // Boolean attribute equal to true. "is-active": { "=": true } }, "PageSize": 2 } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/getEntities"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Requesting list with a geographic query Below is how you can perform a basic geographic boundary query when requesting a list of Entities. ### Get list of records by IDs ```http POST https://{accountId}.api.nextspace.host/v3/getEntities ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "Filter": { "boundaries": { "intersects": [ // South (min latitude). 0, // North (max latitude). 0, // West (min longitude). 0, // East (max longitude). 0 ] } }, "PageSize": number } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/getEntities"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Updating and creating records You can request an update or create an Entity record by performing an HTTPS POST request with the Entity's data within the body. If the Entity ID is not provided, a new Entity will be created. ### Create or update an Entity record ```http POST https://{accountId}.api.nextspace.host/v3/entity/{entity_id}?Type={entityTypeId}&DataOverride={true/false} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | no | The ID of the Entity to update. If unspecified then a new Entity will be created | | `Type={entityTypeId}` | query | no | Required parameter if you're creating a record. This will specify the data schema to create the Entity under. | | `DataOverride={true/false}` | query | no | Overriding the record means that any unspecified attributes will be removed from the record. | **Request body** ```typescript // Body is the same as the Entity definition. // Pass the Entity JSON directly. ``` **Example request body** ```json { "Bruce": { "ID": "my_existing_entity_id" }, "MyAttribute": "My new value" } ``` **Response** ```typescript // Response is the same as the Entity definition. // It returns the Entity JSON directly. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/entity/{entity_id}"; const method = "post"; const token = "your-token"; const body = { "Bruce": { "ID": "my_existing_entity_id" }, "MyAttribute": "My new value" }; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Create or update an array of Entity records ```http POST https://{accountId}.api.nextspace.host/v3/entities?Type={entityTypeId}&DataOverride={true/false} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `Type={entityTypeId}` | query | no | Required parameter if you're creating a record. This will specify the data schema to create the Entity under. | | `DataOverride={true/false}` | query | no | Overriding the record means that any unspecified attributes will be removed from the record. | **Request body** ```typescript Items: IEntity[] ``` **Example request body** ```json { "Items": [ { "Bruce": { "ID": "my_existing_entity_id" }, "MyAttribute": "My new value" } ] } ``` **Response** ```typescript Items: IEntity[] ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/entities"; const method = "post"; const token = "your-token"; const body = { "Items": [ { "Bruce": { "ID": "my_existing_entity_id" }, "MyAttribute": "My new value" } ] }; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apientities # Entity Type (/apientitytype) # Entity Type Entity Types are used to categorize and describe Entities. For example you make a generic category 'Building' and describe what attributes you'd like any building in your account to have. Attributes such as 'Name', 'Address', 'Owner', etc. Entities must be assigned an Entity Type ID. ## Data model Below is a basic data model for an Entity Type record. When further documentation refers to `IEntityType` or just an Entity Type record, you can reference this data model. ```typescript interface IEntityType { // ID of the Entity Type. ID?: string; // Human readable name of the Entity Type. Name: string; // Description of the Entity Type. Description?: string; // An Entity record used for default values. Data?: Entity.IEntity; // If entity type access (and entity access) should be restricted. // When an entity type is restricted a user must have the "EntityType_" permission. IsAccessRestricted?: boolean; // The data schema defining expected attributes for Entities to have. DataSchema?: IDataSchema; // See 'Entity Type: Data Schema' documentation. // Default Style for the corresponding Entities. "DisplaySetting.ID"?: number; // Created/updated date/time in ISO 8601 UTC. Created: string; Updated: string; // ID of the parent Entity Type. // This is used for organization. // When a parent type is deleted, the child types are also deleted. "Parent.EntityType.ID"?: string; } ``` ## Entity Type requests. Below are the basic requests for managing Entity Type records. ### Get Entity Types ```http GET https://{accountId}.api.nextspace.host/entitytypes?typeId=type_id&parentEntityTypeID=type_id&expandSettings=true/false&search=text ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `typeId=type_id` | query | no | Filter for a specific Entity Type ID. Include multiple times to filter for multiple Entity Type IDs. Eg: typeId=1&typeId=2 | | `parentEntityTypeID=type_id` | query | no | Filter for Entity Types with a specific Parent Entity Type ID. Pass 'IS_NULL' to filter for Entity Types without a parent. | | `expandSettings=true/false` | query | no | If the response should include expanded settings such as the Data Schema. This is discouraged unless you have a strict filter. | | `search=text` | query | no | Search for Entity Types by name or description. | **Response** ```typescript interface IResponse { Items: IEntityType[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entitytypes"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` When requesting a list of Entity Types with any filter, the parent Entity Types are included in the response. ### Get Entity Type by ID ```http GET https://{accountId}.api.nextspace.host/entityType/{type_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{type_id}` | path | yes | Entity Type ID to retrieve the record for. | **Response** ```typescript // Response is the Entity Type record directly. // Refer to the IEntityType description at the top. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entityType/{type_id}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Create/Update Entity Type ```http POST https://{accountId}.api.nextspace.host/entityType/{type_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{type_id}` | path | yes | Entity Type ID to create or update. | **Response** ```typescript // Body is the Entity Type record directly. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entityType/{type_id}"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Delete Entity Type ```http DELETE https://{accountId}.api.nextspace.host/entityType/{type_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{type_id}` | path | yes | Entity Type ID to delete. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entityType/{type_id}"; const method = "delete"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` Deleting an Entity Type will also delete all Entities of that type, and all child Entity Types (recursively). --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apientitytype # Entity Type - Data Schema (/apientitytypedataschema) # Entity Type - Data Schema The Data Schema defines what attributes Entities in an Entity Type are expected to have. Entities may go against the schema and define their own attributes, however this means they won't be managed by the system and therefor lack care for data types, validation, and search optimizations. An attribute in the schema must have a key, name, and data type defined. Structure attributes will contain an array of child attribute definitions. We have an experimental API request for analyzing data to generate a schema and mapping you can learn more about [here](/apidatamapping#apidatamappingutils). ## Data model Below is a basic data model for a Data Schema record. When documentation refers to `IDataSchema` or just a Data Schema, you can reference this data model. ```typescript interface IDataSchema { Key: "root", Name: "Root", Structure: IAttribute[], Type: EAttributeType.Structure } export interface IAttribute { // The attribute JSON key. // It is recommended to avoid special characters and spaces (dash and underscore are fine). // It is also recommended to avoid these keys that have special internal meanings: // 'Bruce', 'ID', 'location', 'transform', 'geometry', 'boundaries', and 'position'. Key: string; // The attribute human readable name. Name?: string; // The attribute human readable description. Description?: string; // The attribute type. Type?: EAttributeType; // If this attribute is a structure, this is the structure definition. Structure?: IAttribute[]; // If this attribute is 'pinned' or not. // This indicates it should be displayed in tables and lists. IsImportant?: boolean; // The historic key (if any) this attribute is associated with. // This is a date-time attribute that drives the change for this attribute value. // Attribute path segments are separated by a forward slash. Eg: 'address/city'. HistoricKey?: string; } export enum EAttributeType { // Standard types. // Arbitrary text attribute. String = "String", // Floating point number attribute. Double = "Double", // Whole number attribute. Integer = "Integer", // iso8601 date time string. Datetime = "Datetime", // Group of attributes. Structure = "Structure", // Nextspace vector geometry. Geometry = "Geometry", // True/false attribute. Boolean = "Boolean", // Special types that help guide behavior. // These typically link to existing concepts, or allow UI to present the value/input differently. // String User ID. User = "User", // String Group ID. UserGroup = "UserGroup", // String Entity ID. Entity = "Entity", // String URL attribute. Url = "Url" } ``` ## Alternative schemas Typically, you manage an Entity Type's Data Schema directly through the Entity Type record. However, if you want manage an alternative Data Schema to transform the response data with then you can use the below endpoints. If you include the query param `schema={schema_id}` then the result Entities in an Entity or DataLab request will be mapped to your alternative schema. Note that the mapping is performed after record retrieval, so you can't use this to filter records. ### Get Alternative Schemas ```http GET https://{accountId}.api.nextspace.host/v1/entityType/{entity_type_id}/dataSchemas ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_type_id}` | path | yes | Entity Type ID to get the alternative schemas from. | **Response** ```typescript interface IResponse { Items: { // ID of this alternative schema. "Schema.ID": string; // ID of the related Entity Type. "EntityType.ID": string; // Human-readable name of this schema. "DisplayName": string; // Human-readable description of this schema. "Description": string; }[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v1/entityType/{entity_type_id}/dataSchemas"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Get Alternative Schema By ID ```http GET https://{accountId}.api.nextspace.host/v1/entityType/{entity_type_id}/dataSchema/{schema_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_type_id}` | path | yes | Entity Type ID to get the alternative schema from. | | `{schema_id}` | path | yes | Alternative Schema ID to get. | **Response** ```typescript interface IResponse { // ID of this alternative schema. "Schema.ID": string; // ID of the related Entity Type. "EntityType.ID": string; // Human-readable name of this schema. "DisplayName": string; // Human-readable description of this schema. "Description": string; // Alternative data schema from the default Entity Type one. "DataSchema": IDataSchema; // Read more about this in the Data Mapping documentation. "DataMapping": IDataMapping; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v1/entityType/{entity_type_id}/dataSchema/{schema_id}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Create / Update Alternative Schema ```http POST https://{accountId}.api.nextspace.host/v1/entityType/{entity_type_id}/dataSchema/{schema_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_type_id}` | path | yes | Entity Type ID to create/update the alternative schema for. | | `{schema_id}` | path | yes | Alternative Schema ID to create/update. | **Response** ```typescript interface IResponse { // ID of this alternative schema. // The ID something you provide yourself, unknown IDs will create a new schema record. // We typically generate a short alpha-numeric ID, but you could use an easy to remember string. "Schema.ID": string; // ID of the related Entity Type. "EntityType.ID": string; // Human-readable name of this schema. "DisplayName": string; // Human-readable description of this schema. "Description": string; // Alternative data schema from the default Entity Type one. "DataSchema": IDataSchema; // Read more about this in the Data Mapping documentation. "DataMapping": IDataMapping; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v1/entityType/{entity_type_id}/dataSchema/{schema_id}"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Delete Alternative Schema ```http DELETE https://{accountId}.api.nextspace.host/v1/entityType/{entity_type_id}/dataSchema/{schema_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_type_id}` | path | yes | Entity Type ID to delete the alternative schema from. | | `{schema_id}` | path | yes | Alternative Schema ID to delete. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v1/entityType/{entity_type_id}/dataSchema/{schema_id}"; const method = "delete"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apientitytypedataschema # Data Mapping (/apidatamapping) # Data Mapping This section will cover how Data Mapping is managed. This concept is used for file imports, data transformation, and mapping between different existing Data Schemas. ## Data model Data mapping is defined with this structure. If you exclude functions, it's a simple array of connections between the source and target Data Schemas. ```typescript interface IDataMapping { // Array of connections to define how data is mapped. "Connections": { // Indicates this connection is defining a foreign key. // This is a requirement for imports with an 'update' strategy. // Multiple keys can be defined as a foreign-key, it's recommended to define a single key for a stable result. "IsForeignKey": boolean; // The left side of the connection. // This is the source Data Schema or an output of a Data Mapping function. "Left": { // Set to 'out' to indicate an output from the source Data Schema. // For functions, set to a function's output value path defined in its documentation. "Key": "out" | string; // Type of panel we're getting the value from. "Panel": "left" | "function"; // Path to the attribute within the source panel. // Attribute path segments are separated by a forward slash. Eg: 'address/city'. // For functions, this would be the function's ID. "Path": string; }; // The right side of the connection. // This is the target Data Schema or input to a Data Mapping function. "Right": { // Set to 'in' to indicate an input to the target Data Schema. // For functions, set to a function's input value path defined in its documentation. "Key": "in" | string; // Type of panel we're setting the value to. "Panel": "right" | "function"; // Path to the attribute within the target panel. // Attribute path segments are separated by a forward slash. Eg: 'address/city'. // For functions, this would be the function's ID. "Path": string; }; }[]; // Array of referenced functions in the Data Mapping connections. "Functions": IFunction[]; // See the Function section for more details. } ``` ## Mapping functions Mapping functions let you augment the input value before assigning it. You can chain multiple functions together to create complex transformations. Mapping functions can have multiple inputs/outputs. For example you can concat or split a string. ```typescript interface IFunction { // A unique identifier for the function within the Data Mapping. // You can assign this as complex or simple as you want as long as it's unique within the Data Mapping. "ID": string; // The function to perform. // See documented functions for the different options. "Function" string; // Differs based on the function. // See individual function definitions for more details. "Settings": any; } ``` ## Mapping functions: Concatenate Combine multiple input attributes into a single output string. The value will be available if you use the connection Key: `Result` in combination with this function's ID as the connection path. ```typescript interface IFunction { "ID": string; "Function": "Concatenate"; "Settings": { // Array of inputs to concatenate. // If not specified, we assume "A" and "B" as two default inputs. // Set the right-side connection "Key" to one of these inputs to use them. "Inputs": string[]; // The delimiter to use between the inputs. // When not specified a space is used. // You can set an empty string to concatenate without a delimiter. "Delimiter": string; }; } ``` ## Mapping functions: Split Split an input string value into multiple outputs. The value's Key will be available as a string index of the split result. Eg: splitting `"Hello World"` by space would give you `"0"` as `"Hello"` and `"1"` as `"World"`. If the split fails for any reason, then the "0" output will be set to the original input value. Other outputs are set to empty strings. ```typescript interface IFunction { "ID": string; "Function": "Concatenate"; "Settings": { // This is set to an array of string indexes, eg: ["0", "1", "2"]. // This is not a requirement for the function to run. // It acts as a guide for the UI to draw the number of outputs and related connections. "Outputs": string[]; // Regex to split by. // Most of the time you can just enter plain-text, eg: " " or "," to split against. "splitRegex": string; }; } ``` ## Mapping functions: Replace Run a string replace on an input value to get a new value. Set the input value to the connection Key: `Input` to use this function. The value will be available if you use the connection Key: `Output` in combination with this function's ID as the connection path. ```typescript interface IFunction { "ID": string; "Function": "StringReplace"; "Settings": { // Regex to match against. // Most of the time you can just enter plain-text, eg: " " or "_" to replace. "replaceRegex": string; // The string to replace the matched value with. // This can be an empty string to remove the matched value. "replaceWith": string; }; } ``` ## Mapping functions: DateTime conversion. Specify how your date string should be interpreted so we can store a valid ISO 8601 string. If your strings are already in ISO 8601 format, you can skip this function and assign the value directly. We store all dates in UTC so specifying a timezone that isn't UTC will result in a conversion to UTC. ```typescript interface IFunction { "ID": string; "Function": "DateTimeConversion"; "Settings": { // Format to match. Eg: 'YYYY/MM/dd HH:mm:ss'. "format": string; // Optional timezone to specify if your data is not in UTC. // You can specify "UTC" as a default to avoid conversion. // This supports common timezone names, however it is highly recommended to use offsets instead. // Eg: '+13" or "-5". "timezoneCode"?: string; // Default value to output if the calculation fails or no input is provided. "defaultValue"?: string; // Set to true if you alternatively want to output the original value if the calculation fails. "defaultToOriginal"?: boolean; // Optional regex to match against to run a replace prior to interpreting the date. // Most of the time you can just enter plain-text, eg: " " or "," to split against. "replaceRegex"?: string; // Value to replace with if a regex is specified. // This defaults to an empty string. "replaceValue"?: string; }; } ``` ## Mapping functions: Calculate This is a general-use calculation function where you can run Javascript eval code to get a new value. The code will use $A, $B, etc as placeholders that will be replaced with your input values. The value will be available if you use the connection Key: `Result` in combination with this function's ID as the connection path. **WARNING:** Nulls are not currently handled gracefully. Ensure all your inputs are at least an empty string. ```typescript interface IFunction { "ID": string; "Function": "Calc"; // Array of inputs to concatenate. // If not specified, we assume "A" and "B" as two default inputs. // Set the right-side connection "Key" to one of these inputs to use them. // $ is prepended and used as a placeholder for the input value within the formula. "Inputs": string[]; // The formula to calculate. // Note that if a string is replaced, it won't be automatically quoted. // Eg: '$A + $B' will become 'abc + def' if $A and $B are strings. // Ensure to quote strings in the formula if needed. "Formula": string; } ``` ## API utilities We're working on improving our utilities to help generate these mappings. Here is an experimental endpoint you can use. ### Analyze Source ```http POST https://{accountId}.api.nextspace.host/source/analyze ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IRequest { // Array of JSON data samples to analyze. "DataSamples": any[]; // Optional source schema to use instead of generating one based on the data samples. // This is typically used when you make modifications in UI and want to re-analyze. "SchemaSource": IDataSchema; // See schema docs. // Optional target schema we want to suggest mappings into. // If not supplied, we'll assume it matches the source schema with minor modifications // Modifications are done to avoid a clash with internal attributes. "SchemaTarget"?: IDataSchema; // See schema docs. // Optional existing Data Mapping to supplement. "DataMapping"?: IDataMapping; } ``` **Response** ```typescript interface IResponse { // Number of samples that were analyzed. // Helps determine if any issues occurred by ones being skipped. // Lower number could also indicate that we hit a limit. "CountSampled": number; // Source and target schemas if they are available. // If not found, it's likely there was an issue generating them. "SchemaSource"?: IDataSchema; // See schema docs. "SchemaTarget"?: IDataSchema; // See schema docs; // Suggestions for Data Mapping connections. "DataMapping"?: IDataMapping; // Errors if any were encountered. "Errors"?: string[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/source/analyze"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apidatamapping # Entity Styles (/apientitystyles) # Entity Styles Entity Styles dictate how an Entity is rendered. Their settings can range from fixed values to dynamic values based on Entity properties. This article will cover how to manage Style records and descriptions about the related settings. ## Data model Below is the core data model for a Style. You can manage a record without populating the settings. Check the end of this article for sections related to the specific settings. ```typescript interface IStyle { // Unique ID for the Style. When unset and saved, one will be generated. ID: number; // User friendly name. Name: string; // The Style settings, this differs based on the type of Style it is. Settings: { // Settings are split up by the visual's type. // We will cover these below as separate case studies. pointStyle?: any, polygonStyle?: any, polylineStyle?: any, // 3D model styling is used for both Tileset Entities and 3D model Entities. modelStyle?: any }; // The type of Style. In this case we're covering Entity Styles. // When unset, we assume it is an Entity Style as it is the original type. Type: "ENTITY"; } ``` ## Requesting records You can request a list of all requests or an individual one. When a list is requested it'll exclude the settings JSON. ### Get a list of Styles ```http GET https://{accountId}.api.nextspace.host/ui.entityDisplaySettings ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Response** ```typescript // Returns an array of all Styles excluding the settings JSON. // Be sure to filter out Styles that are not of type ENTITY (reminder that null is also considered ENTITY). interface IResponse { Items: IStyle[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.entityDisplaySettings"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Get Style by ID ```http GET https://{accountId}.api.nextspace.host/ui.entityDisplaySetting/{style_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{style_id}` | path | yes | The ID of the Style to get. | **Response** ```typescript // Response is the same as IStyle definition above. // It returns the JSON directly. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.entityDisplaySetting/{style_id}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Create and update records You can create a record by performing a POST request to the same URL as the GET. If you specify the ID as '0' then it'll create a new record and return it. If you specify an existing ID then it'll update the record and return it. ### Create or update Style ```http POST https://{accountId}.api.nextspace.host/ui.entityDisplaySetting/{style_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{style_id}` | path | yes | The ID of the Style to update. Set as 0 to create a new record. | **Request body** ```typescript // Body should be the Style JSON as specified at the top of the article. ``` **Example request body** ```json { "ID": 0, "Name": "My new Style", "Settings": {}, "Type": "ENTITY" } ``` **Response** ```typescript // Response is the same as IStyle definition above. // It returns the JSON directly. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.entityDisplaySetting/{style_id}"; const method = "post"; const token = "your-token"; const body = { "ID": 0, "Name": "My new Style", "Settings": {}, "Type": "ENTITY" }; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Delete a record We don't provide any bulk deletion but you can delete by record ID. It follows the same pattern as before. ### Delete Style ```http DELETE https://{accountId}.api.nextspace.host/ui.entityDisplaySetting/{style_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{style_id}` | path | yes | The ID of the Style to delete. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.entityDisplaySetting/{style_id}"; const method = "delete"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Style calculator All Style settings run through a common Style calculator. So before we cover the specifics we'll run through information that is shared between any Style. Any setting that can be dictated by an Entity attribute will be set as an array of calculator items. When an Entity is rendered, the calculator will run through each item and apply the first item that resolves with a value. ```typescript // Available calculator item types. enum EValueType { // Color is a fixed css color value. Color = 0, // Input is a fixed or attribute value. // You can either specify an exact value, or an attribute path which will grab the value from the rendered Entity. Input = 1, // A gradient item is calculated using an Entity number attribute. // For example you can have a gradient that changes the color of an Entity based on its height. Gradient = 2, // A mapping item is calculated by an Entity string or number attribute. // For example you might have a region attribute and map those regions to different colours. Mapping = 3, // Tag color will use the first tag on the Entity that has a color. // For example if an Entity is tagged with "New Zealand" in green, and "Auckland city" as yellow, then it'll be green. TagColor = 4, // Random color will generate a random color for each Entity. // This is useful for debugging... Not recommended for production. RandomColor = 5 } // The base calculator item. // A Style property typically is set to an array of these. // The first to resolve with a value will be applied. interface IField { value: IGradientValue | IMappingValue | string | number; type: EValueType; } /// Gradient field. /// // A gradient item is calculated using an Entity number attribute. // For example you can have a gradient that changes the color of an Entity based on its height. interface IGradientValue { // This is an attribute path to load a value from the rendered Entity. // Attribute path segments are separated by a forward slash. Eg: 'address/city'. field: string; // This is a range of values to map the gradient to. // For example you can have a point at "0" then a point at "100". // If the attribute value is "50" then it'll be half way between the two points and a gradient between those two colours would be calculated. points: IGradientPoint[]; } interface IGradientPoint { // The value of this point within the gradient range. position: number; // CSS colour string. color: string; } /// Mapping field. /// // A mapping item is calculated by an Entity string or number attribute. // For example you might have a region attribute and map those regions to different colours. interface IMappingValue { // This is an attribute path to load a value from the rendered Entity. // Attribute path segments are separated by a forward slash. Eg: 'address/city'. field: string; // The attribute value will be compared with these options to find a match. values: IMappingOption[]; } interface IMappingOption { // This is a possible value to match against. // In the case of string values we will do direct comparisons. // In the case of number values you can specify ranges or direct values. // For example you can either specify "5" directly, or "1-5" as a range. fieldValue: string; // This is the result of a successful match. // If this Style is dictating a colour then this would be a css colour string. appliedValue: string; } ``` Alongside the visual settings we also let you set a setting called "altitudeOption". This dictates how the altitude is calculated for the Entity. For example you may want to place all the Entities on the ground so you'll set the altitude option to "ClampToGround". ```typescript enum EOptionId { // When set to absolute the terrain is ignored and the altitude is set to whatever is specified. Absolute = 0, // Altitude is set to the terrain. The Entity altitude is ignored. ClampToGround = 1, // Altitude is set as an offset from the terrain. // Eg: an altitude of 5m would be 5 meters above the ground at that spot. RelativeToGround = 2 } interface IAltitudeOption { id: EOptionId; // Backwards compatibility name. // Some of our older data exports require this set but for rendering in the Navigator this is unused. name?: "Absolute position" | "Clamp to ground" | "Reference to ground"; } ``` ## Point styling Within this section we'll cover what point Style settings are available and how to use them. We will cover the most supported settings. If you need more advanced settings then please contact us. Please refer to the 'Style calculator' section above for definitions that are not explained here. Some are shared between all the types of styling. ```typescript // Points come primarily as icons or pixel points. EPointType { // Point types are rendered at a specific pixel size. Point = "POINT", // Icons are points that render an image instead of a circle. Icon = "ICON" } interface IPointSettings { // The type of point. This helps dictate what settings are relevant. // The default when unspecific will be "POINT". Type?: EPointType; // The size in pixels of the point. // This is only relevant for "POINT" types. For "ICON" types the size is dictated by the icon image itself. // This must resolve in a number to be valid, otherwise it'll default to the application's default value. size?: IField[]; // The url to load an icon from. // This is only relevant for "ICON" types. iconUrl?: IField[]; // The scale factor of the icon. // This is only relevant for "ICON" types. // For example a 50x50 icon with a scale of 2 will render as 100x100 on the screen. // This must resolve in a number to be valid, otherwise it'll default to the application's default value. iconScale?: IField[]; // The colour of the point. // This is only relevant for "POINT" types. // This must resolve into a css colour string to be valid, otherwise it'll default to the application's default value. color?: IField[]; // This dictates what the altitude value of an Entity should mean. // For example if you set this to be clamp-to-ground then the altitude value will be ignored. // Default is clamped to ground. altitudeOption?: IAltitudeOption; } ``` Here is a point Style example. In this example only the colour is set and the rest use default values. ```typescript { color: [ // Mapping based on 'rent' attribute. { value: { field: "rent", values: [ { fieldValue: "£208", appliedValue: "rgba(50, 180, 50, 0.8)" } ] }, type: EValueType.Mapping }, // Gradient based on 'location' -> 'longitude' attribute. { value: { // If you're using the web libraries we recommend using the 'PathUtils' utility to wrap these for you. // Eg: BModels.PathUtils.Wrap(["location", "longitude"]); field: "\"location"/"longitude\"", points: [ { position: -1.099, color: "rgba(255, 0, 0, 0.8)" }, { position: -1.046, color: "rgba(255, 255, 255, 0.8)" } ] }, type: EValueType.Gradient }, // Fallback color in case the above doesn't work. { value: "rgba(255, 255, 255, 0.4)", type: EValueType.Color } ] } ``` ## Polyline styling Within this section we'll cover what polyline Style settings are available and how to use them. We will cover the most supported settings. If you need more advanced settings then please contact us. Please refer to the 'Style calculator' section above for definitions that are not explained here. Some are shared between all the types of styling. ```typescript interface IPolylineSettings { // The colour of the line. // This must resolve into a css colour string to be valid, otherwise it'll default to the application's default value. lineColor: IField[]; // The width of the line in either pixels or meters based on the units specified. // This must resolve into a number to be valid, otherwise it'll default to the application's default value. lineWidth: IField[]; // Default is "px" for backward compatibility. lineWidthUnits?: "px" | "m"; // Polylines do not support relative to ground positioning. // When set to "absolute" then each point's altitude will be used rather than clamping to the ground. // Default is clamped to ground. altitudeOption?: IAltitudeOption; // The z index will dictate what vector appears on top vertically. // It is only applied when the altitude option is set to "clamped-to-ground". // For example a higher z index will appear on top of a lower z index. zIndex?: IField[]; } ``` ## Polygon styling Within this section we'll cover what polygon Style settings are available and how to use them. We will cover the most supported settings. If you need more advanced settings then please contact us. Please refer to the 'Style calculator' section above for definitions that are not explained here. Some are shared between all the types of styling. ```typescript interface IPolygonSettings { // If the polygon should be extruded. useExtrusion: boolean; // The height of the extrusion in meters. // This must resolve into a number to be valid, otherwise it will not extrude. extrusionPath: IField[]; // The colour of the fill. // This must resolve into a css colour string to be valid, otherwise it'll default to the application's default value. fillColor: IField[]; // The colour of the outline. // This must resolve into a css colour string to be valid, otherwise it'll default to the application's default value. lineColor: IField[]; // Width of outline in meters. // This must resolve into a number to be valid, otherwise it'll default to the application's default value. lineWidth: IField[]; // Default is "m". lineWidthUnits?: "px" | "m"; // The z index will dictate what vector appears on top vertically. // It is only applied when the altitude option is set to "clamped-to-ground". // For example a higher z index will appear on top of a lower z index. zIndex?: IField[]; // Dictates what the altitude value of an Entity should mean. // When not set to "clamp-to-ground" each individual's altitude will be used. // Default is clamped to ground. altitudeOption?: IAltitudeOption; } ``` ## 3D model styling Within this section we'll cover what 3D model Style settings are available and how to use them. We will cover the most supported settings. If you need more advanced settings then please contact us. Please refer to the 'Style calculator' section above for definitions that are not explained here. Some are shared between all the types of styling. ```typescript enum EBlendMode { // Multiplies the source color by the target color. Highlight = 0, // Replaces the source color with the target color. Replace = 1, // Blends the source color and target color together Mix = 2 } // When dealing with tilesets the only settings that are applied are "customize" and "fillColor". interface IModelSettings { // Indicates if the 3D model should be coloured by the Style. // Default is false. customize: boolean; // The colour to apply to the model if customize is set to true. // This must resolve into a css colour string to be valid, otherwise it'll default to the application's default value. fillColor: IField[]; // The model's scale factor. // This is multiplied against the Entity's transform scale. // For example if the Entity has a scale of 2 and this resolves to 3 then the final scale will be 6. // This must resolve into a number to be valid, otherwise it'll default to the application's default value (1). scale: IField[]; // Dictates how the colour specified should be applied to the model. fillColorBlendMode?: EBlendMode; // If blend is set to "Mix" then this will dictate how much of the source colour should be used. // This is value between 0 and 1, default is "0.5". 1 means only the applied colour is used. fillColorBlendAmount?: number; // Dictates what the altitude value of an Entity should mean. // Default is relative to ground. altitudeOption?: IAltitudeOption; } ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apientitystyles # Pending Actions (Server Jobs) (/apipendingaction) # Pending Actions (Server Jobs) Pending Action records is how we track job progress and results. Most actions will return a Pending Action ID that you can use to check the status of the job. As a result, it's important to understand how to use the Pending Action API. ## Data model Below is the data model for a Pending Action record. These records are one of our oldest and don't have a tidied up v3 variant yet. When checking for completion, you can see if the `.Status` stops equaling `IN_PROGRESS`. The `Result` property is the primary candidate for a v3 overhaul where it returns a stringified JSON object you have to parse yourself. This is the result of us migrating from a singular result line to a more complex result object. Some of our oldest jobs may return a non-JSON string in this field. ```typescript /** * Available Pending Action statuses. */ export enum EStatus { InProgress = "IN_PROGRESS", Cancelled = "CANCELLED", Complete = "COMPLETE", Failed = "FAILED" } /** * The Pending Action record itself. */ interface IAction { // ID of the Pending Action record. ID: number; // ID of the application that created the Pending Action. "CreatedBy.Application": string; // ID of the user that created the pending action. "CreatedByUser.ID": string; // Result string is a stringified JSON object. // This is not always the case so please check the data before parsing it. Result: string; // Progress % of the pending action. Progress: number; // Description on why the Pending Action was started. Description: string; // Status of the Pending Action. Status: EStatus; // Created/updated/completed date/time in ISO 8601 UTC. Created: string; Updated?: string; Completed?: string; } ``` ## Request the record We recommend requesting the Pending Action record every 5 seconds until it's marked as no longer in progress. ### Get Pending Action by ID ```http GET https://{accountId}.api.nextspace.host/pendingaction/{action_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{action_id}` | path | yes | ID of the Pending Action record. | **Response** ```typescript // Response is the same as IAction definition above. // It returns the JSON directly. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/pendingaction/{action_id}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Request progress messages If you'd like expanded information about the progress of a Pending Action, you can request for progress messages. ### Get Pending Action Progress Messages ```http GET https://{accountId}.api.nextspace.host/pendingaction/{action_id}/progressmessages?SortOrder={order}&PageSize={size}&PageIndex={index}&Type={type} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{action_id}` | path | yes | ID of the Pending Action record. | | `SortOrder={order}` | query | no | Sort order of the messages. Lets you get newest or oldest first. | | `PageSize={size}` | query | no | Number of messages to get per page. | | `PageIndex={index}` | query | no | Page number to get. The first page is 0. | | `Type={type}` | query | no | Filter messages by type. Add multiple query params to filter for multiple types. Eg: Type=INFO&Type=ERROR | **Response** ```typescript interface IResponse { // Array of progress messages found for your search. Items: { // ID of the pending action record. "PendingAction.ID": number; // ID of the message record. ID: number; // ID of the application that created the message. "CreatedBy.Application": string; // Date/time the message was created. "Created": string; // Message type. Certain types can be hidden from end-users as they are only used for debugging. "Message.Type": string; // Message text. Message: string; }[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/pendingaction/{action_id}/progressmessages"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Cancel a Pending Action If you need to cancel a Pending Action, you can do so with this request. ### Cancel Pending Action by ID ```http DELETE https://{accountId}.api.nextspace.host/pendingaction/{action_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{action_id}` | path | yes | ID of the Pending Action record. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/pendingaction/{action_id}"; const method = "delete"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apipendingaction # Client and temp files (/apiclientfile) # Client and temp files Persistent files that we track in your account are called Client Files. These are stored in your File Store Location (typically an S3 bucket) with a corresponding entry in our table with metadata. A temp file uses a similar process however it's cleared periodically. ## Details You can request the metadata on a Client File using the 'details' endpoint. This does not work for temp files. ### Client File details ```http GET https://{accountId}.api.nextspace.host/file/{file_id}/details ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{file_id}` | path | yes | ID of the Client File to get details for. | **Response** ```typescript interface IResponse { // The ID of the file. ID: string; // The file's extension. // Should start with a 'dot', we're actively working to ensure this is always the case. // It's recommended to either parse it from 'OriginalFileName' or check for the dot yourself until stable. FileExt: string; // The file's mime type. MIMEType: string; // The file's original name. This includes the extension. OriginalFileName: string; // The file's size in bytes. OriginalLength: number; // The user's ID who uploaded the file. "UploadedByUser.ID": string; // An optional category for the file. // Some Nextspace default purposes have additional meaning. Purpose?: string; // UTC date/time when the file was uploaded. Created: string; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/file/{file_id}/details"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Download To download a Client File from a known ID use this request: ### Download Client File ```http GET https://{accountId}.api.nextspace.host/file/{file_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{file_id}` | path | yes | ID of the Client File to download. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/file/{file_id}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` To download a temp file from a known ID use this request: ### Download Client File ```http GET https://{accountId}.api.nextspace.host/tempFile/{file_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{file_id}` | path | yes | ID of the temp file to download. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/tempFile/{file_id}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Delete To delete a Client File use this request: ### Delete Client File ```http GET https://{accountId}.api.nextspace.host/file/{file_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{file_id}` | path | yes | ID of the Client File to delete. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/file/{file_id}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Delete multiple Client Files ```http POST https://{accountId}.api.nextspace.host/deleteFiles ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IRequest { // Array of Client File IDs to delete. "Items": string[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/deleteFiles"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Update Purpose If you'd like to update the purpose of an existing Client File. You can use this request: ### Update Client File purpose ```http POST https://{accountId}.api.nextspace.host/file/updatepurpose/{file_id}?Purpose={purpose} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{file_id}` | path | yes | ID of the Client File to update. | | `Purpose={purpose}` | query | yes | The new purpose for the file. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/file/updatepurpose/{file_id}"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Upload file Smaller file uploads (typically up to 100MB) can be done by sending the file directly in the request body. ### Upload Client File ```http POST https://{accountId}.api.nextspace.host/file/uploadNew ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IRequest { // The file to upload. file: File; // Optional purpose to assign to the newly created Client File record. Purpose?: string; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/file/uploadNew"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Upload temp file ```http POST https://{accountId}.api.nextspace.host/file/uploadTemp ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IRequest { // The file to upload. file: File; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/file/uploadTemp"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` For larger file uploads, we have to perform a multi-part upload. You split the file into chunks and upload each part separately. Once all parts are uploaded, your Client File record is created. The final part you upload to complete your Client File will return the Client File record. If you experience an issue with a part upload, you can re-upload that part. The system will overwrite the existing portion with the new one. ### Upload Client File part ```http POST https://{accountId}.api.nextspace.host/fileUpload ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IRequest { // Name of the file. "originalFileName": string; // Unique token to identify this upload. // Use a UUID or similar. "token": string; // Total number of parts you're uploading. "count": number; // The part number of this upload. // This starts at 1. "part": number; // The file part to upload. "file": File; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/fileUpload"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Upload temp file part ```http POST https://{accountId}.api.nextspace.host/tempFileUpload ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/tempFileUpload"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apiclientfile # Entity Attachments (/apientityattachment) # Entity Attachments An Entity Attachment is a linking record between a Client File and Entity. Attachments have corresponding Attachment Type records which let you define a level of categorization. Some Attachment Types are used for our default panels, for example the Attachment Type "photo" will display the files in the "Media" tab when selecting Entities in Navigator. You will find other Attachment Types appear under the 'Attachments' Selected Entity tab in Navigator. They will appear as folders of related Client Files. ## Attachment Types requests Below are the basic requests for managing Attachment Type records. ### Get Attachment Types ```http GET https://{accountId}.api.nextspace.host/entityAttachmentTypes ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Response** ```typescript interface IResponse { Items: { // Attachment type description. Description: string; // Attachment type ID. ID: Type; // Human readable attachment type name. Name: string; }[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entityAttachmentTypes"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Get Attachment Type by ID ```http GET https://{accountId}.api.nextspace.host/entityAttachmentType/{type_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{type_id}` | path | yes | Attachment Type ID to retrieve the record for. | **Response** ```typescript interface IResponse { // Attachment type description. Description: string; // Attachment type ID. ID: Type; // Human readable attachment type name. Name: string; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entityAttachmentType/{type_id}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Create/Update Attachment Type ```http POST https://{accountId}.api.nextspace.host/entityAttachmentType/{type_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{type_id}` | path | yes | Attachment Type ID to create or update. | **Response** ```typescript interface IRequest { // Attachment type description. Description: string; // Attachment type ID. ID: Type; // Human readable attachment type name. Name: string; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entityAttachmentType/{type_id}"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Delete Attachment Type ```http DELETE https://{accountId}.api.nextspace.host/entityAttachmentType/{type_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{type_id}` | path | yes | Attachment Type ID to delete. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entityAttachmentType/{type_id}"; const method = "delete"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Get Entity Attachments You can only request the full list of attachments as we haven't had a need to filter them. This will likely change in the future. ### Get Entity Attachments ```http GET https://{accountId}.api.nextspace.host/entity/{entity_id}/attachments ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | Entity ID to retrieve attachments for. | **Response** ```typescript interface IResponse { Items: { // Attachment ID. ID: number; // Associated Client File record. ClientFile?: ClientFile.IFile; // Associated Client File ID. "ClientFile.ID": string; // Display order among sibling attachments. DisplayOrder?: number; // Entity this attachment is associated with. "Entity.ID": string; // Attachment type ID. "EntityAttachmentType.ID": Type; }[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/{entity_id}/attachments"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Update display order If you have the need to re-order attachments so that certain ones appear first, you can use this request. This order is tied to the Entity ID and Attachment Type ID. ### Update display order ```http POST https://{accountId}.api.nextspace.host/entity/{entity_id}/attachments/{type_id}/setOrder ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | Entity ID to retrieve attachments for. | | `{type_id}` | path | yes | Attachment Type ID to set order for. | **Request body** ```typescript interface IRequest { // Array of Client Files to update display order for. // These should be in the desired order. "ClientFile.ID": string[]; // The starting display order for the first Client File when updating. // Typically this is 0. "DisplayOrder.Start": number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/{entity_id}/attachments/{type_id}/setOrder"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Delete Entity Attachments Deleting Attachments does not delete the corresponding Client File records, it only unlinks them from the Entity. ### Delete Entity Attachments ```http POST https://{accountId}.api.nextspace.host/entity/{entity_id}/attachments/{type_id}/delete ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | Entity ID to delete (unlink) attachments for. | | `{type_id}` | path | yes | Attachment Type ID to filter for. | **Request body** ```typescript interface IRequest { // Array of the related Client File IDs we want to unlink from the Entity. "ClientFile.ID": string[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/{entity_id}/attachments/{type_id}/delete"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Create / update Entity Attachments You can request an array of Attachments to update or create the links to an Entity. It is recommended to read the [Client and temp files](/apiclientfile) documentation first. ### Update Entity Attachments ```http POST https://{accountId}.api.nextspace.host/entity/{entity_id}/attachments ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | Entity ID to delete (unlink) attachments for. | **Request body** ```typescript interface IRequest { "attachments": { // Associated Client File ID. "ClientFile.ID": string; // Display order among sibling attachments. DisplayOrder?: number; // Entity this attachment is associated with. "Entity.ID": string; // Attachment type ID. "EntityAttachmentType.ID": Type; }[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/{entity_id}/attachments"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apientityattachment # Entity Links (/apientitylink) # Entity Links An Entity Link is a labelled external link that is associated with an Entity record. Associated records will appear under the "Attachments" tab when selecting Entities in Navigator. ## Entity Link requests Below are the basic requests for managing Entity Link records. ### Get Entity Links ```http GET https://{accountId}.api.nextspace.host/entity/{entity_id}/links ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | No description available. | **Response** ```typescript interface IResponse { Items: { // Link ID. ID: number; // Human readable name of the link. DisplayName: string; // ID of the entity this link is linked to. "Entity.ID": string; // URL of the link. URL: string; }[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/{entity_id}/links"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Create / Update Entity Link ```http POST https://{accountId}.api.nextspace.host/entity/{entity_id}/link/{link_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | Entity ID to create/update link for. | | `{link_id}` | path | no | Link ID to update. Provide no ID to create a new record. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/{entity_id}/link/{link_id}"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Delete Entity Link ```http DELETE https://{accountId}.api.nextspace.host/entity/{entity_id}/link/{link_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | ID of the Entity to delete the Link from. | | `{link_id}` | path | yes | ID of the Link to delete. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/{entity_id}/link/{link_id}"; const method = "delete"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apientitylink # Entity Tags (/apitags) # Entity Tags An Entity Tag is a way to add different categories for an Entity to improve searchability, and to add access restrictions. If you see references to "Layers" in documentation, that is a legacy term for Tags. ## Data model Below is a basic data model for a Tag record. When further documentation refers to `ITag` or just a Tag record, you can reference this data model. ```typescript interface ITag { // ID of the Tag. // To create a new Tag, don't specify the ID. ID: number; // Human readable name of the Tag. Name: string; // Description of the Tag. Description?: string; // If tag access (and tagged Entity access) should be restricted. // When a Tag is restricted a user must have the "Layer_" permission to view the Tag/Entity. IsAccessRestricted?: boolean; // Colour of the tag to use when styling Entities or displaying a Tag icon. Color?: string; // Parent tag ID. // Purely for organizational purposes. "Parent.Layer.ID"?: number; } ``` ## Entity Tag requests. Below are the basic requests for managing Tag records. ### Get Tags ```http GET https://{accountId}.api.nextspace.host/layers ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Response** ```typescript interface IResponse { Items: ITag[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/layers"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Get Tag by ID ```http GET https://{accountId}.api.nextspace.host/layer/{layer_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{layer_id}` | path | yes | No description available. | **Response** ```typescript // Response is the Tag record directly. // See the 'Data model' section for the structure. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/layer/{layer_id}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Get Tags by IDs ```http POST https://{accountId}.api.nextspace.host/layers/get ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IRequest { "layerIds": number[]; } ``` **Response** ```typescript interface IResponse { Items: ITag[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/layers/get"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Create / Update Tag ```http POST https://{accountId}.api.nextspace.host/layer/{layer_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{layer_id}` | path | no | ID of the Tag. To create a new Tag, don't specify the ID. | **Response** ```typescript // Response is the Tag record directly. // See the 'Data model' section for the structure. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/layer/{layer_id}"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Delete Tag ```http DELETE https://{accountId}.api.nextspace.host/layer/{layer_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{layer_id}` | path | yes | ID of the Tag to delete. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/layer/{layer_id}"; const method = "delete"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Assigning Tags To update an Entity's Tags you update its `Bruce/Layer.ID` field. It is an array of Tag IDs. ```json { "Bruce": { "Layer.ID": [1, 2, 3] } } ``` You can use our typical Create/Update Entity requests to update this value. See [Entities](/apientities). Additionally, we have DataLab actions for adding/removing and setting Tags for Entities based on a query. See [DataLab actions](/apidatalabactions). --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apitags # Entity LODs (representations) (/apientitylod) # Entity LODs (representations) An Entity LOD (level of detail) is a Client File that represents an Entity. LODs have categories which describe what the LOD is for. Our most common category is GLB which is used for GLB and GLTF representations that are supported in CesiumJS (our default renderer). LODs have numeric levels within their LOD Category, where 0 is the highest quality. You can choose to represent your Entity in any format you choose, for example you can have an SVG representation of an Entity that you display in your app. Entity Types can also have LODs assigned to them to act as defaults when an Entity one is unavailable. This is commonly used for large sets of repeated graphics. See more [here](/apientitytypelod) You can read about how to create file download links for LODs here: [Client and temp files](/apiclientfile). ## LOD Category requests Below are the basic requests for managing LOD Category records. ### Get LOD Categories ```http GET https://{accountId}.api.nextspace.host/lodCategories ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Response** ```typescript interface IResponse { Items: { // Description of the category. Description?: string; // Unique identifier for the category. // This is case-insensitive. Key: string; // Name of the category. Name: string; }[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/lodCategories"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Get LOD Category by Key ```http GET https://{accountId}.api.nextspace.host/lodCategory/{key} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{key}` | path | yes | LOD Category key to retrieve the record for. | **Response** ```typescript interface IResponse { // Description of the category. Description?: string; // Unique identifier for the category. // This is case-insensitive. Key: string; // Name of the category. Name: string; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/lodCategory/{key}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Create/Update LOD Category ```http POST https://{accountId}.api.nextspace.host/lodCategory/{key} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{key}` | path | yes | LOD Category Key to create or update. | **Response** ```typescript interface IRequest { // Description of the category. Description?: string; // Unique identifier for the category. // This is case-insensitive. Key: string; // Name of the category. Name: string; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/lodCategory/{key}"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Delete LOD Category ```http DELETE https://{accountId}.api.nextspace.host/lodCategory/{key} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{key}` | path | yes | LOD Category Key to delete. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/lodCategory/{key}"; const method = "delete"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Get Entity LODs This request will not overlay an Entity Type's LODs. It only returns LODs directly associated with the Entity itself. ### Get Entity LODs ```http GET https://{accountId}.api.nextspace.host/entity/{entity_id}/lods ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | Entity ID to retrieve LODs for. | **Response** ```typescript interface IResponse { Items: { // ID of the related Client File record. "ClientFile.ID": string; // ID of the related Entity record. "Entity.ID": string; // Key of the LOD Category. "LODCategory.Key": string; // Level of detail. 0 is the highest. Level: number; }[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/{entity_id}/lods"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Get Entity LODs (with defaults) This an optimized rendering request that will return the "best" LOD per Entity. It will return an Entity Type LOD if no Entity LOD is available. ### Get Entity LODs (with defaults) ```http GET https://{accountId}.api.nextspace.host/entity/getlods ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IRequest { // Whether to use strict mode. // When false, a lower quality LOD (higher level) is returned if the requested level is not available. "strict": boolean; "Items": { // ID of the related Entity record. "entityId": string, // ID of the related LOD Category record. "categoryId": string, // LOD group within its Entity Type. "group"?: string, // Level of detail. 0 is the highest. "level"?: number }[]; } ``` **Response** ```typescript interface IResponse { Items: { // ID of the LOD's Client File. clientFileId: string; // ID of the related Entity record. entityId: string; }[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/getlods"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Delete Entity LODs Unlike typical 'unlinking' deletions, this request **will** delete the related Client File records. This is subject to change as LOD Client Files have a background cleaner that will delete them if they are not in use already. ### Delete Entity LODs ```http DELETE https://{accountId}.api.nextspace.host/entity/{entity_id}/deleteLODs ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | Entity ID to delete LODs for. | **Request body** ```typescript interface IRequest { Items: { // ID of the related LOD Category record. "LODCategory.Key": string; // Level of detail. 0 is the highest. "Level": number; }[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/{entity_id}/deleteLODs"; const method = "delete"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Create Entity LOD You can request to attach a Client File record to an Entity as a LOD. It is recommended to read the [Client and temp files](/apiclientfile) documentation first. ### Create Entity LOD ```http POST https://{accountId}.api.nextspace.host/entity/{entity_id}/lod/{category_key}/{level} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | Entity ID to create the LOD for. | | `{category_key}` | path | yes | LOD Category key to assign the LOD to. | | `{level}` | path | yes | Level of detail. 0 is the highest. | **Request body** ```typescript interface IRequest { "ClientFile.ID": string; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/{entity_id}/lod/{category_key}/{level}"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apientitylod # Entity Type LODs (representations) (/apientitytypelod) # Entity Type LODs (representations) This is continued documentation about Entity LODs (representations) and how you can assign defaults through an Entity Type. This is a common way to achieve repeated graphics for a large set of records. For example the same tree model for all trees. An Entity LOD will override the Entity Type LOD. You can read about how to manage Client File records and generate download URLs here: [Client and temp files](/apiclientfile). ## Entity Type LOD requests Below are the basic requests for managing LODs for an Entity Type. ### Get Entity Type LODs ```http GET https://{accountId}.api.nextspace.host/entityType/{type_id}/lods?group={group_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{type_id}` | path | yes | Related Entity Type ID. | | `group={group_id}` | query | no | Optional Group ID to filter LODs by. Use 'DEFAULT' for ungrouped/default group. | **Response** ```typescript interface IResponse { Items: { // ID of the related Client File record. "ClientFile.ID": string; // ID of the related Entity record. "Entity.ID": string; // Entity Type ID of the related Entity Type record. "EntityType.ID"?: string; // ID of the lod category. "LODCategory.Key": string; // Level of detail. 0 is the highest. Level: number; // LOD group within its Entity Type. // This is an optional layer of categorization you can apply. // Groups are driven by Entity Styling to let you load different LODs based on attribute values./ // 'DEFAULT' or unset is the default group. Group?: string; }[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entityType/{type_id}/lods"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Create Entity Type LOD ```http POST https://{accountId}.api.nextspace.host/entityType/{type_id}/lod/{category_key}/{level}?group={group_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{type_id}` | path | yes | Related Entity Type ID. | | `{category_key}` | path | yes | LOD Category key to assign the LOD to. | | `{level}` | path | yes | Level of detail. 0 is the highest. | | `group={group_id}` | query | no | Optional Group ID. Use 'DEFAULT' for ungrouped/default group. | **Request body** ```typescript interface IRequest { // ID of the related Client File record. "ClientFile.ID": string; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entityType/{type_id}/lod/{category_key}/{level}"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Delete Entity Type LODs ```http POST https://{accountId}.api.nextspace.host/entityType/{type_id}/deleteLODs?group={group_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{type_id}` | path | yes | Related Entity Type ID. | | `group={group_id}` | query | no | Optional Group ID to filter LODs by. Use 'DEFAULT' for ungrouped/default group. | **Request body** ```typescript interface IRequest { Items: { // Key of the related LOD Category. "LODCategory.Key": string; // Related level of detail. "Level": number; }[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entityType/{type_id}/deleteLODs"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apientitytypelod # Import Assemblies (/apiimportassembly) # Import Assemblies You can mimic the import functionality you see in Operator by making your own import process for IFC, RVT, BRZ, etc files. We'll assume you know how to upload files through the API and that you have access to a `TempFile.ID`. ## Analysis This is an **optional** step to the process that gets you the bounding box size and an attribute schema definition. In Operator, this step lets the user pick what attribute to use for creating new Entity Types. ### Assembly Analysis ```http POST https://{accountId}.api.nextspace.host/import/analyze/assembly ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { // Previously uploaded temp file ID. "TempFile.ID": string; // The file name is used to determine the file extension. // Internally we processes everything as a BRZ, which means IFC or other are first converted to BRZ. // This name is also used to create a file import record in the database. // Eg: 'my-file.ifc'. "TempFile.Name": string; } ``` **Example request body** ```json { "TempFile.ID": "abc", "TempFile.Name": "my-file.ifc" } ``` **Response** ```typescript interface IResponse { "PendingActionID": number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/import/analyze/assembly"; const method = "post"; const token = "your-token"; const body = { "TempFile.ID": "abc", "TempFile.Name": "my-file.ifc" }; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` When the Pending Action is marked as completed, here is the result to expect: ```typescript interface IResult { // Assembly bounding box. // You can use this to estimate the size of the assembly. // If it's all zeros, or missing, that indicates we couldn't determine the size. // That is usually a bad sign. "BoundingBox"?: any; // Root data schema item. Includes a hierarchy of attribute definitions. "DataSchema"?: any; // The converted file ID if we went from a non-BRZ to a BRZ format. // Pass this into the import step to avoid re-conversion. "Converted.TempFile.ID"?: string; } ``` ## Import In this step we import the Entities and relationships from the assembly into Nextspace. ### Assembly Import ```http POST https://{accountId}.api.nextspace.host/import/assembly ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { // Previously uploaded temp file ID. "TempFile.ID": string; // The file name is used to determine the file extension. // Internally we processes everything as a BRZ, which means IFC or other are first converted to BRZ. // This name is also used to create a file import record in the database. // Eg: 'my-file.ifc'. "TempFile.Name": string; // Optional temp file ID from the analysis step. // If you have this, provide both the temp file ID and this converted temp file ID. "Converted.TempFile.ID"?: string; // Optional parent Entity Type ID. // New assembly Entity Types will be created underneath this parent. "Parent.EntityType.ID"?: string; // If 'Parent.EntityType.ID' is not provided then this name is used to create a new parent Entity Type. // If this is not set, the file's name is used to create a new parent Entity Type. "NewParentTypeName"?: string; // Attribute path to sort Entity Types by. // Attribute path segments are separated by a forward slash. Eg: 'address/city'. // Attribute values are used to get-or-create Entity Types. // Eg: if you sort by 'IFCType', you'll get-or-create Entity Types based on the IFC Type attribute. "SortAttrPath"?: string; // If you provided a parent Entity Type ID, then this indicates if newly found Entity Types should be created. // Default is true. "ExtendEntityTypes"?: boolean; // If you provided a parent Entity Type ID, then this indicates if the data schemas are allowed to be extended. // Default is true. "ExtendTypeSchemas"?: boolean; // Indicates if newly created Entity Types should look for global definitions to extend from. // Default is true, however our Operator has this on 'false' for the time being. "LoadTypeTemplates"?: boolean; // Indicates if newly created Entity Types should be 'strict'. // Default is true. // When an Entity Type is not strict then newly found attributes are NOT added to its schema. // This is useful for Entity Types where each Entity has a different set of attributes. "CreateStrictTypes"?: boolean; } ``` **Example request body** ```json { "TempFile.ID": "blah-abc", "TempFile.Name": "my-file.ifc", "Converted.TempFile.ID": "blah-123", "NewParentTypeName": "my import", "SortAttrPath": "IFCType" } ``` **Response** ```typescript interface IResponse { "PendingActionID": number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/import/assembly"; const method = "post"; const token = "your-token"; const body = { "TempFile.ID": "blah-abc", "TempFile.Name": "my-file.ifc", "Converted.TempFile.ID": "blah-123", "NewParentTypeName": "my import", "SortAttrPath": "IFCType" }; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` When the Pending Action is marked as completed, here is the result to expect: ```typescript interface IResult { // Array of assembly root Entity IDs. // Typically there is only one, and you take the first one. RootEntityIDs: string[]; } ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apiimportassembly # Import CSV (/apiimportcsv) # Import CSV You can mimic the import functionality you see in Operator by making your own import process for CSV files. We'll assume you know how to upload files through the API and that you have access to a `TempFile.ID` or `ClientFile.ID`. ## Import ### CSV Import ```http POST https://{accountId}.api.nextspace.host/import/csv ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { // Previously uploaded temp file ID or Client File ID. // One of these is required. "TempFile.ID": string; // File name used for creating an import record. // Eg: 'my-file.csv'. // If you specify a Client File ID, this is not needed as the name is already known. "TempFile.Name": string; "ClientFile.ID": string; // Specifies how the rows should be imported. // Eg: 'A' for add, 'U' for update, 'R' for replace (delete before importing). // You can also use 'U+A' to update or add (our default). // Or 'U+A+D' to update or add, then delete any Entities not in the CSV. // These are all relative to the specified Entity Type. "ImportMode": "A" | "U" | "R" | "U+D" | "U+A" | "U+D+A"; // Set to true to skip rows that fail to import. // If false, the first failure will stop the import. "SkipFailedRows"?: boolean; // Destination Entity Type. "EntityType.ID": string; // Array of Nextspace Tag IDs to apply to all imported Entities. "AddLayerIDs"?: number[]; // The character to split the CSV rows by. // Eg: ',' for comma. "Delimiter": string; // Specify the row index where the header is located. // Rows after this index are imported. // Specify -1 to indicate that there is no header. "HeaderIndex": number; // Array of column names in the CSV. // These are used as the key/names for attributes during data mapping. // Eg: if you say the first index is 'Name', then we'll look for the 'Name' attribute in the data mapping. // We will assume all first column values are for that attribute. "CSVColumns": string[]; // Data mapping for the CSV columns to the target Entity Type schema. "DataMapping": any; } ``` **Example request body** ```json { "TempFile.ID": "blah-abc", "TempFile.Name": "my-file.csv", "ImportMode": "U+A", "SkipFailedRows": true, "EntityType.ID": "123", "HeaderIndex": 0, "CSVColumns": [ "Name", "Description" ], "DataMapping": {} } ``` **Response** ```typescript interface IResponse { "PendingActionID": number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/import/csv"; const method = "post"; const token = "your-token"; const body = { "TempFile.ID": "blah-abc", "TempFile.Name": "my-file.csv", "ImportMode": "U+A", "SkipFailedRows": true, "EntityType.ID": "123", "HeaderIndex": 0, "CSVColumns": [ "Name", "Description" ], "DataMapping": {} }; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` When the Pending Action is marked as completed, here is the result to expect: ```typescript interface IResult { // Estimate of total rows. Can be inaccurate due to empty rows of ones skipped. "TotalRowsEst": number; // Last row index processed. // If it's set to not skip failed rows, then this can help you determine where the failure occurred. // Note that it might not be the exact row index. "LastProcessedRow": number; // Number of failed to import rows. "CountFailed": number; // Number of successfully imported rows. "CountImported": number; } ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apiimportcsv # Import GeoJSON (/apiimportgeojson) # Import GeoJSON You can mimic the import functionality you see in Operator by making your own import process for GeoJSON files. We'll assume you know how to upload files through the API and that you have access to a `TempFile.ID` or `ClientFile.ID`. ## Import ### GeoJSON Import ```http POST https://{accountId}.api.nextspace.host/import/geojson ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { // Previously uploaded temp file ID or Client File ID. // One of these is required. "TempFile.ID": string; // File name used for creating an import record. // Eg: 'my-file.geojson'. // If you specify a Client File ID, this is not needed as the name is already known. "TempFile.Name": string; "ClientFile.ID": string; // Specifies how the features should be imported. // Eg: 'A' for add, 'U' for update, 'R' for replace (delete before importing). // You can also use 'U+A' to update or add (our default). // Or 'U+A+D' to update or add, then delete any Entities not in the GeoJSON. // These are all relative to the specified Entity Type. "ImportMode": "A" | "U" | "R" | "U+D" | "U+A" | "U+D+A"; // Set to true to skip features that fail to import. // If false, the first failure will stop the import. "SkipFailedFeatures"?: boolean; // Destination Entity Type. "EntityType.ID": string; // Array of Nextspace Tag IDs to apply to all imported Entities. "AddLayerIDs"?: number[]; // Data mapping for the GeoJSON properties to the target Entity Type schema. "DataMapping": any; } ``` **Example request body** ```json { "TempFile.ID": "blah-abc", "TempFile.Name": "my-file.geojson", "ImportMode": "U+A", "SkipFailedFeatures": true, "EntityType.ID": "123", "DataMapping": {} } ``` **Response** ```typescript interface IResponse { "PendingActionID": number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/import/geojson"; const method = "post"; const token = "your-token"; const body = { "TempFile.ID": "blah-abc", "TempFile.Name": "my-file.geojson", "ImportMode": "U+A", "SkipFailedFeatures": true, "EntityType.ID": "123", "DataMapping": {} }; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` When the Pending Action is marked as completed, here is the result to expect: ```typescript interface IResult { // Number of failed to import features. "CountFailed": number; // Number of successfully imported features. "CountImported": number; } ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apiimportgeojson # Import JSON (/apiimportjson) # Import JSON You can mimic the import functionality you see in Operator by making your own import process for JSON files. We'll assume you know how to upload files through the API and that you have access to a `TempFile.ID` or `ClientFile.ID`. As JSON is a very flexible format, here is an example of the expected file content: ```json [ { "name": "thing1", "description": "This is a thing." }, { "name": "thing2", "description": "This is another thing." }, { "name": "thing3", "description": "This is yet another thing." } ] ``` ## Import ### JSON Import ```http POST https://{accountId}.api.nextspace.host/import/json ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { // Previously uploaded temp file ID or Client File ID. // One of these is required. "TempFile.ID": string; // File name used for creating an import record. // Eg: 'my-file.json'. // If you specify a Client File ID, this is not needed as the name is already known. "TempFile.Name": string; "ClientFile.ID": string; // Specifies how the features should be imported. // Eg: 'A' for add, 'U' for update, 'R' for replace (delete before importing). // You can also use 'U+A' to update or add (our default). // Or 'U+A+D' to update or add, then delete any Entities not in the JSON. // These are all relative to the specified Entity Type. "ImportMode": "A" | "U" | "R" | "U+D" | "U+A" | "U+D+A"; // Set to true to skip features that fail to import. // If false, the first failure will stop the import. "SkipFailedFeatures"?: boolean; // Destination Entity Type. "EntityType.ID": string; // Array of Nextspace Tag IDs to apply to all imported Entities. "AddLayerIDs"?: number[]; // Data mapping for the JSON properties to the target Entity Type schema. "DataMapping": any; } ``` **Example request body** ```json { "TempFile.ID": "blah-abc", "TempFile.Name": "my-file.json", "ImportMode": "U+A", "SkipFailedFeatures": true, "EntityType.ID": "123", "DataMapping": {} } ``` **Response** ```typescript interface IResponse { "PendingActionID": number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/import/json"; const method = "post"; const token = "your-token"; const body = { "TempFile.ID": "blah-abc", "TempFile.Name": "my-file.json", "ImportMode": "U+A", "SkipFailedFeatures": true, "EntityType.ID": "123", "DataMapping": {} }; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` When the Pending Action is marked as completed, here is the result to expect: ```typescript interface IResult { // Number of failed to import features. "CountFailed": number; // Number of successfully imported features. "CountImported": number; } ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apiimportjson # Import KML / KMZ (/apiimportkml) # Import KML / KMZ You can mimic the import functionality you see in Operator by making your own import process for KML files. We'll assume you know how to upload files through the API and that you have access to a `TempFile.ID` or `ClientFile.ID`. ## Analysis This is an **optional** step to the process that gets you a list of KML folders and attribute definitions for data mapping. In Operator, this step lets the user map KML folders to Entity Types, and adjust the data mapping for each folder. ### KML Analysis ```http POST https://{accountId}.api.nextspace.host/import/analyze/kml ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { // Previously uploaded temp file ID or Client File ID. // One of these is required. "TempFile.ID": string; // File name used for creating an import record. // Eg: 'my-file.kml'. // If you specify a Client File ID, this is not needed as the name is already known. "TempFile.Name": string; "ClientFile.ID": string; } ``` **Example request body** ```json { "TempFile.ID": "abc", "TempFile.Name": "my-file.kml" } ``` **Response** ```typescript interface IResponse { "PendingActionID": number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/import/analyze/kml"; const method = "post"; const token = "your-token"; const body = { "TempFile.ID": "abc", "TempFile.Name": "my-file.kml" }; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` When the Pending Action is marked as completed, here is the result to expect: ```typescript interface IResult { "KMLFolders": { "ID": string; "Name": string; "DataSchema": any; "PlacemarksCount": number; "PolygonsCount": number; }[]; } ``` ## Import ### KML Import ```http POST https://{accountId}.api.nextspace.host/import/kml ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { // Previously uploaded temp file ID or Client File ID. // One of these is required. "TempFile.ID": string; // File name used for creating an import record. // Eg: 'my-file.kml'. // If you specify a Client File ID, this is not needed as the name is already known. "TempFile.Name": string; "ClientFile.ID": string; // Optional analysis ID to reference. // This is used to estimate import progress as analysis includes counts. "Analysis.PendingAction.ID"?: number; // Specifies how the features should be imported. // Eg: 'A' for add, 'U' for update, 'R' for replace (delete before importing). // You can also use 'U+A' to update or add (our default). // Or 'U+A+D' to update or add, then delete any Entities not in the KML. // These are all relative to the specified Entity Type. "ImportMode": "A" | "U" | "R" | "U+D" | "U+A" | "U+D+A"; // Settings per KML folder for the import. // At least one folder is required. "KMLFolders": { "KMLFolder.ID": string; // Destination Entity Type. "EntityType.ID": string; // Array of Nextspace Tag IDs to apply to all imported Entities. "AddLayerIDs"?: number[]; // Data mapping for the KML properties to the target Entity Type schema. "DataMapping"?: any; // Key of the LOD Category to use for imported LOD files. // Default is 'GLB'. "LODCategory.Key"?: string; // LOD Level to use for imported LOD files. // Default is 0 (best quality). "LODLevel"?: number; // Scale factor applied to 'DAE' -> 'GLB' conversion during import. // Default is 1.0 (no scaling). "ConversionScale"?: number; }[]; } ``` **Example request body** ```json { "TempFile.ID": "blah-abc", "TempFile.Name": "my-file.kml", "ImportMode": "A", "Analysis.PendingAction.ID": 1 } ``` **Response** ```typescript interface IResponse { "PendingActionID": number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/import/kml"; const method = "post"; const token = "your-token"; const body = { "TempFile.ID": "blah-abc", "TempFile.Name": "my-file.kml", "ImportMode": "A", "Analysis.PendingAction.ID": 1 }; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` When the Pending Action is marked as completed, here is the result to expect: ```typescript interface IResult { // Number of failed to import features. "CountFailed": number; // Number of successfully imported features. "CountImported": number; } ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apiimportkml # Export BRZ (/apiexportbrz) # Export BRZ You can mimic the export to BRZ functionality you see in Navigator by making your own export process for assemblies. You'll need to know a `RootEntity.ID` value. ## Export We recommend running at most two export jobs at a time. ### Export BRZ ```http POST https://{accountId}.api.nextspace.host/export ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "Type": "BRZ"; "RootEntity.ID": string; } ``` **Example request body** ```json { "Type": "BRZ", "RootEntity.ID": "abc" } ``` **Response** ```typescript interface IResponse { "PendingActionID": number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/export"; const method = "post"; const token = "your-token"; const body = { "Type": "BRZ", "RootEntity.ID": "abc" }; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` When the Pending Action is marked as completed, here is the result to expect: ```typescript interface IResult { // ID of the generated temp file. // To create a download URL, use your API base url with "/tempfile/{ID}" as the path. "TempFile.ID": string; } ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apiexportbrz # DataLab queries (/apidatalabqueries) # DataLab queries DataLab is a powerful API you can utilize to perform complex queries, and later actions against those queries. Here we'll run through a few common queries you might want to perform. ## Basic query / Entity Type criteria Here is an example of the most basic query you can perform, this is a query for a page of Entities from a specific Entity Type. ### DataLab query | Entity Type ```http POST https://{accountId}.api.nextspace.host/v3/datalab/runSearch ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { // Definition of the criteria for the query. // Each item in the primary selection is an AND/OR condition. // An item can also represent a '()' group that contains sub-conditions to help portion the AND/OR conditions. "PrimarySelection": { "Items": [ // Reduce response to a specific Entity Type. { "key": "EntityType", "EntityType": { "EntityType.ID": string } } ] } // Pagination. "Skip"?: number; "Load"?: number; } ``` **Response** ```typescript interface IResponse { // Array of Entities. "Items": any[]; // Array of any encountered errors. // Typically, this is for external data sources that might have failed to be retrieved. "Error"?: string[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/datalab/runSearch"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Criteria: Attributes Here is how you can add an attribute criteria to your query. ### DataLab query | Entity Type + attribute ```http POST https://{accountId}.api.nextspace.host/v3/datalab/runSearch ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "PrimarySelection": { "Items": [ // Reduce response to a specific Entity Type. { "key": "EntityType", "EntityType": { "EntityType.ID": string }, "LogicOperator": "AND" }, // Reduce response to Entities that have a specific attribute. { "key": "Attribute", // This is a Nextspace path string. // Attribute path segments are separated by a forward slash. Eg: 'address/city'. "AttributePath": "my-attribute-path", // This is the value you want to search for. "OperandA": "search-value", // This is the operator you want to use. "Operator": "EQUALS", // You can use "AND NOT" if you want to exclude Entities that match this criteria. // Eg: You can use the Operator "NULL" with "AND NOT" to find Entities that have this attribute. "LogicOperator": "AND" // If you pass a string value into OperatorA, you can use these operators: // "EQUALS", "CONTAINS", "STARTS WITH", and "ENDS WITH". // If you pass an array to 'OperandA', then you can use the "IN" operator to perform a search against the array of values. // If you pass a numeric value into OperatorA, you can use these operators: // "=", ">", "<", ">=", "<=", and "IN" (for an array of values). // If you pass a date value into OperatorA, you can use these operators: // "=", ">", "<", ">=", and "<=". // If you want to use a "Between" operator for numeric or date values, you have to specify both a OperandA and OperandB value. // All Operators support the "NULL" operator as well. This value is needed. // This will check if the attribute both exists and is not an empty string. // For dates, this will ensure the value parses into a valid date (means it's not in ISO 8601 format). } ] } "Skip"?: number; "Load"?: number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/datalab/runSearch"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Criteria: Tags ### DataLab query | Entity Type + Tags ```http POST https://{accountId}.api.nextspace.host/v3/datalab/runSearch ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "PrimarySelection": { "Items": [ // Reduce response to a specific Entity Type. { "key": "EntityType", "EntityType": { "EntityType.ID": string }, "LogicOperator": "AND" }, // Reduce response to Entities that have specific Tags. { // Layer is the original keyword for Tag. "key": "Layer", "Layers": { // Array of Tag IDs. "Layer.ID": [ 1, 2, 3 ], // Operator 'all' means that the Entity must have all of the Tags. // Operator 'either' means that the Entity must have at least one of the Tags. // Operator 'neither' means that the Entity must not have any of the Tags. // Operator 'none' means that the Entity must not have any Tags. You don't need to pass an array of Tag IDs for this Operator. "Operator": "all" }, "LogicOperator": "AND" } ] } "Skip"?: number; "Load"?: number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/datalab/runSearch"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Criteria: User ### DataLab query | Entity Type + User ```http POST https://{accountId}.api.nextspace.host/v3/datalab/runSearch ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "PrimarySelection": { "Items": [ // Reduce response to a specific Entity Type. { "key": "EntityType", "EntityType": { "EntityType.ID": string }, "LogicOperator": "AND" }, // Reduce response to Entities created by a specific User. { "key": "CreatedBy", "CreatedBy": { "User.ID": "some-user-id" }, "LogicOperator": "AND" } ] } "Skip"?: number; "Load"?: number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/datalab/runSearch"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Criteria: LOD As assemblies can include Entities that act as groups without a visual representation, this is commonly used to narrow down an assembly's Entities into just those that can be seen. ### DataLab query | Entity Type + LOD ```http POST https://{accountId}.api.nextspace.host/v3/datalab/runSearch ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "PrimarySelection": { "Items": [ // Reduce response to a specific Entity Type. { "key": "EntityType", "EntityType": { "EntityType.ID": string }, "LogicOperator": "AND" }, // Reduce response to Entities to those that have a LOD (level of detail). { "key": "LOD", "LOD": { // Commonly "GLB". "Type": "some-lod-category-id", // You can use "have-not" to find those without any LODs of the specified type. "LogicOperator": "have" }, "LogicOperator": "AND" } ] } "Skip"?: number; "Load"?: number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/datalab/runSearch"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Criteria: Assembly This is a criteria for finding Entities that are part of a specific assembly. You specify the root Entity ID and all Entities (including the root) are included. WARNING: You cannot perform 'OR' logic operators with this criteria at this time. ### DataLab query | Assembly ```http POST https://{accountId}.api.nextspace.host/v3/datalab/runSearch ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "PrimarySelection": { "Items": [ { "key": "Root", // It is recommended to keep this at 'true' as it will ignore UCS Entities. // A UCS is an Entity that is used to share coordinates between multiple assemblies, which means it is not part of the assembly itself. "IgnoreUCSRoots": true, // You can choose to exclude assemblies that have Tilesets. // This is a useful option to find assemblies that don't have a Tileset yet, which often means they aren't being used. "IgnoreWithTilesets": false, "LogicOperator": "AND" } ] } "Skip"?: number; "Load"?: number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/datalab/runSearch"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Criteria: List assemblies This is a criteria for finding assembly root Entities. These Entities' IDs are typically used to generate Tilesets (optimized graphics for rendering), and to search against hierarchies generated from a file import (IFC/RVT/etc). ### DataLab query | Assembly ```http POST https://{accountId}.api.nextspace.host/v3/datalab/runSearch ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "PrimarySelection": { "Items": [ { "key": "AssemblyRoot", "RootID": "some-root-entity-id", "LogicOperator": "AND" } ] } "Skip"?: number; "Load"?: number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/datalab/runSearch"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Criteria: Reference to another DataLab query You can perform an IN search against an attribute in another query. For example, "Find all Work Order Entities that have a 'status' value in my "Valid Work Order status" query". ### DataLab query | Assembly ```http POST https://{accountId}.api.nextspace.host/v3/datalab/runSearch ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "PrimarySelection": { "Items": [ { "key": "attribute-join", "AttributeJoin": { // Attribute within this query to perform the IN against. // Attribute path segments are separated by a forward slash. Eg: 'address/city'. "InputAttributePath": string; // A saved DataLab query to reference by ID. "DataLabQueryID": number; // Attribute within the referenced query to gather valid values from. // Attribute path segments are separated by a forward slash. Eg: 'address/city'. "TargetAttributePath": string; }, "LogicOperator": "AND" } ] } "Skip"?: number; "Load"?: number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/datalab/runSearch"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Criteria: '()' groups Below is an example of how you can use '()' groups to help portion your criteria. ### DataLab query | '()' groups ```http POST https://{accountId}.api.nextspace.host/v3/datalab/runSearch ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "PrimarySelection": { "Items": [ // Reduce response to a specific Entity Type. { "key": "EntityType", "EntityType": { "EntityType.ID": string }, "LogicOperator": "AND" }, // A criteria for finding Entities that include 'A' or 'B' in a specific attribute. { "key": "SubQuery", "LogicOperator": "AND", "Subquery": { "Items": [ { "key": "Attribute", // Attribute path segments are separated by a forward slash. Eg: 'address/city'. "AttributePath": "my-attribute-path", "OperandA": "A", "Operator": "CONTAINS", "LogicOperator": "OR" }, { "key": "Attribute", // Attribute path segments are separated by a forward slash. Eg: 'address/city'. "AttributePath": "my-attribute-path", "OperandA": "B", "Operator": "CONTAINS" } ] } } ] } "Skip"?: number; "Load"?: number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/datalab/runSearch"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apidatalabqueries # DataLab actions (/apidatalabactions) # DataLab actions If you've finished reading the DataLab queries documentation, you're ready to start performing actions against the response data. An action is a server-side job that will be performed against all found Entities for your query. This allows you to perform bulk operations without having to iterate over the results in your client code. Performing an action will return a `PendingAction.ID` for you to monitor. Refer to the Pending Action documentation for help. Not all actions are currently documented. Please contact support if you want more information about any actions, including those available in Operator but not documented here. ## Action: Set attribute value ### DataLab action | Set attribute value ```http POST https://{accountId}.api.nextspace.host/entities/datalab/takeAction ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "PrimarySelection": { "Items": any[]; } "Action": { "ActionType": "set-attribute"; // Performs some additional validation during the process based on the supplied value. // This will default to 'string' when unspecified. "AttributeType": "string" | "integer" | "double" | "datetime"; // This is a Nextspace path string. // Attribute path segments are separated by a forward slash. Eg: 'address/city'. "Path": string; // Value to set. "Value": any; } } ``` **Response** ```typescript interface IResponse { // ID of the Pending Action to monitor. ID: number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entities/datalab/takeAction"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Action: Update Tags ### DataLab action | Update Tags ```http POST https://{accountId}.api.nextspace.host/entities/datalab/takeAction ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "PrimarySelection": { "Items": any[]; } "Action": { "ActionType": "layer"; "Layer": { // Tags IDs. "OperandA": [1, 2, 3], // Operation we're performing. // 'add' will add the Tags to the Entity. // 'remove' will remove the Tags from the Entity. // 'set' will replace the Tags on the Entity with the supplied tags. "Operator": "add" | "remove" | "set"; } } } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entities/datalab/takeAction"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Action: Create Entity Attachments Attach an existing file to the selected Entities. This will not duplicate the file in the File Store. ### DataLab action | Attach file ```http POST https://{accountId}.api.nextspace.host/entities/datalab/takeAction ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "PrimarySelection": { "Items": any[]; } "Action": { "ActionType": "attachclientfile"; "AttachClientFile": { // ID of the file to attach. "ClientFile.ID": string; // Destination Attachment Type ID to attach the file to. "AttachmentType.ID": string; // Optional grouping within the related Attachment Type. // This is useful to further filter attachments within the same group. // Pass an empty string or null if you don't want to group the attachment. "Group": string; } } } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entities/datalab/takeAction"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Action: Export JSON Export the found Entities and related records into a series of JSON files zipped together. ### DataLab action | Export JSON ```http POST https://{accountId}.api.nextspace.host/entities/datalab/takeAction ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "PrimarySelection": { "Items": any[]; } "Action": { "ActionType": "export-json"; // Optional expand property to expand additional data to the export. // Some options are "Attachment", "Tag", "LOD", "Relation", "EntityType". // Comma-separate if you want to do multiple, eg: "Attachment,Tag". "Expand"?: string; // Optional Scenario ID or Key to use for the export. // This will request the Scenario Entity data (if available) for the Entities. "Scenario?: string | number; } } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entities/datalab/takeAction"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Action: Export CSV ### DataLab action | Export CSV ```http POST https://{accountId}.api.nextspace.host/entities/datalab/takeAction ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "PrimarySelection": { "Items": any[]; } "Action": { "ActionType": "export-csv"; // ID of the Entity Type to reference for the schema definition. // Schema attributes marked as important/pinned will be included in the CSV. // The ID of the selection does not need to match this Type, however the schema should be compatible so you don't get empty rows. "EntityTypeID": string; } } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entities/datalab/takeAction"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Action: Delete Entities ### DataLab action | Delete Entities ```http POST https://{accountId}.api.nextspace.host/entities/datalab/takeAction ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "PrimarySelection": { "Items": any[]; } "Action": { "ActionType": "delete"; } } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entities/datalab/takeAction"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Action: Delete Relationships This action will delete Entity Relationships where the found Entities are the parent. ### DataLab action | Delete Relationships ```http POST https://{accountId}.api.nextspace.host/entities/datalab/takeAction ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "PrimarySelection": { "Items": any[]; } "Action": { "ActionType": "delete"; // The ID of the Relation Type to delete. "RelationType.ID": string; } } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entities/datalab/takeAction"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Action: Data Transformation You can apply a data transformation process against each found Entity by creating a transformation record and applying it to an action. We'll briefly cover the basic CRUD of these records, then show an example action using them. ### Get Transformation records ```http GET https://{accountId}.api.nextspace.host/entityType/{type_id}/dataTransforms ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{type_id}` | path | yes | The ID of the Entity Type to get the transformations for. | **Response** ```typescript interface IResponse { Items: { // ID of the transformation. // This is auto-generated for new records. "ID": number; // Human-readable name of the transformation. "Name": string; // Human-readable description of the transformation. "Description"?: string; // The ID of the Entity Type this transformation is for. "EntityType.ID": string; // Data Mapping to apply between the existing data to the newly saved data. // You have only to specify connections that you want changed. // See data mapping documentation for more information. "DataMapping": IDataMapping; }[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entityType/{type_id}/dataTransforms"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Get Transformation by ID ```http GET https://{accountId}.api.nextspace.host/entityType/{type_id}/dataTransform/{transform_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{type_id}` | path | yes | The ID of the Entity Type to get the transformation for. | | `{transform_id}` | path | yes | The ID of the transformation to get. | **Response** ```typescript interface IResponse { // ID of the transformation. // This is auto-generated for new records. "ID": number; // Human-readable name of the transformation. "Name": string; // Human-readable description of the transformation. "Description"?: string; // The ID of the Entity Type this transformation is for. "EntityType.ID": string; // Data Mapping to apply between the existing data to the newly saved data. // You have only to specify connections that you want changed. // See data mapping documentation for more information. "DataMapping": IDataMapping; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entityType/{type_id}/dataTransform/{transform_id}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Create/Update Transformation record ```http DELETE https://{accountId}.api.nextspace.host/entityType/{type_id}/dataTransform/{transform_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{type_id}` | path | yes | The ID of the Entity Type to create/update the transformation for. | | `{transform_id}` | path | no | The ID of the transformation to update, don't specify to create a new transformation. | **Request body** ```typescript interface IResponse { // ID of the transformation. // This is auto-generated for new records. "ID"?: number; // Human-readable name of the transformation. "Name": string; // Human-readable description of the transformation. "Description"?: string; // The ID of the Entity Type this transformation is for. "EntityType.ID": string; // Data Mapping to apply between the existing data to the newly saved data. // You have only to specify connections that you want changed. // See data mapping documentation for more information. "DataMapping": IDataMapping; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entityType/{type_id}/dataTransform/{transform_id}"; const method = "delete"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Delete Transformation by ID ```http DELETE https://{accountId}.api.nextspace.host/entityType/{type_id}/dataTransform/{transform_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{type_id}` | path | yes | The ID of the Entity Type to delete the transformation for. | | `{transform_id}` | path | yes | The ID of the transformation to delete. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entityType/{type_id}/dataTransform/{transform_id}"; const method = "delete"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` Here is how you can apply a transformation to an action: ### DataLab action | Data transformation ```http POST https://{accountId}.api.nextspace.host/entities/datalab/takeAction ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IPostBody { "PrimarySelection": { "Items": any[]; } "Action": { "ActionType": "data-transform"; // ID of the transformation to apply. "DataTransformID": number; } } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entities/datalab/takeAction"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apidatalabactions # DataLab secondary queries (/apidatalabsecondaryqueries) # DataLab secondary queries DataLab has the ability to chain two queries so you can perform a secondary query against each found Entity in the primary one. This effectively gives you for-loop like functionality to further isolate the specific records you want to work with. ## Basics The documentation for secondary queries matches the primary documentation. So we'll cover how to reference the primary query and what actions will be supported. When specifying a criterion you can assign a `.ValueOption` property to equal "PRIMARY" as a way to indicate that the Operand should be referenced from the primary query. Here is how you can find Entities near other Entities as an example. Note that this doesn't return the found result as it's two dimensional, instead it lets you perform an action against the result. ```typescript const postBody = { // Primary selection to first selection Entities in an Entity Type. "PrimarySelection": { "Items": [ { "key": "EntityType", "EntityType": { "EntityType.ID": "my-entities-1" }, "LogicOperator": "AND" } ] } // Now we'll perform a secondary selection where these Entities are near the primary. "SecondarySelection": { "Items": [ { "key": "attribute", "LogicOperator": "AND", // Attribute we're comparing from this query. "AttributePath: "Bruce/Location", // Location to compare this query to. // This is being referenced from the primary query due to the ValueOption. "ValueOption": "PRIMARY", "Operator": "NEAR BY", "OperandA": { "AttributePath": "Bruce/Location" }, // Distance in meters. "Distance": 500, } ] } } ``` You can get a sample of the result against a specific Entity by specifying these properties. ```typescript const postBody = { "PrimarySelection": { ... } "SecondarySelection": { ... }, // Specify the values to get a sample page of the secondary results. "PrimaryEntityID": "my-entity-id", "PrimaryEntityTypeID": "my-entity-type-id" } ``` ## Action: Set attribute value You can set a value on each found Entity in the secondary selection through the set-attribute action. This will let you reference the relevant primary Entity for the value if you want to use that over a fixed value. ```json interface IAction { "ActionType": "set-attribute"; // Performs some additional validation during the process based on the supplied value. // This will default to 'string' when unspecified. "AttributeType": "string" | "integer" | "double" | "datetime"; // This is a Nextspace path string. // Attribute path segments are separated by a forward slash. Eg: 'address/city'. "Path": string; // Specify "PRIMARY" to reference the related primary Entity for getting the value. // Specify "SAME_ENTITY" to reference the current Entity for getting the value. // Don't specify (or use 'CONST') to set a fixed value. "ValueOption": "PRIMARY" | "SAME_ENTITY"; // Value to set. // When 'ValueOption' is 'CONST' this will be the value directy to set. // When 'ValueOption' is 'PRIMARY' this will be the path to get the value from the primary Entity. // When 'ValueOption' is 'SAME_ENTITY' this will be the path to get the value from the current Entity. "Value": { // Path to get the attribute from when the "ValueOption" is "PRIMARY". "AttributePath": string; } } ``` ## Action: Merge Entities This action will merge each secondary Entity into the corresponding primary Entity. The secondary Entities are not deleted. Specify the mapping below to dictate the merge logic. ```json interface IAction { "ActionType": "merge"; // Data Mapping to apply between the found secondary Entities and the primary Entity. "DataMapping": IDataMapping; } ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apidatalabsecondaryqueries # Entity historic data (/apientityhistoricdata) # Entity historic data Entity historic records are time-stamped snapshots of an Entity's data. You can assign a set of your attributes to be tied to one of your date-time attributes, and the system will automatically create a new historic record each time the date-time attribute changes. You can have multiple date-time attributes that create different historic records for different sets of attributes. We call these date-times "Historic keys", and attributes that change "Historic attributes". You can flag an attribute as tied to a historic key by setting the `HistoricKey` property on the attribute in the Entity Type's Data Schema. See below a preview of the Data Schema definition that you can find more information about in the [Entity Type Data Schema documentation](/apientitytypedataschema). ```typescript export interface IAttribute { ... // The historic key (if any) this attribute is associated with. // This is a date-time attribute that drives the change for this attribute value. // Attribute path segments are separated by a forward slash. Eg: 'address/city'. HistoricKey?: string; } ``` Once set, updating Entities in that Entity Type will check for valid date-time values in that key and create/update historic records based on the changes. If you update an Entity with no valid date-time, then the default attribute values are updated within the 'normal' Entity record. These default values are returned when querying an Entity without a timestamp, or when no historic records are found for the timestamp provided. Note that there is a default limit of 1,000 historic records per Entity ID unless an upgraded limit is discussed. To request for historic data, simply use the "HistoricPoint=an_iso_8601_date_time" query parameter during Entity or DataLab requests. The latest historic record to that point in time will be returned. Records in the future will not be returned. Note that historic records are applied to found Entity records after the filtering and sorting is done. Returned records will have additional metadata on what historic records were overlaid on the Entity record. Below is a preview of how an Entity's data structure is extended to include historic metadata. ```typescript interface IEntity { // Bruce is a code-word for the Nextspace API. // These are internal fields. Bruce: { // Outline on what parts of the Entity were loaded from where. // This is a preview on what properties are available (focused on Historic here). "Outline"?: { // Indicates that this is the baseline source of Entity data. // If true, then any attributes not specified by another source are assumed to be from this one. "Baseline"?: boolean; // Human readable name of the source. "Source.Name": string; // Foreign-key, for historic data this is the date-time attribute path. "ForeignKey"?: string; // DateTime representing the time of the record. "DateTime"?: string; // Indicates that this source is editable. // When false, inputs for the related attribute are disabled. "Editable": boolean; // Array of attribute paths that were loaded from this source. // If Baseline=true, then all attributes that weren't specified are loaded from this source. "Attribute"?: string[]; }[]; } } ``` ## Work with records directly Overlaying a point in time answers what an Entity looked like then. To work with the records themselves, reading a series or writing one, use the endpoints below. A record is shaped like the Entity it belongs to. Your attribute values sit at the root of its `Entity` object and platform managed values sit under that object's `Bruce` key, exactly as they do on a live Entity. The same shape is returned by a read, accepted by a write, and accepted by a delete, so a record can be moved between them without translation. ```typescript interface IHistoricRecord { // ID of the Entity this record belongs to. "Entity.ID": string; // Quoted path of the attribute history is tracked against. // Eg: '"reading_taken"', or '"readings"/"taken"' when nested in a Structure. "AttrKey": string; // ISO 8601 date-time for when these values applied. "DateTime": string; // The Entity as it was at that date-time. "Entity": { // Platform managed values as at that date-time. "Bruce"?: { "Location"?: any; "Transform"?: any; "Boundaries"?: any; "VectorGeometry"?: any; }; // Your attribute values sit alongside it, at the root. [attribute: string]: any; }; // Scenario this record belongs to, if any. "Scenario"?: string | number; // Read-only. "Created"?: string; "Updated"?: string; "Entity.InternalID"?: number; } ``` Together, `Entity.ID`, `AttrKey` and `DateTime` are what identify a record. That is all a delete needs, and it is why a record read back can be handed straight to one. ## Get records There are three ways to read, differing only in how the Entities are named. Use the body form when the list of Entity IDs is long enough to be awkward in a url. ### Get historic records for one Entity ```http GET https://{accountId}.api.nextspace.host/v3/entity/{entity_id}/historic?AttrKey=historic_key&DateTimeFrom=date_time&DateTimeTo=date_time&PageIndex=0&PageSize=50 ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | The Entity to read records for. | | `AttrKey=historic_key` | query | no | Quoted path of the attribute history is tracked against. Repeat the param to match more than one. Eg: AttrKey="reading_taken". | | `DateTimeFrom=date_time` | query | no | ISO 8601 date-time for the inclusive start of the range to read. Eg: '2026-01-01T00:00:00Z'. | | `DateTimeTo=date_time` | query | no | ISO 8601 date-time for the inclusive end of the range to read. Eg: '2026-01-31T23:59:59Z'. | | `PageIndex=0` | query | no | Zero-based page index. | | `PageSize=50` | query | no | Records per page. Capped at 1,000. | **Response** ```typescript interface IResponse { // Records for this page. See IHistoricRecord above. "Items": IHistoricRecord[]; "PageIndex": number; "PageSize": number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/entity/{entity_id}/historic"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Get historic records for many Entities ```http GET https://{accountId}.api.nextspace.host/v3/entities/historic?Entity.ID=entity_id&AttrKey=historic_key&DateTimeFrom=date_time&DateTimeTo=date_time ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `Entity.ID=entity_id` | query | yes | The Entity to read records for. Repeat the param, or comma separate, for more than one. Eg: Entity.ID=entity_id1&Entity.ID=entity_id2. | | `AttrKey=historic_key` | query | no | Quoted path of the attribute history is tracked against. Repeat the param to match more than one. | | `DateTimeFrom=date_time` | query | no | ISO 8601 date-time for the inclusive start of the range to read. | | `DateTimeTo=date_time` | query | no | ISO 8601 date-time for the inclusive end of the range to read. | **Response** ```typescript interface IResponse { "Items": IHistoricRecord[]; "PageIndex": number; "PageSize": number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/entities/historic"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Get historic records with a request body ```http POST https://{accountId}.api.nextspace.host/v3/entities/getHistoric ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IRequest { // IDs of the Entities to read records for. // A single ID may be sent as a string. "Entity.ID": string[]; // Quoted paths of the attributes history is tracked against. // A single path may be sent as a string. "AttrKey"?: string[]; // ISO 8601 date-time for the inclusive start of the range to read. "DateTimeFrom"?: string; // ISO 8601 date-time for the inclusive end of the range to read. "DateTimeTo"?: string; // ISO 8601 date-time to read only records written after. "CreatedAfter"?: string; // Scenario to read records for. Omit for real world data. "Scenario"?: string | number; "OrderBy"?: "ID" | "DateTime" | "AttrKey" | "Created"; "OrderDir"?: "ASC" | "DESC"; "PageIndex"?: number; // Capped at 1,000. "PageSize"?: number; // Returns counts across the matched range instead of the records themselves. // Use it to find where history exists before paging through it. "Analysis"?: boolean; } ``` **Response** ```typescript interface IResponse { "Items": IHistoricRecord[]; "PageIndex": number; "PageSize": number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/entities/getHistoric"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Update records Writing a record directly is how you add history the platform did not capture itself, such as a backfill from another system. The date-time attribute named by `AttrKey` should also appear in the recorded values, since that attribute is what the record is keyed on. A record that cannot be written does not fail the request. It is named in `Warning` and the remaining records still land, so check that rather than assuming every record was accepted. Note that there is a default limit of 1,000 historic records per Entity ID unless an upgraded limit is discussed. Past that, the oldest records are dropped. ### Update historic records for one Entity ```http POST https://{accountId}.api.nextspace.host/v3/entity/{entity_id}/historic ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | The Entity to write records for. A record's "Entity.ID" is ignored here, since the route already names the Entity. | **Request body** ```typescript interface IRequest { // Records to create or update. See IHistoricRecord above. "Items": IHistoricRecord[]; // Scenario to write every record against, instead of naming one per record. "Scenario"?: string | number; } ``` **Response** ```typescript interface IResponse { // The records that were written. "Items": IHistoricRecord[]; // One entry per record that was skipped, naming its position and why. "Warning"?: string[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/entity/{entity_id}/historic"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Update historic records for many Entities ```http POST https://{accountId}.api.nextspace.host/v3/entities/historic ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IRequest { // Records to create or update. Each names its Entity with "Entity.ID". "Items": IHistoricRecord[]; // Scenario to write every record against, instead of naming one per record. "Scenario"?: string | number; } ``` **Example request body** ```json { "Items": [ { "Entity.ID": "entity_id", "AttrKey": "\"reading_taken\"", "DateTime": "2026-01-01T09:00:00Z", "Entity": { "reading_taken": "2026-01-01T09:00:00Z", "temperature": 21.4 } }, { "Entity.ID": "other_entity_id", "AttrKey": "\"surveyed\"", "DateTime": "2026-01-01T09:00:00Z", "Entity": { "surveyed": "2026-01-01T09:00:00Z", "Bruce": { "Location": { "latitude": -36.8485, "longitude": 174.7633, "altitude": 0 } } } } ] } ``` **Response** ```typescript interface IResponse { "Items": IHistoricRecord[]; "Warning"?: string[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/entities/historic"; const method = "post"; const token = "your-token"; const body = { "Items": [ { "Entity.ID": "entity_id", "AttrKey": "\"reading_taken\"", "DateTime": "2026-01-01T09:00:00Z", "Entity": { "reading_taken": "2026-01-01T09:00:00Z", "temperature": 21.4 } }, { "Entity.ID": "other_entity_id", "AttrKey": "\"surveyed\"", "DateTime": "2026-01-01T09:00:00Z", "Entity": { "surveyed": "2026-01-01T09:00:00Z", "Bruce": { "Location": { "latitude": -36.8485, "longitude": 174.7633, "altitude": 0 } } } } ] }; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Delete records A delete either names the records to remove or describes them with a filter. Both endpoints take both forms, and `Items` wins when a request carries both. **Naming the records** takes them in the shape a read returns, so records you just listed can be handed straight back. Only the identifying fields are read, and anything else on the object is ignored. **Filtering** uses the same fields a read accepts. Outside a Scenario, `AttrKey` and both ends of the range are all required. That is deliberate: it is what stops an unbounded delete. Matching no records is not an error. ### Delete historic records for one Entity ```http DELETE https://{accountId}.api.nextspace.host/v3/entity/{entity_id}/historic?AttrKey=historic_key&DateTimeFrom=date_time&DateTimeTo=date_time ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | The Entity to delete records for. | | `AttrKey=historic_key` | query | no | Quoted path of the attribute to delete records for. Required when filtering outside a Scenario. Ignored when the records are named in the body. | | `DateTimeFrom=date_time` | query | no | ISO 8601 date-time for the inclusive start of the range to delete. Required when filtering outside a Scenario. | | `DateTimeTo=date_time` | query | no | ISO 8601 date-time for the inclusive end of the range to delete. Required when filtering outside a Scenario. | **Request body** ```typescript // Optional. Without a body, the query params above are the filter. interface IRequest { // The records to remove, as a read returns them. // Only "AttrKey" and "DateTime" are needed on each, since the route names the Entity. "Items"?: IHistoricRecord[]; // Scenario every named record belongs to, instead of naming one per record. "Scenario"?: string | number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/entity/{entity_id}/historic"; const method = "delete"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Delete historic records for many Entities ```http POST https://{accountId}.api.nextspace.host/v3/entities/deleteHistoric ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IRequest { // Either name the records to remove, as a read returns them. // Each needs "Entity.ID" plus "AttrKey" and "DateTime", or "Entity.ID" and "Scenario". "Items"?: IHistoricRecord[]; // Or describe them with a filter. // IDs of the Entities to delete records for. // A single ID may be sent as a string. "Entity.ID"?: string[]; // Quoted path of the attribute to delete records for. // Required unless a Scenario is supplied. "AttrKey"?: string; // ISO 8601 date-time for the inclusive start of the range to delete. // Required unless a Scenario is supplied. "DateTimeFrom"?: string; // ISO 8601 date-time for the inclusive end of the range to delete. // Required unless a Scenario is supplied. "DateTimeTo"?: string; // Scenario to delete records from. // Supplying one relaxes the attribute and range requirements. "Scenario"?: string | number; } ``` **Example request body** ```json { "Items": [ { "Entity.ID": "entity_id", "AttrKey": "\"reading_taken\"", "DateTime": "2026-01-01T09:00:00Z" }, { "Entity.ID": "other_entity_id", "AttrKey": "\"reading_taken\"", "DateTime": "2026-01-01T09:00:00Z" } ] } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/entities/deleteHistoric"; const method = "post"; const token = "your-token"; const body = { "Items": [ { "Entity.ID": "entity_id", "AttrKey": "\"reading_taken\"", "DateTime": "2026-01-01T09:00:00Z" }, { "Entity.ID": "other_entity_id", "AttrKey": "\"reading_taken\"", "DateTime": "2026-01-01T09:00:00Z" } ] }; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Doing this in Python The Python library wraps all of the above behind one namespace, including paging, so there is no need to pick between these endpoints yourself. See the [Python Entity requests documentation](/pythonentityrequests). ```python import bruce_models as BM records = list(BM.EntityHistoricData.iterate( bruce, ["entity-id"], attr_key=["reading_taken"], date_time_from="2026-01-01T00:00:00Z", date_time_to="2026-01-31T23:59:59Z" )) # Records read can be handed straight back to remove exactly those. BM.EntityHistoricData.delete_records(bruce, records) ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apientityhistoricdata # Entity | Scenarios (/apientityscenarios) # Entity | Scenarios An Entity scenario is alternative state for the Entity to be in. It is a way to manage simulations where an Entity needs to take on a temporary state. ## Creating a 'Scenario' Entity If you update an Entity record and include the `Scenario` value, then the primary record is not touched. It will create/update the related scenario data instead. You can alternatively specify the scenario ID or Key in the query param, which will override any values supplied in the body. You must be updating an existing Entity record. ### Create/Update one 'Scenario' Entity ```http POST https://{accountId}.api.nextspace.host/v3/entity?Scenario={scenario} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `Scenario={scenario}` | query | no | Scenario ID or key to reference. Can be supplied here, or within the Entity data. | **Request body** ```typescript interface IPostBody { "Bruce": { "ID": "your-entity-id", // Can be supplied here, or through the query param. "Scenario": "your-scenario-key-or-id" }, "some-key": "some-value" } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/entity"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Create/Update multiple 'Scenario' Entities ```http POST https://{accountId}.api.nextspace.host/v3/entities?Scenario={scenario} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `Scenario={scenario}` | query | no | Scenario ID or key to reference. Can be supplied here, or within the Entity data. If supplied here then it will override any individually specified values. | **Request body** ```typescript interface IPostBody { "Items": [ { "Bruce": { "ID": "your-entity-id", // If the query param is supplied, then this value will be overridden. "Scenario": "your-scenario-key-or-id" } }, { "Bruce": { "ID": "your-entity-id-2" // Can omit the Scenario if the query param is supplied. } } ] } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/entities"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Getting a 'Scenario' Entity You can perform our typical requests and include either `Scenario=blah` in the query or body (depending on if you're doing a GET or POST search). The scenario data is included after the query resolves, so your filters will not apply against the scenario! This is something we're looking to improve. ### Get a 'Scenario' Entity ```http GET https://{accountId}.api.nextspace.host/v3/entity/{entity_id}?Scenario={scenario} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | Entity ID to retrieve. | | `Scenario={scenario}` | query | no | Scenario ID or key to get. | **Response** ```typescript interface IResponse { "Bruce": { "CreatedBy.User.ID": "dummy", "Created": "some-date-time-string", "EntityType.ID": "some-entity-type-id", "ID": "some-entity-id", // Outline on what parts of the Entity were loaded from where. // This is a preview on what properties are available (focused on Scenarios here). "Outline"?: { // Indicates that this is the baseline source of Entity data. // If true, then any attributes not specified by another source are assumed to be from this one. "Baseline"?: boolean; // Human readable name of the source. "Source.Name": string; // Related Scenario record if any. // This is the scenario ID or key. Typically the Key. "Scenario"?: string | number; // DateTime representing the time of the record (if any). "DateTime"?: string; // Indicates that this source is editable. // When false, inputs for the related attribute are disabled. "Editable": boolean; // Array of attribute paths that were loaded from this source. // If Baseline=true, then all attributes that weren't specified are loaded from this source. "Attribute"?: string[]; }[] }, "some-key": "some-value" } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/entity/{entity_id}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Get multiple 'Scenario' Entities ```http GET https://{accountId}.api.nextspace.host/v3/entities?Type={entity_type_id}&Scenario={scenario} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `Type={entity_type_id}` | query | no | Example filter, in this case an Entity Type ID to filter by. | | `Scenario={scenario}` | query | no | Scenario ID or key to get. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v3/entities"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Getting a 'Scenario' record You can get the plain records directly as well. This means it won't include our typical internal `Bruce` data. Note that this uses the same technology as the historic-records so until a v3 request is available, some terminology may be the same. ### Get 'Scenario' records ```http GET https://{accountId}.api.nextspace.host/v1/entity/historicData?Scenario={scenario}&entityId={entity_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `Scenario={scenario}` | query | yes | Scenario ID or key to get. | | `entityId={entity_id}` | query | yes | Entity ID to retrieve. Specify this query param multiple times to get multiple records. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v1/entity/historicData"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Deleting 'Scenario' Entity records If an Entity is deleted, the related scenario data is also deleted. if the scenario is deleted, all scenario data related to it is deleted. Note that this uses the same technology as the historic-records so until a v3 request is available, some terminology may be the same. ### Delete 'Scenario' records ```http DELETE https://{accountId}.api.nextspace.host/v1/entity/historicData?Scenario={scenario}&entityId={entity_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `Scenario={scenario}` | query | yes | Scenario ID or key to get. | | `entityId={entity_id}` | query | yes | Related Entity ID. Specify this query param multiple times to delete multiple records. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/v1/entity/historicData"; const method = "delete"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apientityscenarios # Project Views (/apiprojectview) # Project Views Project Views are records that hold configured Menu Items used to load different data into Navigator's scene. Bookmark records sit alongside Project Views to snapshot particular configurations of the scene with different enabled Menu Items. Note that "Slide" is a legacy term for "Bookmark" so you will see it occasionally in the API. ## Data models Below you will find the data models for both Project Views and Bookmarks. ```typescript interface IProjectView { // ID of the project view. // This is a unique identifier you can pick yourself to create new records. // We usually use a short alphanumeric string. However you can use simple easier to remember strings. ID: string; // Human readable name of the Project View. Name: string; // Human readable description of the Project View. Description?: string; // (20) Indicates the type of Project View this is. // This is used to determine the expected settings and application. Eg: 3D vs 2D Navigator. Type?: "WEB_3D_NAVIGATOR" | string; // ID of the user that created the Project View. "CreatedByUser.ID"?: string; // Created/updated date/time in ISO 8601 UTC. Created?: string; Updated?: string; // Version of the UI that created this project view. // Set to -1 if unknown. CreatedByUIVersion?: string; // Settings for the project view. Settings?: any; // Depends on the Type. Documented further down. } ``` ```typescript interface IBookmark { // ID of the Bookmark record. ID?: string; // Human readable title of the Bookmark. Title?: string; // Human readable description of the Bookmark. Note?: string; // ID of the Client File used for the Bookmark thumbnail. // Must be set. Use an empty string if no thumbnail is desired. "Screenshot.ClientFile.ID"?: string; // ID of the associated Project View. "UI.View.ID"?: string; // ID of the user who created the Bookmark. "Created.ByUser.ID"?: string; // Bookmark grouping ID. "UI.SlideGroup.ID"?: number; // Display order related to sibling Bookmarks. DisplayOrder?: number; // Bookmark camera position. Camera?: any; // Depends on the type. Documented further down. // Settings for the Bookmark. Settings?: Settings; // Depends on the type. Documented further down. // Created/updated date/time in ISO 8601 UTC. Created: string; Updated?: string; } ``` ## Type 3D Navigator This is our typical Project View you would experience using Navigator. Below are the details for the settings and camera position. ```typescript interface IProjectView { ... Settings?: { // If true, anonymous users won't be able to access the view. IsLoginRequired?: boolean; // If true, users must have the permission "UIVIEW_" to access the view. IsRestricted?: boolean; // CSS color string to use when selecting Entities. // Default is yellow. selectionColor?: string; // CSS color string to use when highlighting Entities. // This is typically when you hover over rows such as the Scene Tree Entities. // Default is a light blue. highlightColor?: string; // Available Menu Items for users to toggle, or for Bookmarks to enable. menuItem?: any[]; // Will be documented separately as it's a larger topic. // Available basemaps for users to toggle, or for Bookmarks to enable. // When not specified, we will provide a default list. imageries?: { // ID of the Tileset to load. // This can either be an imported geotiff, a setup external source, or a default ID for the defaults we provide. tilesetId: string; // If you manage multiple Nextspace accounts, you can share imports basemaps by specifying where to load one from. // Keep empty, or set to your account's (organization) ID. accountId?: string; }[]; // Available terrains for users to toggle, or for Bookmarks to enable. // When not specified, we will provide a default list. terrains?: { // ID of the Tileset to load. // This can either be an imported geotiff, a setup external source, or a default ID for the defaults we provide. tilesetId: string; // If you manage multiple Nextspace accounts, you can share imports terrrains by specifying where to load one from. // Keep empty, or set to your account's (organization) ID. accountId?: string; }[]; // Branding settings specific to this Project View. // These compliment the account-level branding settings. branding?: { // Client File ID for the Project View's banner. // In Navigator this is found within the Navigator's dashboard. bannerFileId?: string; } // Presentation settings. // These are referenced when a Project View is 'presented'. present?: { // Time in milliseconds to hold bookmarks before moving to the next bookmark. // This is a default time and can be overridden by the bookmark's duration. bookmarkDuration?: number; } // Entity selection settings. // These can be specified to have alternative panels for selected Entities. selection?: { // Specify what should occur by default when an Entity is selected. // When not set, the default of action of INFO_VIEW (typical selected Entity panel) is used. defaultAction: ISelectionAction; // See below for the details. // Array of Entity Type mappings to specify what should occur when an Entity of that type is selected. // When an action is not found, the default one is referenced. entityTypeMapping?: ISelectionAction[]; } // Plugin related settings. // Plugins are custom code packages that can be loaded into Navigator. plugins?: { // Enabled Plugin IDs. enabledIds: string[]; }[]; } } /** * These are the params related to the "selection" settings found in the Project View. */ interface ISelectionAction { // Null = any Entity Type. // This is the Entity Type ID to match against. entityTypeId?: string; // This is the action to perform on user-click. // A NONE action will stop on-hover pointer cursors and a panel from appearing. action: "INFO_VIEW" | "CUSTOM_FORM" | "PLUGIN" | "NONE"; // These are the params related to the action. actionParams?: undefined | IActionInfoView | IActionCustomForm | IActionPlugin; // (Optional) alternative schema ID. // This loads a different Data Schema when the Entity details are presented. schemaId?: string; } /** * These are the params related to the "info-view" action. * This is the Nextspace default entity details panel. */ interface IActionInfoView { // Optional parameter to specify what tabs within the panel should be enabled. // By default, all tabs are available. // Null or empty means all tabs should be available. tabs?: ("SUMMARY" | "ATTRIBUTES" | "RELATIONSHIPS" | "FILES" | "MEDIA" | "COMMENTS" | "GRAPHICS" | "LODS")[]; // If the version is not >=1 then code will consider "ATTRIBUTES" tab "available". // This is because it is a new tab added in version 1. dataVersion?: number; } /** * These are the params related to the "custom-form" action. */ export interface IActionCustomForm { // This is the ID of the Custom Form to display. formId: number; } /** * These are the params related to the "plugin" action. * This is custom code that can be executed in place of the form's content. */ export interface IActionPlugin { pluginId: string; } ``` ```typescript interface IBookmark { ... Camera?: { // Meters relative to ground. altitude: number; // In degrees. latitude: number; longitude: number; heading: number; pitch: number; roll: number; // If frustum is different between two bookmarks, transition should be instant. // 0 = Perspective, 1 = Orthographic. // Default is 0. cameraFrustum?: 0 | 1; }; Settings?: { // Default is WEB_3D. This will render the typical CesiumJS experience. // IFRAME will render an embedded iframe in the bookmark. contentType?: "WEB_3D" | "IFRAME"; // This is the URL to the iframe content. // This is only used when contentType = IFRAME. iframeUrl?: string; // Below are settings related to the WEB_3D content type. // (Optional) array of Menu Items to enabled. // When a Menu Item is enabled, child items are also enabled. menuItemIds?: string[]; // (Optional) ISO 8601 datetime string to load the scene at a specific time. // String itself includes timezone information. Navigator will create a UTC string when saving. dateTime?: string; // (Optional) timeline settings. timeline?: { // ISO 8601 date time string for the start of the timeline. start: string; // ISO 8601 date time string for the end of the timeline. end: string; // Indicates that time should move between start and end at the provided speed. playing: boolean; // 1 = 1 second per second. speed: number; } // (Optional) array of enabled basemaps. An empty array is allowed to disable all basemaps. imagery?: { // ID of the Tileset to load. // This can either be an imported geotiff, a setup external source, or a default ID for the defaults we provide. tilesetId: string; // If you manage multiple Nextspace accounts, you can share imports basemaps by specifying where to load one from. // Keep empty, or set to your account's (organization) ID. accountId?: string; // 0 - 1, 1 = 100% opacity. // Default = 1. alpha?: number; // Default = 1. brightness?: number; // Default = 1. saturation?: number; // Default = 1. contrast?: number; // Default = 1. gamma?: number; // Default = 0. hue?: number; }[]; // (Optional) enabled terrain. // When unspecified, a default will be loaded (usually flat terrain). // Only one terrain is enabled at a time. terrain?: { // ID of the Tileset to load. // This can either be an imported geotiff, a setup external source, or a default ID for the defaults we provide. tilesetId: string; // If you manage multiple Nextspace accounts, you can share imports terrrains by specifying where to load one from. // Keep empty, or set to your account's (organization) ID. accountId?: string; }; // (Optional) if true, the terrain will be displayed as a wireframe. // This is mostly a debug feature. terrainWireframe?: boolean; // (Optional) CSS color to apply to the globe underneath the basemaps. // Default is a dark blue. globeColor?: string; // (Optional) opacity of the globe color. // 0 = 0%, 1 = 100%. Default is 100%. // This lets you see underground data in a more natural way, hover stops position picking from working. globeAlpha?: number; // (Optional) if true, the globe will be hidden. Default is false. // This is typically used for Google Photogrammetry data to avoid it clashing with the globe. globeHidden?: boolean; // (Optional) if ground occlusion should be enabled. // Ground occlusion stops Entities under the terrain from being visible. groundOcclusion?: boolean; // (Optional) shadow settings. shadows?: { // If true, shadows are enabled. enabled: boolean; // If true, shadow edges are softened. soften: boolean; // Pixel size determines shadow quality. // This typically increments in sets of 1024. pixelSize: number; // Darkness of shadows. // Default is "0.3". darkness: number; } // (Optional) ambient occlusion settings. ambientOcclusion?: { enabled: boolean; intensity: number; lengthCap: number; bias: number; blurStepSize: number; stepSize: number; } // (Optional) lighting settings. lighting?: { // Colour of the sun. color: string; // Intensity of the sun. intensity: number; } // (Optional) This enables lighting that makes terrain elevation apparent. hillShades?: boolean; // (Optional) quality settings. quality?: { // If true, "smooth rendering" is enabled. fxaa: boolean; } // (Optional) array live Entities that will ping for changes occasionally. // It's recommended to keep this list small (around 30 at most). liveEntityIds?: string[]; // (Optional) array of Entity 'states'. states?: { // Entity ID to tie the state to. entityId: string; // This is considered a 'precise' value when set and therefor values will override the unspecified ones. // Eg: If Entity ID + Menu Item ID is set to hidden, and another isn't, the Entity will be hidden. menuItemId?: string; // Value between 0 and 1. 1 = 100% opacity. opacity?: number; // If the Entity should be hidden. hidden?: boolean; // If the Entity should have a selection colour applied. // This will also make the selection panel appear. selected?: boolean; // If the Entity is isolated. // When at least one Entity is isolated, only isolated Entities are visible. isolated?: boolean; // If the Entity is labelled. // This draws a label on the Entity based on its name and "additional labels" settings in the Entity Type. labelled?: boolean; // If the Entity is highlighted. // The selection colour takes priority over this. highlighted?: boolean; }[]; }; } ``` ## Project View requests Below are the basic requests for managing Project View records. ### Get Project Views ```http GET https://{accountId}.api.nextspace.host/ui.view/list ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Response** ```typescript interface IResponse { Items: IProjectView[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.view/list"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Get Project View by ID ```http GET https://{accountId}.api.nextspace.host/ui.view/{view_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{view_id}` | path | yes | Project View ID to retrieve the record for. | **Response** ```typescript // Response is the Project View record directly. // See the data model above for details. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.view/{view_id}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Create Project View ```http POST https://{accountId}.api.nextspace.host/ui.view/{view_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{view_id}` | path | yes | Project View ID to create. | **Request body** ```typescript // Body is the Project View record directly. // Enter the new ID yourself. // We usually do a short alphanumeric string. However you can use simple easier to remember strings. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.view/{view_id}"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Update Project View ```http PUT https://{accountId}.api.nextspace.host/ui.view/{view_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{view_id}` | path | yes | Project View ID to update. | **Request body** ```typescript // Body is the Project View record directly. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.view/{view_id}"; const method = "put"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Delete Project View by ID ```http DELETE https://{accountId}.api.nextspace.host/ui.view/{view_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{view_id}` | path | yes | Project View ID to delete. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.view/{view_id}"; const method = "delete"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Bookmark requests Below are the basic requests for managing Bookmark records. Note that "Slide" is a legacy term for "Bookmark". ### Get Project View Bookmarks ```http GET https://{accountId}.api.nextspace.host/ui.view/{view_id}/slides ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{view_id}` | path | yes | Project View ID to retrieve the Bookmarks for. | **Response** ```typescript interface IResponse { Items: IBookmark[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.view/{view_id}/slides"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Get Bookmark by ID ```http GET https://{accountId}.api.nextspace.host/ui.view/{view_id}/slide/{slide_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{view_id}` | path | yes | Project View ID to retrieve the record for. | | `{slide_id}` | path | yes | Bookmark ID to retrieve the record for. | **Response** ```typescript // Response is the Bookmark record directly. // See the data model above for details. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.view/{view_id}/slide/{slide_id}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Create / Update Bookmark ```http POST https://{accountId}.api.nextspace.host/ui.view/{view_id}/slide/{slide_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{view_id}` | path | yes | Project View ID the Bookmark is related to. | | `{slide_id}` | path | no | Bookmark ID to create. Leave blank to create a new one. | **Request body** ```typescript // Body is the Bookmark record directly. // Exclude the ID when creating a new Bookmark record. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.view/{view_id}/slide/{slide_id}"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Delete Bookmark by ID ```http DELETE https://{accountId}.api.nextspace.host/ui.view/{view_id}/slide/{slide_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{view_id}` | path | yes | Project View ID the Bookmark is related to. | | `{slide_id}` | path | yes | Bookmark ID to delete. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.view/{view_id}/slide/{slide_id}"; const method = "delete"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Update Bookmark display order ```http POST https://{accountId}.api.nextspace.host/ui.view/{view_id}/slides/setOrder ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{view_id}` | path | yes | Project View ID the Bookmarks are related to. | **Request body** ```typescript interface IRequest { // The starting display order value for the first Bookmark. // This is typically 0. "DisplayOrder.Start": number; // Bookmark IDs in their new order. "UISlide.ID": string[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.view/{view_id}/slides/setOrder"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Bookmark groups You can optionally organize your Bookmarks into folders by using Bookmark Groups. Groups will appear above ungrouped Bookmarks in panels. ```typescript interface IBookmarkGroup { // Unique ID of the group. ID?: number; // Name of the group. Name: string; // Display order of the group. DisplayOrder: number; } ``` ### Get Bookmark Groups ```http GET https://{accountId}.api.nextspace.host/ui.view/{view_id}/slideGroups ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{view_id}` | path | yes | Project View ID to retrieve the Bookmark Groups for. | **Response** ```typescript interface IResponse { Items: IBookmarkGroup[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.view/{view_id}/slideGroups"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Create / Update Bookmark Group ```http GET https://{accountId}.api.nextspace.host/ui.view/{view_id}/slideGroup/{group_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{view_id}` | path | yes | Project View ID related to the Bookmark Groups. | | `{group_id}` | path | no | Bookmark Group ID to create or update. Leave blank or set to 0 to create a new one. | **Request body** ```typescript // Body is the Bookmark Group record directly. // Exclude the ID when creating a new Bookmark Group record. ``` **Response** ```typescript // Response is the Bookmark Group record directly. // See the data model above for details. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.view/{view_id}/slideGroup/{group_id}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Delete Bookmark Group ```http DELETE https://{accountId}.api.nextspace.host/ui.view/{view_id}/slideGroup/{group_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{view_id}` | path | yes | Project View ID related to the Bookmark Groups. | | `{group_id}` | path | yes | Bookmark Group ID to delete. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.view/{view_id}/slideGroup/{group_id}"; const method = "delete"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Update Bookmark Groups display order ```http POST https://{accountId}.api.nextspace.host/ui.view/{view_id}/slideGroups/setOrder ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{view_id}` | path | yes | Project View ID the Bookmark Groups are related to. | **Request body** ```typescript interface IRequest { // Bookmark Group IDs in their new order. "UI.SlideGroup.ID": number[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.view/{view_id}/slideGroups/setOrder"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apiprojectview # Menu Items (/apimenuitem) # Menu Items Menu Items are configured toggles for the user or Bookmarks to enable in a Project View. Menu Items don't have their own records. They sit within their Project View record. ## Base settings Below are the settings that are consistent between the different Menu Item types. ```typescript interface IMenuItem { // Unique ID for this Menu Item. id: string; // Human readable name for this Menu Item. Caption: string; // Child Menu Items. // These should only be set if the Type is null, or set to 'NONE'. Children?: IMenuItem[]; // The type of Menu Item. // This will drive the different actions and configuration settings. Type: EType; // Priority in relation to other Menu Items. // If two Menu Items draw the same Entity, the higher priority is drawn and the other is hidden. // If the priority is equal, BOTH are drawn at the same time. renderPriority?: number; } enum EType { // Container for other menu items. None = "NONE", // Renders Nextspace Entities based on an Entity Type ID and optional attribute filter. Entities = "BruceEntity", // Renders Entities matching specific IDs. EntitiesIds = "EntitiesIds", // Renders a Cesium Tileset made from importing an assembly file into Nextspace. CadTileset = "ModelTileset", // Renders legacy Nextspace tilesets. // This is primarily used for static Tilesets as they don't have an upgraded version yet. ArbTileset = "ArbitraryTileset", // Renders a Cesium Tileset by Ion asset ID. IonTileset = "Cesium3DTileset", // Renders a Cesium Tileset made from arbitrary Nextspace Entities. EntityTileset = "EntitiesTileset", // Renders a Cesium Point Cloud imported from LAS/LAZ file into Nextspace. PointCloud = "PointCloudTileset", // Renders Cesium OSM Buildings. Osm = "OSMBuildingsTileset", // Renders the Cesium Google photogrammetry tileset. GooglePhotoTileset = "GooglePhotoTileset", // Renders the result of a Google search. // Eg: the Menu Item can be configured to render 'restaurants' around the viewport. GoogleSearch = "GoogleSearch" } ``` ## Loading Entities Below are the Menu Items for loading Nextspace Entity data. ```typescript // Loading Entities around the viewport based on the configured Entity Type. interface IMenuItem { "Type": "BruceEntity"; "BruceEntity": { // ID of the Entity Type to query. "EntityType.ID": string; // (Optional) Scenario Key/ID to use when requesting Entities. // Scenario attributes will be overlaid if found. "Scenario"?: number; // (Optional) If true, the Menu Item will request historic Entities. // The scene's datetime will be included in the request to overlay historic attributes. // When set, the Menu Item will also react to the scene's time changing to update the Entities. historic?: boolean; // (Optional) alternative schema to request. // This is used to calculate read-only Entity data under a different Data Schema. schemaId?: string; // Entity attribute filter to apply. // This will be combined with a view-port boundary search when requesting Entities. // See Entities documentation around "Requesting list with an attribute query" for more information. "Filter"?: any; } // The historic settings are only used when the 'historic' flag is set to true. // If true, historic Entities will interpolate between records based on current scene time. // This will slow down initial render as more data will be required for the render. historicInterpolation?: boolean; // If true, historic point/icon/model Entities will draw their movement as a polyline. // Polyline styling will apply. historicDrawTrack?: boolean; } ``` If you want to keep things simple and render a specific set of Entities, you can use this type: ```typescript // Loading specific array of Entity IDs. interface IMenuItem { "Type": "EntitiesIds"; "BruceEntity": { // Array of Entity IDs to load. EntityIds: string[]; } } ``` Here is how you can control the appearance of the Entities through Style records. Both of the above Menu Items support this. ```typescript interface IMenuItem { // Extending the above data models. ... // Defining what Style should be applied between different distances from the camera. // The maximum distance will determine how far away before the Entities are hidden from view. "CameraZoomSettings": { // The minimum distance in meters from the camera to enable this setting. MinZoom: number, // The maximum distance in meters from the camera to enable this setting. MaxZoom: number, // The display type to use for this setting. // This will determine the "best" allowed rendering type. // For example, if you display as "3D", then it will first try find a 3D model, then geometry, then point. // If you display as "geometry", it will first try find a geometry, then point and skip 3D models. DisplayType?: "hidden" | "point" | "geometry" | "3d", // The Style to use for this item. // If unset it will locate the default Style for the Entity Type. // If that is missing, then hard-coded defaults will apply. StyleID?: number, // The LOD category to use for this item. // When unset, it will be GLB to get GLB or GLTF 3D model files. LODCategoryID?: string, // The LOD level to use for this item. // When unset, it will be 0 for the highest quality. LODLevel?: number | string; }[]; } ``` ## Loading Tilesets Below is how you can load different kinds of Tilesets. This includes our imported assemblies (IFC, RVT, etc), point clouds (LAS/LAZ), and other Cesium tilesets that aren't terrains or basemaps. ```typescript // Loading an assembly (IFC, RVT, etc). interface IMenuItem { "Type": "ModelTileset"; "tileset": { // ID of the Tileset record to load. "TilesetID": string; } // An assembly can contain Entities from a range of Entity Types. // As a result, we provide a way to map different Styles by Entity Type ID> // Mapping for Entity Type IDs and their Styles to load. "StyleMapping"?: { // The Entity Type ID to map to a Style ID. "EntityTypeID": string; // The Style ID to use when colouring the Entities. "StyleID": number; }[]; // The default Style to use when a specific mapping is not available. // If unset it will locate the default Style for the related Entity Types. "styleId"?: number; // Setting for how the style colours should be blended. StyleBlending?: { // Hightlight = 0, replace = 1, mix = 2. blendMode: 0 | 1 | 2; // 1 = 100% new colour. 0 = original colour. blendPercent: number; }; } ``` ```typescript // Loading a Point Cloud. interface IMenuItem { "Type": "PointCloudTileset"; "tileset": { // ID of the Tileset record to load. "TilesetID": string; } } ``` ```typescript // Loading a static Tileset or Cesium Ion asset. interface IMenuItem { "Type": "Cesium3DTileset" | "ArbitraryTileset"; // If set, an ion asset is loaded.. IonResource?: { AssetID: string | number; // If not supplied then the Client Account default is used. AccessToken?: string; } // If set, a Nextspace static Tileset is loaded. tileset?: { // ID of the static Tileset to load. "TilesetID": string; // Alternatively, you can use an external URL to the "tileset.json" file. // This is not recommended for accessing Nextspace Tilesets. TilesetURL?: string; } // If true, it will disable the memory watcher that distributes a max-memory allowance between Tilesets. // This is occasionally needed for poorly optimized Tilesets. // Default is false. noMaximumMemory?: boolean; // If a colour mask should be applied. applyMask?: boolean; // Css color string to apply to the tileset. // This follows Cesium logic for when color is pure white, // that means don't apply colour and only apply alpha. colorMask?: string; } ``` ## Loading OSM 3D Buildings This is a data-set provided by Cesium. Learn more about IP rights and usage at [Cesium OSM Buildings](https://cesium.com/platform/cesium-ion/content/cesium-osm-buildings/). This Menu Item will load the Cesium OSM Buildings tileset into the scene. It is recommended to pair this with the Cesium World Terrain to ensure buildings are on the ground. ```typescript // Loading a Point Cloud. interface IMenuItem { "Type": "OSMBuildingsTileset"; BruceEntity?: { // Preferred Entity Type ID for making new Entity records. // When a graphic is selected in UI and no record is found, this type ID will be used as the suggested type. "EntityType.ID"?: string; // Source record for loading and saving the mapping between OSM IDs and Nextspace IDs. // If not set then Entity selection will not work. // While documentation is not available, create a new 'Simple ID mapping' Data Source through Operator and use that ID. // Keep the ID consistent between different projects to maintain the same mapping. SourceId: string; }; // If enabled then pending OSM buildings will be partially transparent. // A sourceId must be set for this to work. // A building is considered pending while it hasn't checked for a corresponding Nextspace ID. ghostLoading?: boolean; } ``` ## Loading Google 3D Photorealistic tiles This is a data-set provided by Cesium and Google. Learn more about IP rights and usage at [Google 3D Photorealistic tiles](https://cesium.com/learn/photorealistic-3d-tiles-learn/). Your account (organization) must be configured with an API key to use this feature. See the 'application keys' panel in Operator for more information. This Menu Item will the Google photogrammetry tileset into the scene. It is recommended to hide the globe when this is enabled. ```typescript // Loading a Point Cloud. interface IMenuItem { "Type": "GooglePhotoTileset"; // Css color string to apply to the tileset. // This follows Cesium logic for when color is pure white, // that means don't apply colour and only apply alpha. colorMask?: string; } ``` ## Loading Google Places This is a data-set provided by Google. Learn more about IP rights and usage at [Google Places API](https://developers.google.com/maps/documentation/places/web-service/overview). Your account (organization) must be configured with an API key to use this feature. See the 'application keys' panel in Operator for more information. This Menu Item will load Google places based on a search keyword around the viewport. ```typescript // Loading a Point Cloud. interface IMenuItem { "Type": "GoogleSearch"; // Search keyword, eg: 'restaurants'. keyword: string; // Google API Key to use. // If not provided then the account default will be used. accessToken?: string; } ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apimenuitem # Project View Data (/apiprojectviewdata) # Project View Data To avoid the need for parsing Project View Menu Items to perform changes to what Entity Types and Tilesets are loadeded. We have a dedicated endpoint for performing bulk changes. This endpoint will match a given Entity Type ID or Tileset ID and update all related Menu Items to a new ID. Alternatively, you can delete all related Menu Items for a given Entity Type or Tileset. ## Requests Below are the endpoints to reference. ### Update a single Project View ```http POST https://{accountId}.api.nextspace.host/ui.view/{view_id}/relatedData ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{view_id}` | path | yes | Project View ID to we're updating. | **Request body** ```typescript interface ITypeChangeRequest { // Entity Type ID we're updating. "EntityType.ID": string; // The new Entity Type ID to swap related Menu Items to. "New.EntityType.ID"?: string; // If true, the related Menu Items will have their captions updated to the new type's name. "UpdateCaptions"?: boolean; // If true, the related Menu Items will be deleted instead of updated. // The newTypeID and captions don't need to be provided if this is true. "Delete"?: boolean; } interface ITilesetChangeRequest { // Tileset ID we're updating. "Tileset.ID": string; // The new Tileset ID to swap related Menu Items to. "New.Tileset.ID"?: string; // If true, the related Menu Items will have their captions updated to the new tileset's name. "UpdateCaptions"?: boolean; // If true, the "New.Tileset.ID" doesn't need to be provided. // Instead, we look for the latest Tileset with a matching name and use it if it's newer than the current one. "Predictive"?: boolean; // If true, the related Menu Items will be deleted instead of updated. // The newTilesetID and captions don't need to be provided if this is true. "Delete"?: boolean; } interface IBody { // List of changes. "Items": (ITypeChangeRequest | ITilesetChangeRequest)[]; } ``` **Example request body** ```json { "Items": [ { "EntityType.ID": "my-existing-entity-type-id", "New.EntityType.ID": "my-new-entity-type-id" }, { "Tileset.ID": "my-existing-tileset-id", "New.Tileset.ID": "my-new-tileset-id", "UpdateCaptions": true }, { "EntityType.ID": "entity-type-id-to-remove", "Delete": true }, { "Tileset.ID": "tileset-id-to-remove", "Delete": true }, { "Tileset.ID": "my-existing-tileset-id", "Predictive": true } ] } ``` **Response** ```typescript // Response is the same for one view and multiple views being updated. // So for one view, you mainly change for 0/1 counter to see if anything was updated. interface IResponse { // Specific view IDs that were updated. "Updated.UIView.ID": string[]; // Simple counter of View IDs that were updated. "Updated.Count": number; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.view/{view_id}/relatedData"; const method = "post"; const token = "your-token"; const body = { "Items": [ { "EntityType.ID": "my-existing-entity-type-id", "New.EntityType.ID": "my-new-entity-type-id" }, { "Tileset.ID": "my-existing-tileset-id", "New.Tileset.ID": "my-new-tileset-id", "UpdateCaptions": true }, { "EntityType.ID": "entity-type-id-to-remove", "Delete": true }, { "Tileset.ID": "tileset-id-to-remove", "Delete": true }, { "Tileset.ID": "my-existing-tileset-id", "Predictive": true } ] }; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Update multiple or all Project Views ```http POST https://{accountId}.api.nextspace.host/ui.view/relatedData ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Request body** ```typescript interface IBody { // List of changes (same as singular view request). "Items": (ITypeChangeRequest | ITilesetChangeRequest)[]; // Array of view IDs to apply the changes to. // Send ["*"] to update all Project Views. "View.ID": string[]; } ``` **Response** ```typescript // Same as singular view update. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/ui.view/relatedData"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apiprojectviewdata # Custom Forms (/apicustomform) # Custom Forms Custom Forms are panels you can customize to display an alternative view of your Entity data. Custom Forms are built up with a series of components that we provide in addition to settings for each component. If this doesn't suit your needs, you can look at our Plugin system where you can load your own Javascript code. ## Data models Below you will find the data model for Custom Form records. ```typescript interface ICustomForm { // Custom form ID. // Exclude this value for new records. ID?: number; // This is the Entity Type ID that this custom form is associated with. "EntityType.ID"?: string; // Human readable custom form name. Name?: string; // Custom form type. This helps identify how to parse the settings. // The only type that's supported is "COMPONENT_STACK", other types are currently deprecated. Type?: "COMPONENT_STACK"; // Custom form content. Settings?: { // If null then it'll use the default value within the application. // For Navigator this is 430px (subject to change). size?: { // Value is in pixels. width: number; }, // Title is the Entity and Entity Type's names. // Default = true. includeTitle?: boolean; // Tools is the toolbar that includes buttons such as delete, fly, etc. // Default = true. includeTools?: boolean; // The stack of components that make up the form. // See below for the stack item data model. stack: IStackItem[]; } } /** * Represents a component in the Custom Form stack. */ interface IStackItem { // Indicates what component should be loaded. component: "STATIC_TEXT" | "ENTITY_TEXT" | "IFRAME" | "LINK" | "ENTITY_GALLERY" | "ENTITY_ATTRIBUTES" | "ENTITY_ATTRIBUTE_GROUP" | "ENTITY_COMMENTS" | "BUTTON" | "NUMBER_CHART" | "NUMBER_SERIES_CHART" // Params are based on the component type. // See below for each component's params. // Not all components require params, for example "ENTITY_GALLERY" will load the typical Entity gallery- // that you see in the default Info View. params?: any[]; } interface IStaticTextParams { // Typed text by user we populate box with. text: string; } interface IEntityTextParams { // Path to attribute we want to populate this text with. // Attribute path segments are separated by a forward slash. Eg: 'address/city'. attrPath: string; } interface IIFrameParams { // URL to load in the iframe. // Use ${"your_attribute_path"} to substitute in the attribute value of the Entity. url: string; // Height of the iframe in pixels. height: number; } interface ILinkParams { // URL to navigate to. // Use ${"your_attribute_path"} to substitute in the attribute value of the Entity. url: string; // Label to display on the link. label: string; } /** * The button component is used to perform some sort of navigation or action. * Navigation is between other forms or the default info-view dialog. */ interface IButtonParams { action: "OPEN_FORM" | "OPEN_INFO_VIEW"; // Custom Form ID to load if the action is to open a form. formId?: number; // Text to display on the button. // Can be empty if you want to just display an icon. buttonText?: string; // Expected is a font-awesome icon. // Eg: "fas fa-plus". buttonIcon?: string; // If the icon should be on the left or right of the text. buttonIconSide?: "LEFT" | "RIGHT"; } /** * This is an alternative to the entity attributes section. * The other one will use the entity type's schema to display all attributes whereas this one will display specific ones. */ interface IEntityAttrGroupParams { items: IAttrRow[]; } interface IAttrRow { // Attribute path segments are separated by a forward slash. Eg: 'address/city'. attrPath: string; } interface INumberChartParams { /** * The kind of chart to draw. * * Bar = rectangle that fills up based on the value. * Battery = rectangle that fills up based on the value, but has a battery shape. * Thermometer = rectangle that fills up based on the value, but has a thermometer shape. * Needle Gauge = a gauge that fills up based on the value. Similar look to a speedometer. */ type: "BAR" | "BATTERY" | "THERMOMETER" | "NEEDLE_GAUGE" | "LARGE_NUMBER" | "ON_OFF_SWITCH"; // Static min value to use. This will be used if the attribute is not provided, or if the attribute is not a number. minValue?: number; // Path to the attribute that holds the chart's min value. minValueAttrPath?: string; // Static max value to use. This will be used if the attribute is not provided, or if the attribute is not a number. maxValue?: number; // Path to the attribute that holds the chart's max value. maxValueAttrPath?: string; // Suffix to append to the value. Eg: "%" to turn the number 5 into "5%". textSuffix?: string; // Path to the attribute that holds the chart's value. // Attribute path segments are separated by a forward slash. Eg: 'address/city'. valueAttrPath: string; // Default value to use if the attribute is null or not a number. value?: number; // Array of colors to use for the chart. /* Example: [ { percent: 0, color: "red" }, { percent: 50, color: "yellow" }, { percent: 100, color: "green" } ] */ colors?: { percent: number; color: string; }[]; // Default = false. // If true then the chart will be a gradient of colours instead of solid colours. gradient?: boolean; // Chart title. This will be displayed above the chart. title?: string; } /** * A series chart differs to a number chart in that it displays how a value changes over time. * If configured, it will sample a historic-attribute's history. */ interface INumberSeriesChartParams { // The kind of chart to draw. type: "LINE"; // Path to the attribute that holds the chart's value. // Attribute path segments are separated by a forward slash. Eg: 'address/city'. valueAttrPath: string; // Default value to use if the attribute is null or not a number. value?: number; // Chart title. This will be displayed above the chart. title?: string; // Suffix to append to the value. Eg: "%" to turn the number 5 into "5%". textSuffix?: string; } ``` ## Custom Form requests Below are the basic requests for managing Custom Form records. ### Get Custom Forms ```http GET https://{accountId}.api.nextspace.host/entityType/{type_id}/customForms ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{type_id}` | path | yes | Entity Type ID to retrieve the records for. | **Response** ```typescript interface IResponse { // Array of Custom Form records. // See the data model above for the record structure. Items: ICustomForm[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entityType/{type_id}/customForms"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Get Custom Form by ID ```http GET https://{accountId}.api.nextspace.host/customForm/{form_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{form_id}` | path | yes | Custom Form ID to retrieve the record for. | **Response** ```typescript // The response is the Custom Form record directly. // See the data model above for the record structure. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/customForm/{form_id}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Create / Update Custom Form ```http POST https://{accountId}.api.nextspace.host/customForm/{form_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{form_id}` | path | no | Custom Form ID to create or update. Exclude or set to 0 for new records. | **Request body** ```typescript // The request is the Custom Form record directly. // Exclude or set the ID to 0 for new records. ``` **Response** ```typescript // The response is the Custom Form record directly. // See the data model above for the record structure. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/customForm/{form_id}"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Delete Custom Form by ID ```http DELETE https://{accountId}.api.nextspace.host/customForm/{form_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{form_id}` | path | yes | Custom Form ID to delete. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/customForm/{form_id}"; const method = "delete"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apicustomform # Entity Relationship Types (/apirelationtypes) # Entity Relationship Types A Relationship Type is a classification of a Relationship between two Entity records. It gives a human-readable way to describe how your Entities relate to each-other. The most simple Relationship Type is "Parent" and "Child" where you provide the name and reverse-name of "parent of" and "child of". This allows us to create human-readable sentences that describe your records. Entity A is parent of Entity B. Entity B is child of Entity A. Some Relationship Types are generated automatically and serve an internal purpose. For example the ID `COMMENTS_REPLY` is reserved for relating Comment records to their replies. `BruceRsrvContains` is reserved for our Assembly imports for parent/child relationships that have relative positioning. `BruceRsrvInstanceOf` is a legacy Assembly Relationship Type. ## Relationship Type requests Below are the basic requests for managing Relationship Types. ### Get Relationship Types ```http GET https://{accountId}.api.nextspace.host/entityRelationTypes ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | **Response** ```typescript interface IResponse { Items: { // Unique ID for the Relationship Type. "ID": string; // Human readable name. "Name": string; // Forward name to use in a sentence. "ForwardName": string; // Reverse name to use in a sentence. "ReverseName": String; // Default Style ID. // This guides rendering the lines/parabolas between records in 3D. "EntityDisplaySettingsID"?: number; // Default Entity Type ID. // This is used as the default Entity Type when creating Entity data records for Relationships. "Relation.EntityType.ID"?: string; }[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entityRelationTypes"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Create / Update Relationship Type ```http POST https://{accountId}.api.nextspace.host/entityRelationType/{type_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{type_id}` | path | yes | ID of the Relationship Type to create/update. | **Request body** ```typescript interface IBody { // Human readable name. "Name": string; // Forward name to use in a sentence. "ForwardName": string; // Reverse name to use in a sentence. "ReverseName": String; } ``` **Response** ```typescript // Body is a JSON object with the same properties as a single item from the list above. // It returns a single Relationship Type record directly. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entityRelationType/{type_id}"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Delete Relationship Type ```http DELETE https://{accountId}.api.nextspace.host/entityRelationType/{type_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{type_id}` | path | yes | ID of the Relationship Type delete. | **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entityRelationType/{type_id}"; const method = "delete"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apirelationtypes # Entity Relationships (/apirelations) # Entity Relationships An Entity Relationship record is a connection between two Entity records. It can be used to retrieve a hierarchy of records, draw the connections between records in 3D, and to view the hierarchy as a 2D diagram. It's recommended to read the Relationship Type docs [here](/apirelationtypes) first. When requesting Entity records using `v3/entity/{entity_id}` or `v3/entity/entities`, you can include the query param `?$expand=relation` to include child Relationship records for the returned Entity records in the response. They will be inside `Bruce/Relations` in the response JSON. ## Relationship requests Below are the basic requests for managing Relationships. ### Get Entity Relationships ```http GET https://{accountId}.api.nextspace.host/entity/{entity_id}/relations/ ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | ID of the Entity to get Relationships for. | **Response** ```typescript interface IResponse { Items: { // ID of the parent Entity. "Principal.Entity.ID": string; // ID of the child Entity. "Related.Entity.ID": string; // Relationship Type ID. "Relation.Type.ID": string; // (Optional) ID of the data Entity. // The data Entity is where the Relationship attribute data is stored. "Data.Entity.ID"?: string; }[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/{entity_id}/relations/"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Get Entity Relationships by Type ```http GET https://{accountId}.api.nextspace.host/entity/{entity_id}/relations/{type_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | ID of the Entity to get Relationships for. | | `{type_id}` | path | yes | ID of the Relationship Type to get Relationships for. | **Response** ```typescript // Same response body as the above get-list. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/{entity_id}/relations/{type_id}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Update Relationship ```http POST https://{accountId}.api.nextspace.host/entity/${parent_id}/otherEntityID/{child_id}/relation/{type_id}/update ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{child_id}` | path | yes | ID of the child Entity. | | `{type_id}` | path | yes | ID of the related Relationship Type. | **Request body** ```typescript interface IBody { // (Optional) ID of the data Entity. // The data Entity is where the Relationship attribute data is stored. "Data.Entity.ID"?: string; } ``` **Response** ```typescript // Body is a JSON object with the same properties as a single item from the get-list above. // It returns a single Relationship record directly. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/${parent_id}/otherEntityID/{child_id}/relation/{type_id}/update"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Update Relationship ```http POST https://{accountId}.api.nextspace.host/entity/${parent_id}/otherEntityID/{child_id}/relation/{type_id}/update ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{child_id}` | path | yes | ID of the child Entity. | | `{type_id}` | path | yes | ID of the related Relationship Type. | **Request body** ```typescript interface IBody { // ID of the data entity. // The data Entity is where the Relationship attribute data is stored. "Data.Entity.ID"?: string; } ``` **Response** ```typescript // Body is a JSON object with the same properties as a single item from the get-list above. // It returns a single Relationship record directly. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/${parent_id}/otherEntityID/{child_id}/relation/{type_id}/update"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Create Relationships ```http POST https://{accountId}.api.nextspace.host/entity/${parent_id}/relation/{type_id}/add ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{type_id}` | path | yes | ID of the related Relationship Type. | **Request body** ```typescript interface IBody { // Array of child Entity IDs to create Relationships with. // If an existing Relationship exists, nothing occurs for that child. "Related.Entity.ID": string[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/${parent_id}/relation/{type_id}/add"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Update Relationship ```http POST https://{accountId}.api.nextspace.host/entity/${parent_id}/otherEntityID/{child_id}/relation/{type_id}/update ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{child_id}` | path | yes | ID of the child Entity. | | `{type_id}` | path | yes | ID of the related Relationship Type. | **Request body** ```typescript interface IBody { // ID of the data entity. // The data Entity is where the Relationship attribute data is stored. "Data.Entity.ID"?: string; } ``` **Response** ```typescript // Body is a JSON object with the same properties as a single item from the get-list above. // It returns a single Relationship record directly. ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/${parent_id}/otherEntityID/{child_id}/relation/{type_id}/update"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ### Delete Relationships ```http POST https://{accountId}.api.nextspace.host/entity/{parent_id}/relation/{type_id}/delete ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. - Logged in user auth token: A token for an active user session on the account, sent as "Authorization: Bearer ". - Power user auth token: A token for an active power user session on the account, sent as "Authorization: Bearer ". | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{parent_id}` | path | yes | ID of the parent Entity. | | `{type_id}` | path | yes | ID of the related Relationship Type. | **Request body** ```typescript interface IBody { // Array of the child Entity IDs to delete Relationships with. "Related.Entity.ID": string[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/{parent_id}/relation/{type_id}/delete"; const method = "post"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` ## Experimental requests To get the top-level Entity for for a given Entity ID and Relationship Type, you can perform the below request. ### Get top-level Entity for Entity and Relationship Type ```http GET https://{accountId}.api.nextspace.host/entity/{entity_id}/relation/{type_id}/top ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | ID of the Entity to get Relationships for. | | `{type_id}` | path | yes | ID of the Relationship Type to get Relationships for. | **Response** ```typescript interface IResponse { // ID of the top-level Entity for the provided Relationship information. // Legacy naming as this is related to a deprecated prototype/instance feature. "InstanceEntityID": string; // Path of Entity IDs from the top-level Entity to the provided Entity. // Currently, circular paths can cause issues in other requests. // So if this has recurring IDs (or is very long, eg: >30 length), // then it's recommended to avoid requesting a tree diagram for this Relationship. "Path": string[]; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/entity/{entity_id}/relation/{type_id}/top"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` Here is how you can request a full Entity tree from a given starting Entity. Avoid calling this request for circular paths, which can be detected with the above get-top request. ### Get Entity hierarchy ```http GET https://{accountId}.api.nextspace.host/experimental/hierarchy/{entity_id}?HierarchyTypeID={type_id} ``` **Requires:** - Account ID: Account ID must be specified in the subdomain of the request url. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `{accountId}` | path | yes | The account id of the account to perform the request on. | | `{entity_id}` | path | yes | ID of the Entity to get Relationships for. | | `HierarchyTypeID={type_id}` | query | yes | ID of the Relationship Type to get Relationships for. | **Response** ```typescript interface IResponse { Root: INode; } interface INode { // ID of the Entity. ID: string; // ID of the Relationship Type. "Type.ID": string; // Array of child Entities (if any). "Children": ITreeResNode[] | null; } ``` **Javascript example** ```javascript const url = "https://{accountId}.api.nextspace.host/experimental/hierarchy/{entity_id}"; const method = "get"; const token = "your-token"; const body = null; async function doRequest(type, url, body, token) { const headers = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }; const options = { method: type, headers: headers, body: body ? JSON.stringify(body) : null }; const res = await fetch(url, options); const json = await res.json(); return json; } doRequest(method, url, body, token).then((res) => { console.log(res); }).catch((err) => { console.error(err); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/apirelations # Getting started (with web) (/webgettingstarted) # Getting started (with web) Nextspace provides two web libraries through NPM. - One provides communication with the Nextspace API.Page linkDocumentation link - Page link - Documentation link - One handles rendering Nextspace data through CesiumJS.Page linkDocumentation link - Page link - Documentation link It is best to know upfront what your requirements are as the process of integration for these libraries may cause you problems down the line in your development. We have found that ensuring the two Nextspace libraries, and the CesiumJS one all work together can be a tricky one. In the sidebar to the left you can pick if you're after a React or a no-framework solution. We use React for our web navigator so that is where you will receive the best support. If you have a different framework such as Angular we recommend you look at the React examples to try figure out your own implementation. ## Render example Below is a basic example on how to render a Nextspace Project View so that you can see how the libraries work before you start integrating them into your own project. [JSFiddle example](https://jsfiddle.net/nLadc6xm/) --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webgettingstarted # No framework (/webnoframework) # No framework We use React for our web navigator but if you don't use the same tools then read through the documentation below on how you can get started. If you'd like proper typing then it's recommended to read the React tab and then try to implement the same in your own framework. [JSFiddle example](https://jsfiddle.net/phs29jze/) ## Starter kit To give you something to try without any setup we've made a simple starter kit that you can double-click and run in your browser. It will render a specific test bookmark. [JSFiddle example](https://jsfiddle.net/1k0fmh46/1/) [Download starter kit](/res/samples/web-no-framework-example.zip) ![](/res/web-no-framework/example.png) ## Just API communication Just loading the communication end is very simple and only requires a single script tag. Once you have installed the NPM package you can copy the folder elsewhere if you wish. Here is how you install the package. ```bash npm i bruce-models ``` Here is how you load the package and test if it's loaded properly. ```html Document ``` ## Rendering engine Getting the CesiumJS library and the Nextspace ones to properly connect is trickier. CesiumJS loads resources dynamically from local files. This means if you are loading the libraries from local files you will need to create a web-server otherwise the requests will be blocked. Below I will showcase two examples, one with local files with a web-server, and one with an online link that doesn't require a web-server. ### Online files without web-server ```html Document ``` ### Local files with web-server Please ensure you install cesium@1.93 as that is the latest supported version in this workflow. Cesium changed how they package their file so our examples will focus on "1.93". Our React examples are focused on their latest version. ```bash npm i cesium@1.93 ``` You can use the http-server package to run a web-server locally. This command will serve the index.html file in your directory. ```bash npx http-server ``` Here is how your index.html file should look like. ```html Document ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webnoframework # React (/webreact) # React We use React as our primary framework so this is where we can best help with. We'll be using Typescript in our examples, and the craco-cesium plugin to help with the Cesium integration. This plugin will allow us to use the latest Cesium version instead of being limited to 1.93 where they changed their build process. ## Starter kit As Typescript setups typically involve a number of libraries working together to produce a package that can be served. We've created a starter kit for you to use. [Download starter kit](/res/samples/web-react-example.zip) Once you've downloaded and extracted the ZIP, run the following commands to start it up. ```bash npm i npm start ``` Please give the initial setup some time. It will take a while to download all the dependencies and prepare the React app. ![](/res/web-react/example.png) The example will load a list of bookmarks for you to select from, and render some point Entities that you can click. Within the "Shell.tsx" file you can edit the parameters to change the initial view to match your own account and data. ```typescript const VIEW_ID = ""; const BOOKMARK_ID = ""; // Replace with your own or else you are at risk of losing bing-maps and cesium terrain access. const CESIUM_DEFAULT_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJiODI1OWQyZC0wYzdlLTRlOTctODFlOC1kYjIwOGYzOWE0NGIiLCJpZCI6MTE3NDg0LCJpYXQiOjE2NzAzODczOTR9.sx0EZdD-Y33FQ7gB_R3CkTsk3KhNpODoQGrnpvSH4UQ"; // Please extend rather than replace like this example to avoid removing any default parameters. ENVIRONMENT.PARAMS = { ...ENVIRONMENT.PARAMS, // Replace with your accountId, found in your account url. // Eg: "https://my-account-id.nextspace.host" would mean "my-account-id" is your ID. // The ZIP example will be pointed at a test account. accountId: "", // Replace with UAT or PROD for client data. // DEV is for Nextspace only data. // The ZIP example will be set to DEV until you change it! env: Api.EEnv.DEV, sessionId: "" } ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webreact # Angular (/webangular) # Angular We don't develop using Angular but we've made an example to help you get started. ## Starter kit As Typescript setups typically involve a number of libraries working together to produce a package that can be served. We've created a starter kit for you to use. [Download starter kit](/res/samples/web-angular-example.zip) Once you've downloaded and extracted the ZIP, run the following commands to start it up. ```bash npm i -g @angular/cli npm i ng serve ``` Please give the initial setup some time. It will take a while to download all the dependencies and prepare the Angular app. ![](/res/web-react/example.png) The example will load a list of bookmarks for you to select from, and render some point Entities that you can click. Within the "app.component.ts" file you can edit the parameters to change the initial view to match your own account and data. ```typescript ENVIRONMENT.PARAMS = { ...ENVIRONMENT.PARAMS, // Replace with your accountId, found in your account url. // Eg: "https://my-account-id.nextspace.host" would mean "my-account-id" is your ID. // The ZIP example will be pointed at a test account. accountId: "", // Replace with UAT or PROD for client data. // DEV is for Nextspace only data. // The ZIP example will be set to DEV until you change it! env: Api.EEnv.DEV, // Required if your Project View is restricted. sessionId: "" }; const viewer = new Cesium.Viewer("VIEWER"); ViewerUtils.InitViewer({ viewer: viewer, // Set to true if you're seeing default Cesium widgets. destroyWidgets: false, styleContainer: true, nextspaceWidgets: true }) ViewRenderEngine.Render({ viewer: viewer, bookmarkId: "", viewId: "", // Skip the transition for first bookmark render to avoid flying every page reload! skipTransition: true }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webangular # React Native using Expo (/reactnativeexpo) # React Native using Expo We don't develop using React Native but we've made an example to help you get started. React Native does not support CesiumJS so we've opted to use an IFrame for the example. We recommend looking through our Web Library examples that focus on API communication and authentication rather than rendering data as that is the same for React Native. ## Starter kit To save some setup explanation we've created a starter kit for you to use. [Download starter kit](/res/samples/web-react-native-expo-example.zip) Once you've downloaded and extracted the ZIP, run the following commands to start it up. ```bash npm i npx expo start ``` Please give the initial setup some time. It will take a while to download all the dependencies and prepare the app. Once started you can download the Expo App on your mobile device and scan the QR code that will be shown in the terminal. ![](/res/expo-react-native/preview.jpg) The example will showcase a basic connection to the Nextspace API, and render an IFrame of a Project View. Within the "App.tsx" file you can edit the parameters to change the initial view to match your own account and data. ```typescript ENVIRONMENT.PARAMS = { ...ENVIRONMENT.PARAMS, // Replace with your accountId, found in your account url. // Eg: "https://my-account-id.nextspace.host" would mean "my-account-id" is your ID. // The ZIP example will be pointed at a test account. accountId: "", // Replace with UAT or PROD for client data. // DEV is for Nextspace only data. // The ZIP example will be set to DEV until you change it! env: Api.EEnv.DEV, // Required if your Project View is restricted. sessionId: "" }; /** * Warning: This will not include information on Project Views that weren't accessible due to permissions. */ ProjectView.GetList({}).then((data) => { // You can use a request like this to drive the IFrame URL and load one of your Project Views. const views = data.views; if (views.length) { setText("Project views: " + views.map(x => x.Name).join(", ")); } else { setText("No project views found."); } }).catch((e) => { console.error(e); setText("Failed to load project views."); }); ``` ```typescript // Replace the Iframe URL with adjusted parameters. // Check the embed documentation site here: https://embed.nextspace-dev.net/ ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/reactnativeexpo # Authentication (/webauth) # Authentication Authentication is performed through the Login and Logout functions. It is an HTTPS POST request that will include the username, password, and the accountId we're logging into. Wherever a `sessionId` is accepted you can pass either a session token from a login or a long-lived access token. The library authenticates every request with `Authorization: Bearer `, so it does not need to know which kind you hold. **Typescript** ```typescript import { Api, ENVIRONMENT, Session } from "bruce-models"; // Setup global defaults. ENVIRONMENT.PARAMS = { ...ENVIRONMENT.PARAMS, accountId: "your-account-id" }; Session.Login({ // Login to your specific account, this will not fallback to a default. accountId: "your-account-id", // Username can be your user's ID, email, or username. username: "your-username", password: "your-password" }).then((data) => { console.log("Login result", data); const ssid = data.session.ID; // Record this inside internal storage or other to retain a session. localStorage.setItem("ssid", ssid); // Reset the environment to use the session. ENVIRONMENT.Reset({ sessionId: ssid }); }).catch((e) => { console.log(e); }); Session.Get({ // Loaded from internal storage in some way! // This accepts a session token or an access token, the library sends either as a bearer token. sessionId: "your-token" }).then((data) => { console.log(data); // Dead session. if (!data.session?.User || data.session.User.ID == "anonymous") { return; } // Reset the environment to use the session. // It is good to do this after requesting it instead of straight from your local-storage. // That way you know it's valid. ENVIRONMENT.Reset({ sessionId: data.session.ID }); }).catch((e) => { console.log(e); }); ``` **Javascript** ```javascript // Setup global defaults. BModels.ENVIRONMENT.PARAMS = { ...BModels.ENVIRONMENT.PARAMS, accountId: "your-account-id" }; BModels.Session.Login({ // Login to your specific account, this will not fallback to a default. accountId: "your-account-id", // Username can be your user's ID, email, or username. username: "your-username", password: "your-password" }).then((data) => { console.log("Login result", data); const ssid = data.session.ID; // Record this inside internal storage or other to retain a session. localStorage.setItem("ssid", ssid); // Reset the environment to use the session. BModels.ENVIRONMENT.Reset({ sessionId: ssid }); }).catch((e) => { console.log(e); }); BModels.Session.Get({ // Loaded from internal storage in some way! // This accepts a session token or an access token, the library sends either as a bearer token. sessionId: "your-token" }).then((data) => { console.log(data); // Dead session. if (!data.session?.User || data.session.User.ID == "anonymous") { return; } // Reset the environment to use the session. // It is good to do this after requesting it instead of straight from your local-storage. // That way you know it's valid. BModels.ENVIRONMENT.Reset({ sessionId: data.session.ID }); }).catch((e) => { console.log(e); }); ``` Here is an example login screen you can reference. This example does not handle the success/failure gracefully and just displays a simple message. You will need to edit it to set your `accountId`. [JSFiddle example](https://jsfiddle.net/g5oh392q/53/) --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webauth # Default widgets (/webdefaultwidgets) # Default widgets As part of the library bundle you will gain access to a number of out of the box widgets made by Nextspace. [JSFiddle example](https://jsfiddle.net/agfwc13e/) ## Enabling / Disabling [JSFiddle example](https://jsfiddle.net/9khmb13p/) The widgets are enabled by default and created when you run `ViewerUtils.InitViewer` You can opt out of them using the `nextspaceWidgets: false` param. **Typescript** ```typescript import * as Cesium from "cesium"; import { ViewerUtils } from "bruce-cesium"; const container = document.getElementById("VIEWER"); const viewer = new Cesium.Viewer(container); // No Nextspace widgets will be created. ViewerUtils.InitViewer({ viewer: viewer, nextspaceWidgets: false }); // Widgets will be made. const data = ViewerUtils.InitViewer({ viewer: viewer }); // This is a key/value dictionary of enabled widgets you can access. // The keys are not random and will stay consistent with the type of widget it is. // For example "widgets.bookmarks" will let you access the bookmarks widget. const widgets = data.widgets; ``` **Javascript** ```javascript const container = document.getElementById("VIEWER"); const viewer = new Cesium.Viewer(container); // No Nextspace widgets will be created. BEngine.ViewerUtils.InitViewer({ viewer: viewer, nextspaceWidgets: false }); // Widgets will be made. const data = BEngine.ViewerUtils.InitViewer({ viewer: viewer }); // This is a key/value dictionary of enabled widgets you can access. // The keys are not random and will stay consistent with the type of widget it is. // For example "widgets.bookmarks" will let you access the bookmarks widget. const widgets = data.widgets; ``` If you want specific widgets then you can create them in two parts. **Typescript** ```typescript import * as Cesium from "cesium"; import { ViewerUtils } from "bruce-cesium"; const container = document.getElementById("VIEWER"); const viewer = new Cesium.Viewer(container); // First initialize the viewer without widgets. ViewerUtils.InitViewer({ viewer: viewer, nextspaceWidgets: false }); // Then create the specific widgets. const widgets = ViewerUtils.CreateWidgets({ viewer: viewer, // If you don't pass "defaultState: false" then it will assume that any widget you don't specify should be created. defaultState: false, navCompass: true, branding: true, cursorBar: false, searchBar: false, infoView: false, bookmarks: true, // This is an optional param for the "bookmarks" widget so it can load available bookmarks. // Read further down to see alternative ways to see this property. viewId: "some_view_id", }); ``` **Javascript** ```typescript const container = document.getElementById("VIEWER"); const viewer = new Cesium.Viewer(container); // First initialize the viewer without widgets. BEngine.ViewerUtils.InitViewer({ viewer: viewer, nextspaceWidgets: false }); // Then create the specific widgets. const widgets = BEngine.ViewerUtils.CreateWidgets({ viewer: viewer, // If you don't pass "defaultState: false" then it will assume that any widget you don't specify should be created. defaultState: false, navCompass: true, branding: true, cursorBar: false, searchBar: false, infoView: false, bookmarks: true, // This is an optional param for the "bookmarks" widget so it can load available bookmarks. // Read further down to see alternative ways to see this property. viewId: "some_view_id", }); ``` If you want to disable a widget after it has been created then you can do so by accessing the widget directly. Once disposed a widget will need to be recreated, so recall the creation function to return it. **Typescript** ```typescript import * as Cesium from "cesium"; import { ViewerUtils } from "bruce-cesium"; const data = ViewerUtils.InitViewer({ viewer: viewer }); const widgets = data.widgets; // Let's kill the search bar widget. widgets.searchBar.Dispose(); // Let's recreate it. const widgets = ViewerUtils.CreateWidgets({ viewer: viewer, // Set defaultState to be false so no other widgets are made. defaultState: false, // Only create the search bar widget. searchBar: true, }); ``` **Javascript** ```javascript const data = BEngine.ViewerUtils.InitViewer({ viewer: viewer }); const widgets = data.widgets; // Let's kill the search bar widget. widgets.searchBar.Dispose(); // Let's recreate it. const widgets = BEngine.ViewerUtils.CreateWidgets({ viewer: viewer, // Set defaultState to be false so no other widgets are made. defaultState: false, // Only create the search bar widget. searchBar: true, }); ``` ## Cursor bar The cursor bar widget is a toolbar found in the top-left of your screen. It provides a default set of 3 tools with a future intention of allowing custom tools to be added. The tools available so far are: - Select: allows you to click Entities within the scene to select them. - Pan: stops the cursor from selecting Entities and allows you to pan the camera around the scene. - Measure: allows you to measure a point, polyline, or polygon. ## Navigation compass The navigation compass will let you rotate around the center of the camera view, turn the camera up and down, zoom in and out, and switch between 2D and 3D camera lenses. ## Search bar The search bar runs on the Cesium address API which means you will need the Cesium's access token set in order to use it. **Typescript** ```typescript import * as Cesium from "cesium"; const MY_KEY = "abc"; Cesium.Ion.defaultAccessToken = MY_KEY; ``` **Javascript** ```javascript const MY_KEY = "abc"; Cesium.Ion.defaultAccessToken = MY_KEY; ``` ## Bookmarks selection The Bookmarks selection widget allows you to swap between Bookmarks for a particular Project View. When you call `ViewRenderEngine.Render` it will automatically update the widget with the related View and Bookmark. Alternatively you can interact with the widget programmatically and avoid the view render engine call. **Typescript** ```typescript import { ViewRenderEngine } from "bruce-cesium"; // Running a call like this will update the bookmarks widget. ViewRenderEngine.Render({ viewer, viewId: "some_view_id", bookmarkId: "some_bookmark_id", skipTransition: true }); const bWidget = data.widgets.bookmarks; // Load View, Bookmark, and potentially enable it. bWidget.LoadViewId({ viewId: "my_view_id", // If set then it will try enable this Bookmark when list is loaded. bookmarkId: "my_bookmark_id", // If true, the first Bookmark will be run automatically. // This is useful in the case bookmarkId is not set or found. // Default is true. runFirstBookmark: true, // Starts auto-playing the Bookmarks. // This will cycle bookmarks at a 5 seconds per Bookmark rate. // Default is false. autoPlay: false }); // Change the selected Bookmark within the widget without rendering it. bWidget.LastEnabledBookmarkId = "some_bookmark_id"; // Change the Project View (and loaded Bookmarks as result) without any rendering. bWidget.ViewId = "some_view_id"; ``` **Javascript** ```javascript // Running a call like this will update the bookmarks widget. BEngine.ViewRenderEngine.Render({ viewer, viewId: "some_view_id", bookmarkId: "some_bookmark_id", skipTransition: true }); const bWidget = data.widgets.bookmarks; // Load View, Bookmark, and potentially enable it. bWidget.LoadViewId({ viewId: "my_view_id", // If set then it will try enable this Bookmark when list is loaded. bookmarkId: "my_bookmark_id", // If true, the first Bookmark will be run automatically. // This is useful in the case bookmarkId is not set or found. // Default is true. runFirstBookmark: true, // Starts auto-playing the Bookmarks. // This will cycle bookmarks at a 5 seconds per Bookmark rate. // Default is false. autoPlay: false }); // Change the selected Bookmark within the widget without rendering it. bWidget.LastEnabledBookmarkId = "some_bookmark_id"; // Change the Project View (and loaded Bookmarks as result) without any rendering. bWidget.ViewId = "some_view_id"; ``` ## Info view Info view is a panel that shows selected Entity details. Right now it'll show attributes and the default photo. This panel will appear when there is at least one selected Entity, if multiple Entities are selected then the first one's data is visible. If this widget is enabled alongside the cursor-bar then the user can click on the scene and view the Entity details. The JSFiddle below shows how to disable the default Info View widget and start building your own. [JSFiddle example](https://jsfiddle.net/cf86t1sp/2/) --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webdefaultwidgets # Drawing tools (/webdrawingtools) # Drawing tools As part of providing the measurement widget we've also added access to some drawing utilities for making your own tools. Today this includes the polygon and polyline drawing tools. It is recommended to give the user some control over the ground occlusion state while tools are active. Within our web navigator we allow pressing "g" on the keyboard to toggle it. Here is how Cesium code lets you toggle it: **Typescript** ```typescript import * as Cesium from "cesium"; const container = document.getElementById("VIEWER"); const viewer = new Cesium.Viewer(container); // Hide Entities that are clipping or below ground. viewer.scene.globe.depthTestAgainstTerrain = true; // Show Entities that are clipping or below ground. viewer.scene.globe.depthTestAgainstTerrain = false; ``` **Javascript** ```javascript const container = document.getElementById("VIEWER"); const viewer = new Cesium.Viewer(container); // Hide Entities that are clipping or below ground. viewer.scene.globe.depthTestAgainstTerrain = true; // Show Entities that are clipping or below ground. viewer.scene.globe.depthTestAgainstTerrain = false; ``` ## Polyline Here is how you can enable and manage the polyline drawing tool. The positions it works with are `Cesium.Cartesian3`. Read to the end of this page to learn how to turn that into latitude and longitude information. **Typescript** ```typescript import * as Cesium from "cesium"; import { Draw3dPolyline, ViewerUtils } from "bruce-cesium"; const container = document.getElementById("VIEWER"); const viewer = new Cesium.Viewer(container); ViewerUtils.InitViewer({ viewer }); const drawPolyline = new Draw3dPolyline({ viewer: viewer, onFinish: (points3d) => { console.log("Finished drawing polyline", points3d); // You can also call .Start() again if you'd prefer to restart the tool. drawPolyline.Dispose(); }, onChange: (points3d) => { // Called whenever the shape changes. }, // (Optional) positions to start the tool with. posses: null, // (Optional) default = false, if false then it will be clamped to ground. perPositionHeight: false }); drawPolyline.Start(); // To kill the tool use: // drawPolyline.Dispose(); // To request current positions use: const pos3ds = drawPolyline.GetPositions(); // Pausing a tool stops updates but keeps the Entities alive. // To pause use: // drawPolyline.Pause(); ``` **Javascript** ```javascript const container = document.getElementById("VIEWER"); const viewer = new Cesium.Viewer(container); BEngine.ViewerUtils.InitViewer({ viewer }); const drawPolyline = new BEngine.Draw3dPolyline({ viewer: viewer, onFinish: (points3d) => { console.log("Finished drawing polyline", points3d); // You can also call .Start() again if you'd prefer to restart the tool. drawPolyline.Dispose(); }, onChange: (points3d) => { // Called whenever the shape changes. }, // (Optional) positions to start the tool with. posses: null, // (Optional) default = false, if false then it will be clamped to ground. perPositionHeight: false }); drawPolyline.Start(); // To kill the tool use: // drawPolyline.Dispose(); // To request current positions use: const pos3ds = drawPolyline.GetPositions(); // Pausing a tool stops updates but keeps the Entities alive. // To pause use: // drawPolyline.Pause(); ``` ## Polygon Here is how you can enable and manage the polygon drawing tool. The positions it works with are `Cesium.Cartesian3`. Read to the end of this page to learn how to turn that into latitude and longitude information. **Typescript** ```typescript import * as Cesium from "cesium"; import { Draw3dPolygon, ViewerUtils } from "bruce-cesium"; const container = document.getElementById("VIEWER"); const viewer = new Cesium.Viewer(container); ViewerUtils.InitViewer({ viewer }); const drawPolygon = new Draw3dPolygon({ viewer: viewer, onFinish: (hierarchy) => { console.log("Finished drawing polygon", hierarchy.positions); // You can also call .Start() again if you'd prefer to restart the tool. drawPolygon.Dispose(); }, onChange: (hierarchy) => { // Called whenever the shape changes. }, // (Optional) hierarchy to start the tool with. hierarchy: null, // (Optional) default = false, if false then it will be clamped to ground. perPositionHeight: false, // (Optional) Max number of points to allow user to draw. Default is infinite. maxPoints: null }); drawPolygon.Start(); // To kill the tool use: // drawPolygon.Dispose(); // To request current shape use: const pos3ds = drawPolygon.GetHierarchy(); // Pausing a tool stops updates but keeps the Entities alive. // To pause use: // drawPolygon.Pause(); ``` **Javascript** ```javascript const container = document.getElementById("VIEWER"); const viewer = new Cesium.Viewer(container); BEngine.ViewerUtils.InitViewer({ viewer }); const drawPolygon = new BEngine.Draw3dPolygon({ viewer: viewer, onFinish: (hierarchy) => { console.log("Finished drawing polygon", hierarchy.positions); // You can also call .Start() again if you'd prefer to restart the tool. drawPolygon.Dispose(); }, onChange: (hierarchy) => { // Called whenever the shape changes. }, // (Optional) hierarchy to start the tool with. hierarchy: null, // (Optional) default = false, if false then it will be clamped to ground. perPositionHeight: false, // (Optional) Max number of points to allow user to draw. Default is infinite. maxPoints: null }); drawPolygon.Start(); // To kill the tool use: // drawPolygon.Dispose(); // To request current shape use: const pos3ds = drawPolygon.GetHierarchy(); // Pausing a tool stops updates but keeps the Entities alive. // To pause use: // drawPolygon.Pause(); ``` ## 2D cursor into 3D position If you need more or would like to develop your own tools. It is recommended to use our `GetAccuratePosition` method instead of Cesium directly. This is because Cesium has a number of bugs where it may pick the position through the terrain and miss the mark by over 10km. Our method uses some techniques to make this a rare occurrence. Here is an example on how you can place 3D points on the map. **Typescript** ```typescript import * as Cesium from "cesium"; import { DrawingUtils, ViewerUtils } from "bruce-cesium"; const container = document.getElementById("VIEWER"); const viewer = new Cesium.Viewer(container); ViewerUtils.InitViewer({ viewer }); const canvas = viewer.canvas; // Create an event handler. You can have multiple of these at a time. // Running .destroy() on one will remove all this specific instance is associated with. const eventHandler = new Cesium.ScreenSpaceEventHandler(canvas); // Lets show a preview on where the user would place a point. // This will keep updating "last3dPos" as the user moves their cursor around the 3D scene. let last3dPos: Cesium.Cartesian3 = null; eventHandler.setInputAction((e) => { const pos2d = e.endPosition; // Check to make sure 2d position is valid. if (pos2d?.x) { const pos3d = DrawingUtils.GetAccuratePosition(viewer, pos2d); if (!pos3d?.x) { return; } last3dPos = pos3d; } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); // Now let's make a Cesium Entity and make it follow this 3D position. // "last3dPos" can be null. This will make the Entity disappear. // But if it's an invalid 3d position it will crash cesium, this is why we validate it before updating it. viewer.entities.add({ // Passing a callback means it will go read the value from the variable every render. position: new Cesium.CallbackProperty(() => last3dPos, false) as any, point: { pixelSize: 10, color: Cesium.Color.RED, outlineColor: Cesium.Color.WHITE, outlineWidth: 2, heightReference: Cesium.HeightReference.NONE, show: true } }); // Now let's create a new point when the user clicks. eventHandler.setInputAction((e) => { const pos2d = e.position; if (pos2d?.x) { const pos3d = DrawingUtils.GetAccuratePosition(viewer, pos2d); if (!pos3d?.x) { return; } viewer.entities.add({ position: pos3d, point: { pixelSize: 10, color: Cesium.Color.BLUE, outlineColor: Cesium.Color.WHITE, outlineWidth: 2, heightReference: Cesium.HeightReference.NONE, show: true } }); } }, Cesium.ScreenSpaceEventType.LEFT_CLICK); ``` **Javascript** ```javascript const container = document.getElementById("VIEWER"); const viewer = new Cesium.Viewer(container); BEngine.ViewerUtils.InitViewer({ viewer }); const canvas = viewer.canvas; // Create an event handler. You can have multiple of these at a time. // Running .destroy() on one will remove all this specific instance is associated with. const eventHandler = new Cesium.ScreenSpaceEventHandler(canvas); // Lets show a preview on where the user would place a point. // This will keep updating "last3dPos" as the user moves their cursor around the 3D scene. let last3dPos = null; eventHandler.setInputAction((e) => { const pos2d = e.endPosition; // Check to make sure 2d position is valid. if (pos2d?.x) { const pos3d = BEngine.DrawingUtils.GetAccuratePosition(viewer, pos2d); if (!pos3d?.x) { return; } last3dPos = pos3d; } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); // Now let's make a Cesium Entity and make it follow this 3D position. // "last3dPos" can be null. This will make the Entity disappear. // But if it's an invalid 3d position it will crash cesium, this is why we validate it before updating it. viewer.entities.add({ // Passing a callback means it will go read the value from the variable every render. position: new Cesium.CallbackProperty(() => last3dPos, false), point: { pixelSize: 10, color: Cesium.Color.RED, outlineColor: Cesium.Color.WHITE, outlineWidth: 2, heightReference: Cesium.HeightReference.NONE, show: true } }); // Now let's create a new point when the user clicks. eventHandler.setInputAction((e) => { const pos2d = e.position; if (pos2d?.x) { const pos3d = BEngine.DrawingUtils.GetAccuratePosition(viewer, pos2d); if (!pos3d?.x) { return; } viewer.entities.add({ position: pos3d, point: { pixelSize: 10, color: Cesium.Color.BLUE, outlineColor: Cesium.Color.WHITE, outlineWidth: 2, heightReference: Cesium.HeightReference.NONE, show: true } }); } }, Cesium.ScreenSpaceEventType.LEFT_CLICK); ``` ## Drawing utilities A common need is conversion between cartesian3 and cartographic. Here is how you can do that. ```typescript // Some cartesian3, typically found by asking an Entity where it is or sampling the 3d scene using your 2d cursor. const myCartesian3 = new Cesium.Cartesian3(1, 2, 3); // Converted cartographic. This will be in radians. const myCartographic = Cesium.Cartographic.fromCartesian(myCartesian3); // Here is how you can get information into degrees and meters. const latitude = Cesium.Math.toDegrees(myCartographic.latitude); const longitude = Cesium.Math.toDegrees(myCartographic.longitude); const altitude = myCartographic.height; // Here is how you can do the opposite and turn known lat/lon points into cartesian3. // Altitude is optional. const cartesian3 = Cesium.Cartesian3.fromDegrees(longitude, latitude, altitude); const cartesian3 = Cesium.Cartesian3.fromRadians(longitude, latitude, altitude); ``` Below is a cheat sheet of some utilities that Nextspace provides within its library to help interact with the Cesium viewer. As mentioned before getting a 3d position from a 2d cursor has some bugs in Cesium. Here is a reminder on the utility we provide to help with that. ```typescript const my2dPos = new Cesium.Cartesian2(100, 100); const my3dPos = DrawingUtils.GetAccuratePosition(viewer, my2dPos); // You can optionally restrict the picking to only allow clicking on Entities instead of the ground. const my3dPos = DrawingUtils.GetAccuratePosition(viewer, my2dPos, true); ``` Here is a utility for getting the height of the terrain at a specific position. This will never crash nor return a non-number. It will include a "error" property within the data if you'd like to know when there was a problem. ```typescript const my3dPos = new Cesium.Cartesian3(1, 2, 3); const data = await DrawingUtils.GetTerrainHeight({ pos3d: my3dPos, viewer }); const height = data.height; ``` If you have a polyline and need to know a position x meters along it then use this utility: ```typescript const my3dPos = new Cesium.Cartesian3(1, 2, 3); const myLine = [ new Cesium.Cartesian3(1, 2, 3), new Cesium.Cartesian3(1, 2, 3), new Cesium.Cartesian3(1, 2, 3), new Cesium.Cartesian3(1, 2, 3) ]; const data = DrawingUtils.PointAcrossPolyline({ viewer, posses: myLine, // 50 meters along the line. If distance exceeds the length of the line then it will return you the last line position. distance: 50 }) const pointOnLine = data.point; ``` Cesium has a concept of "HeightReference" which means an altitude value can mean different things in different concepts. This can be quite annoying to program with as an Entity might say "50 meters height" but you'll need to calculate what that means to you. For example you might want to know the height above the ground or the height above the sea. ```typescript let myPos3d = new Cesium.Cartesian3(1, 2, 3); myPos3d = DrawingUtils.EnsurePosHeight({ viewer, pos3d: myPos3d, // In this example we have a point that is positioned above sea level but we want it in relative to ground height. desiredHeightRef: Cesium.HeightReference.RELATIVE_TO_GROUND, heightRef: Cesium.HeightReference.NONE }) ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webdrawingtools # Entity interaction (/webentityinteraction) # Entity interaction Once a scene is rendered there are a few ways to interact with it. In this section we'll be going over Entity-specific interaction. [JSFiddle example](https://jsfiddle.net/xnk3cmur/) ## Menu item manager Rendering of data is performed through Menu Items within these libraries. Menu items are actions with instructions on how to retrieve then draw data. If you have data you'd prefer to load in yourself you can do so by either creating a Menu Item that delivers prepared JSON entities, or by manually adding them to the visual register through the `register.AddRego` method. Please contact our support for examples as these are not prepared yet. The Menu Item manager manages what Menu Items are enabled and lets you push Entity updates to the visuals register. The Menu Item manager is accessible through your Cesium viewer instance. **Typescript** ```typescript import * as Cesium from "cesium"; import { ViewerUtils } from "bruce-cesium"; const container = document.getElementById("VIEWER"); const viewer = new Cesium.Viewer(container); ViewerUtils.InitViewer({ viewer: viewer }); const manager = ViewerUtils.GetManager({ viewer: viewer, createIfMissing: true }); ``` **Javascript** ```javascript const container = document.getElementById("VIEWER"); const viewer = new Cesium.Viewer(container); BEngine.ViewerUtils.InitViewer({ viewer: viewer }); const manager = BEngine.ViewerUtils.GetManager({ viewer: viewer, createIfMissing: true }); ``` Here is how you can enable, disable, and know what Menu Items are enabled. ```typescript const enabledItemIds = manager.GetEnabledItemIds(); const enabledItem = manager.GetEnabledItem("my_menu_item_id"); manager.RemoveItemById({ menuItemId: "my_menu_item_id", // Default is true, this means all child Menu Items will also be disabled. recursive: true }); manager.RenderItem({ // Cannot be null in real uses. // Read through "Render Entities" and "Render Tilesets" docs for how these can be created. item: null, // Default = true. This will enable all child Menu Items. recursive?: boolean }) ``` If you've updated an Entity record and would like the related visual to refresh then you can do it like so: ```typescript manager.ReRender({ // Either an array of Entity IDs or Entity data must be provided. // If entity ids are provided then they will be retrieved from the server before being refreshed. entityIds: [], entities: null, // Optional string array to be explicit about what Menu Items to refresh. // This is highly recommended as it will be faster. menuItemIds: null, // Default = false, this will skip queues and render immediately. // Recommended to only use this for small batches if they are refreshing often. force: false }) ``` ## Visuals register Any Nextspace Entity related object that is loaded into the scene will be registed into the visuals register for your Cesium viewer. The register is accessible through the Menu Items manager. **Typescript** ```typescript import * as Cesium from "cesium"; import { ViewerUtils } from "bruce-cesium"; ViewerUtils.InitViewer({ viewer: viewer }); const manager = ViewerUtils.GetManager({ viewer: viewer, createIfMissing: true }); const register = manager.VisualsRegister; console.log("register", register); ``` **Javascript** ```javascript BEngine.ViewerUtils.InitViewer({ viewer: viewer }); const manager = BEngine.ViewerUtils.GetManager({ viewer: viewer, createIfMissing: true }); const register = manager.VisualsRegister; console.log("register", register); ``` Here is how you can retrieve loaded Entities through different methods. ```typescript const register = manager.VisualsRegister; // Get a registration for an Entity by ID for any Menu Item. const rego = register.GetRego({ entityId: "my_entity_id" }); // Get a registration for an Entity by ID for a specific Menu Item. const rego = register.GetRego({ entityId: "my_entity_id", menuItemId: "my_menu_item_id" }); // Get registrations from under your 2d cursor. const regos = register.GetRegosFromCursor({ cursor: { x: 100, y: 200 } }); // Get all registered Entity IDs. const ids = register.GetEntityIds(); ``` Here are some ways to apply adjustments to the Entities. These methods will work regardless of whether the related Entity is loaded or not. If you set an Entity ID to be hidden, then if that Entity loads in later it will hide anyways. ```typescript const register = manager.VisualsRegister; // Select Entities like so: register.ClearSelected(); register.SetSelected({ selected: true, entityIds: ["a", "b", "c"] }); // Check if something is selected. const isSelected = register.GetIsSelected({ entityId: "a" }); const allSelectedIds = register.GetSelected(); // Isolating Entities will hide all other Entities. register.ClearIsolated(); register.SetIsolated({ entityIds: ["a"], isolated: true }) // Check if something is isolated. const isIsolated = register.GetIsIsolated({ entityId: "a" }); const allIsolatedIds = register.GetIsolated(); // Hide Entities. register.ClearHidden(); register.SetHidden({ entityIds: ["a"], hidden: true }); // Check if something is hidden. const isHidden = register.GetIsHidden({ entityId: "a" }); const allHiddenIds = register.GetHidden(); // Apply opacity. register.SetOpacity({ entityIds: ["a"], // Value between 0 and 1. opacity: 0.5 }); // Check opacity. // If multiple Entities are supplied, then the average is returned. // Unknown opacity is treated as 100% opacity. const opacity = register.GetOpacity({ entityIds: ["a"], }); // Dictionary of Entity ID -> opacity value. // This will only return Entities that have an opacity applied. const opacityMap = register.GetOpacityMap(); // Clear any applied opacity. register.ClearOpacity(); ``` You can subscribe to the register's updates so you can know when an Entity has loaded in or out of the scene. ```typescript const register = manager.VisualsRegister; const removal = register.OnUpdate.Subscribe((data) => { console.log(data); }); // Call the removal function to unsubscribe. //removal(); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webentityinteraction # Project views (/webprojectviews) # Project views Project views are saved configurations for what is available to be rendered by a user. For example you may import a few files into your account, then setup a project view to showcase or manage one or many of those imports. Project views contain bookmarks, these are saved views of the data. Think of them like powerpoint slides that show where to look and what data is enabled. As a developer you have the choice of creating and rendering project views, or handling what they do yourself. Below will be examples on how to load what project views and bookmarks exist in your account, then how to render them. [JSFiddle example](https://jsfiddle.net/8vzoy3jf/) ## Listing available data Project views can be restricted by login so please ensure to allow the user to login before requesting this information. **Typescript** ```typescript import { ClientFile, ENVIRONMENT, IDictionary, ProjectView, ProjectViewBookmark } from "bruce-models"; // Setup global defaults. ENVIRONMENT.PARAMS = { ...ENVIRONMENT.PARAMS, accountId: "your-account-id", // Important if your Project Views are not set to be publicly accessible. sessionId: "your-session-id" }; /** * Warning: This will not include information on Project Views that weren't accessible due to permissions. */ ProjectView.GetList({}).then((data) => { const views = data.views; console.log("Views", views); }).catch((e) => { console.error(e); }); const someViewId = "some-view-id"; ProjectViewBookmark.GetList({ viewId: someViewId }).then((data) => { const bookmarks = data.bookmarks; console.log("Bookmarks", bookmarks); // We can loop through and prepare the thumbnail urls. // You can then use those urls to display preview images for the user. const thumbnails: IDictionary = {}; bookmarks.forEach((bookmark) => { if (!bookmark["Screenshot.ClientFile.ID"]) { return; } thumbnails[bookmark.ID] = ClientFile.GetUrl({ fileId: bookmark["Screenshot.ClientFile.ID"], viaCdn: true }); }); }).catch((e) => { console.error(e); }); ``` **Javascript** ```javascript // Setup global defaults. BModels.ENVIRONMENT.PARAMS = { ...BModels.ENVIRONMENT.PARAMS, accountId: "your-account-id", // Important if your Project Views are not set to be publicly accessible. sessionId: "your-session-id" }; /** * Warning: This will not include information on Project Views that weren't accessible due to permissions. */ BModels.ProjectView.GetList({}).then((data) => { const views = data.views; console.log("Views", views); }).catch((e) => { console.error(e); }); const someViewId = "some-view-id"; BModels.ProjectViewBookmark.GetList({ viewId: someViewId }).then((data) => { const bookmarks = data.bookmarks; console.log("Bookmarks", bookmarks); // We can loop through and prepare the thumbnail urls. // You can then use those urls to display preview images for the user. const thumbnails = {}; bookmarks.forEach((bookmark) => { if (!bookmark["Screenshot.ClientFile.ID"]) { return; } thumbnails[bookmark.ID] = BModels.ClientFile.GetUrl({ fileId: bookmark["Screenshot.ClientFile.ID"], viaCdn: true }); }); }).catch((e) => { console.error(e); }); ``` ## Rendering Rendering a project view requires you to know the ID of both the view and bookmark you're interested in. Additionally it is recommended to set `skipTransition` to `true` for the first bookmark render. This will skip the camera movement from its default position. **Typescript** ```typescript import { ViewerUtils, ViewRenderEngine } from "bruce-cesium"; import { ENVIRONMENT } from "bruce-models"; import * as Cesium from "cesium"; // Setup global defaults. ENVIRONMENT.PARAMS = { ...ENVIRONMENT.PARAMS, accountId: "your-account-id", // Important if your Project Views are not set to be publicly accessible. sessionId: "your-session-id" }; const viewId = "some-view-id"; const bookmarkIds = [ "some-bookmark-id", "some-other-bookmark-id", "another-bookmark-id" ]; const loadView = async () => { // Ensure this is a fixed height box or else Cesium viewer will keep expanding it. const container = document.getElementById("VIEWER"); // Proxy Cesium Ion requests through the Nextspace API. // This allows for access to Cesium Ion resources without exposing your token, or the default token. const bruceApi = ENVIRONMENT.Api().GetBruceApi(); await bruceApi.Loading; (Cesium.Ion as any).defaultServer = bruceApi.GetBaseUrl() + "proxy/cesium-ion/"; Cesium.Ion.defaultAccessToken = "proxy"; const viewer = new Cesium.Viewer(container, { // Setting some defaults in case the current Cesium token is dead. terrainProvider: new Cesium.EllipsoidTerrainProvider(), imageryProvider: new Cesium.ArcGisMapServerImageryProvider({ url: "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer", enablePickFeatures: false }) }); ViewerUtils.InitViewer({ viewer: viewer, // True if you want to kill the default Cesium widgets. // Eg: Their timeline widget. destroyWidgets: false, styleContainer: true }); // Example on how to cycle through bookmarks. // You can reference this and instead allow the user to click through bookmarks. let currentBookmarkId: string = ""; const doRender = async () => { const firstRender = !currentBookmarkId; const index = bookmarkIds.indexOf(currentBookmarkId); if (index === -1) { currentBookmarkId = bookmarkIds[0]; } else { currentBookmarkId = index > bookmarkIds.length - 2 ? bookmarkIds[0] : bookmarkIds[index + 1]; } await ViewRenderEngine.Render({ viewer, bookmarkId: currentBookmarkId, viewId: viewId, // Skip the transition for first bookmark render to avoid flying every page reload! skipTransition: firstRender }); }; setInterval(() => { doRender(); }, 5000); }; loadView(); ``` **Javascript** ```javascript // Setup global defaults. BModels.ENVIRONMENT.PARAMS = { ...BModels.ENVIRONMENT.PARAMS, accountId: "your-account-id", // Important if your Project Views are not set to be publicly accessible. sessionId: "your-session-id" }; const viewId = "some-view-id"; const bookmarkIds = [ "some-bookmark-id", "some-other-bookmark-id", "another-bookmark-id" ]; const loadView = async () => { // Ensure this is a fixed height box or else Cesium viewer will keep expanding it. const container = document.getElementById("VIEWER"); // Proxy Cesium Ion requests through the Nextspace API. // This allows for access to Cesium Ion resources without exposing your token, or the default token. const bruceApi = BModels.ENVIRONMENT.Api().GetBruceApi(); await bruceApi.Loading; Cesium.Ion.defaultServer = bruceApi.GetBaseUrl() + "proxy/cesium-ion/"; Cesium.Ion.defaultAccessToken = "proxy"; const viewer = new Cesium.Viewer(container, { // Setting some defaults in case the current Cesium token is dead. terrainProvider: new Cesium.EllipsoidTerrainProvider(), imageryProvider: new Cesium.ArcGisMapServerImageryProvider({ url: "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer", enablePickFeatures: false }) }); BEngine.ViewerUtils.InitViewer({ viewer: viewer, // True if you want to kill the default Cesium widgets. // Eg: Their timeline widget. destroyWidgets: false, styleContainer: true }); // Example on how to cycle through bookmarks. // You can reference this and instead allow the user to click through bookmarks. let currentBookmarkId = ""; const doRender = async () => { const firstRender = !currentBookmarkId; const index = bookmarkIds.indexOf(currentBookmarkId); if (index === -1) { currentBookmarkId = bookmarkIds[0]; } else { currentBookmarkId = index > bookmarkIds.length - 2 ? bookmarkIds[0] : bookmarkIds[index + 1]; } await BEngine.ViewRenderEngine.Render({ viewer: viewer, bookmarkId: currentBookmarkId, viewId: viewId, // Skip the transition for first bookmark render to avoid flying every page reload! skipTransition: firstRender }); }; setInterval(() => { doRender(); }, 5000); }; loadView(); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webprojectviews # Render entities (/webrenderentities) # Render entities If you're working with vector data, icons on the map, or loose 3D models instead of a BIM or CAD file, you can render it through the library without having to create a project view. This data is organized in Nextspace through `entity types`. This is a level of organization that also defines default styling, and the data schema for the records. ## Listing and rendering Below is how you can list what entity types you've got available to render. We can find the `entityTypeId` from here. **Typescript** ```typescript import { ENVIRONMENT, EntityType } from "bruce-models"; import * as Cesium from "cesium"; import { MenuItemCreator } from "bruce-cesium"; // Setup global defaults. ENVIRONMENT.PARAMS = { ...ENVIRONMENT.PARAMS, accountId: "your-account-id" }; /** * @Warning it is recommended to set a sessionId prior to making this request. * The GetList call will not include information on what Entity types were excluded due to permissions. */ EntityType.GetList({}).then((data) => { // You can use the ".Name" property and allow the user to select what they'd like to render. console.log("Entity types", data.entityTypes); }).catch((e) => { console.error(e); }); // Cannot be null in real use! // Check earlier docs for initialization of the Cesium viewer. const viewer: Cesium.Viewer = null; MenuItemCreator.RenderEntityTypes({ entityTypeIds: ["some-type-id"], viewer: viewer }); ``` **Javascript** ```javascript // Setup global defaults. BModels.ENVIRONMENT.PARAMS = { ...BModels.ENVIRONMENT.PARAMS, accountId: "your-account-id" }; /** * @Warning it is recommended to set a sessionId prior to making this request. * The GetList call will not include information on what Entity types were excluded due to permissions. */ BModels.EntityType.GetList({}).then((data) => { // You can use the ".Name" property and allow the user to select what they'd like to render. console.log("Entity types", data.entityTypes); }).catch((e) => { console.error(e); }); // Cannot be null in real use! // Check earlier docs for initialization of the Cesium viewer. const viewer = null; BEngine.MenuItemCreator.RenderEntityTypes({ entityTypeIds: ["some-type-id"], viewer: viewer }); ``` ## Specific records [JSFiddle example](https://jsfiddle.net/27bhwa9d/) The above JSFiddle example shows how to render and remove records dynamically. If you know one or many specific entity records you'd like to render then you can do so like so: **Typescript** ```typescript import * as Cesium from "cesium"; import { MenuItemCreator } from "bruce-cesium"; import { Bounds, ENVIRONMENT, Entity, MenuItem, ObjectUtils } from "bruce-models"; // Setup global defaults. ENVIRONMENT.PARAMS = { ...ENVIRONMENT.PARAMS, accountId: "your-account-id" }; // Cannot be null in real use! // Check earlier docs for initialization of the Cesium viewer. const viewer: Cesium.Viewer = null; MenuItemCreator.RenderEntities({ entityIds: ["some-entity-id"], viewer: viewer }); // If you have reasons to avoid touching the Nextspace database you can provide the JSON directly. // This is an advanced feature which is why you'll have to create the Menu Item definition yourself instead of using a helper. // Below is an example. const myFakeEntity: Entity.IEntity = { Bruce: { // ID must be supplied. ID: ObjectUtils.UId(), "EntityType.ID": null, Location: { longitude: 174.764975, latitude: -36.843922, altitude: 200 } } }; myFakeEntity.boundaries = Bounds.FromEntity(myFakeEntity); MenuItemCreator.RenderMenuItems({ menuItems: [ { // ID must be supplied. id: ObjectUtils.UId(), Caption: "Preloaded data", BruceEntity: { // Populate this with your Entity JSON data. Entities: [ myFakeEntity ] }, Type: MenuItem.EType.EntitiesLoaded } ], viewer }); ``` **Javascript** ```javascript // Setup global defaults. BModels.ENVIRONMENT.PARAMS = { ...BModels.ENVIRONMENT.PARAMS, accountId: "your-account-id" }; // Cannot be null in real use! // Check earlier docs for initialization of the Cesium viewer. const viewer = null; BEngine.MenuItemCreator.RenderEntities({ entityIds: ["some-entity-id"], viewer: viewer }); // If you have reasons to avoid touching the Nextspace database you can provide the JSON directly. // This is an advanced feature which is why you'll have to create the Menu Item definition yourself instead of using a helper. // Below is an example. const myFakeEntity = { Bruce: { // ID must be supplied. ID: BModels.ObjectUtils.UId(), "EntityType.ID": null, Location: { longitude: 174.764975, latitude: -36.843922, altitude: 200 } } }; myFakeEntity.boundaries = BModels.Bounds.FromEntity(myFakeEntity); BEngine.MenuItemCreator.RenderMenuItems({ menuItems: [ { // ID must be supplied. id: BModels.ObjectUtils.UId(), Caption: "Preloaded data", BruceEntity: { // Populate this with your Entity JSON data. Entities: [ myFakeEntity ] }, Type: BModels.MenuItem.EType.EntitiesLoaded } ], viewer }); ``` ## Custom filter If you are familiar with the filter syntax, you can also use that to filter the records you'd like to render. **Typescript** ```typescript import { ENVIRONMENT, EntityType } from "bruce-models"; import * as Cesium from "cesium"; import { MenuItemCreator } from "bruce-cesium"; // Setup global defaults. ENVIRONMENT.PARAMS = { ...ENVIRONMENT.PARAMS, accountId: "your-account-id" }; // Cannot be null in real use! // Check earlier docs for initialization of the Cesium viewer. const viewer: Cesium.Viewer = null; MenuItemCreator.RenderEntityFilter({ attrFilter: { entityTypeId: "some-entity-type-id", "some-attribute-path": { "equals": "some-value" } as any // Typing is wrong so we'll override it! }, viewer: viewer }); ``` **Javascript** ```javascript // Setup global defaults. BModels.ENVIRONMENT.PARAMS = { ...BModels.ENVIRONMENT.PARAMS, accountId: "your-account-id" }; // Cannot be null in real use! // Check earlier docs for initialization of the Cesium viewer. const viewer = null; BEngine.MenuItemCreator.RenderEntityFilter({ attrFilter: { entityTypeId: "some-entity-type-id", "some-attribute-path": { "equals": "some-value" } }, viewer: viewer }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webrenderentities # Render Cesium Tilesets (/webrendertilesets) # Render Cesium Tilesets If you've performed an import of a BIM, CAD, or point cloud file, you can render it through the library without having to create a Project View. This also includes static mesh Tilesets you've uploaded from your own files. You will need to know the `tilesetId` that was generated from your import. [JSFiddle example](https://jsfiddle.net/crz4wn5b/) ## Listing and rendering Below is how you can list what Tilesets you've got available to render. We can find the `tilesetId` from here. **Typescript** ```typescript import { ENVIRONMENT, Tileset } from "bruce-models"; import { MenuItemCreator } from "bruce-cesium"; import * as Cesium from "cesium"; // Setup global defaults. ENVIRONMENT.PARAMS = { ...ENVIRONMENT.PARAMS, accountId: "your-account-id" }; /** * @Warning it is recommended to set a sessionId prior to making this request. * The GetList call will not include information on what Tilesets were excluded due to permissions. */ Tileset.GetList({}).then((data) => { // You can use the ".name" property and allow the user to select what they'd like to render. console.log("Tilesets", data.tilesets); // CAD. Eg: IFC, BRZ, etc. const cad = data.tilesets.filter(x => x.type == Tileset.EType.Cad); // Point clouds. const pointClouds = data.tilesets.filter(x => x.type == Tileset.EType.PointCloud); // Entities. // This is created from an Entity Type of arbitrary Entities, eg: vector polygons. const entities = data.tilesets.filter(x => x.type == Tileset.EType.EntitiesSet); // Static files uploaded directly by a user. // This is usually a mesh tileset. const userFiles = data.tilesets.filter(x => x.type == Tileset.EType.LegacyStatic); }).catch((e) => { console.error(e); }); // Cannot be null in real use! // Check earlier docs for initialization of the Cesium viewer. const viewer: Cesium.Viewer = null; // Render using a record. // This cannot be null in real use. Retrieve the record then plug it in. const someSpecificRecord: Tileset.ITileset = null; MenuItemCreator.RenderTileset({ tileset: someSpecificRecord, viewer: viewer }); // Render using an ID. const someTilesetId: string = "some-tileset-id"; MenuItemCreator.RenderTileset({ tilesetId: someTilesetId, viewer: viewer }); ``` **Javascript** ```javascript // Setup global defaults. BModels.ENVIRONMENT.PARAMS = { ...BModels.ENVIRONMENT.PARAMS, accountId: "your-account-id" }; /** * @Warning it is recommended to set a sessionId prior to making this request. * The GetList call will not include information on what Tilesets were excluded due to permissions. */ BModels.Tileset.GetList({}).then((data) => { // You can use the ".name" property and allow the user to select what they'd like to render. console.log("Tilesets", data.tilesets); // CAD. Eg: IFC, BRZ, etc. const cad = data.tilesets.filter(x => x.type == BModels.Tileset.EType.Cad); // Point clouds. const pointClouds = data.tilesets.filter(x => x.type == BModels.Tileset.EType.PointCloud); // Entities. // This is created from an Entity Type of arbitrary Entities, eg: vector polygons. const entities = data.tilesets.filter(x => x.type == BModels.Tileset.EType.EntitiesSet); // Static files uploaded directly by a user. // This is usually a mesh tileset. const userFiles = data.tilesets.filter(x => x.type == BModels.Tileset.EType.LegacyStatic); }).catch((e) => { console.error(e); }); // Cannot be null in real use! // Check earlier docs for initialization of the Cesium viewer. const viewer = null; // Render using a record. // This cannot be null in real use. Retrieve the record then plug it in. const someSpecificRecord = null; BEngine.MenuItemCreator.RenderTileset({ tileset: someSpecificRecord, viewer: viewer }); // Render using an ID. const someTilesetId = "some-tileset-id"; BEngine.MenuItemCreator.RenderTileset({ tilesetId: someTilesetId, viewer: viewer }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webrendertilesets # Render terrain and imagery (/webrenderterrain) # Render terrain and imagery We provide lists of default imagery and terrain providers, but you can also render your own imported files. If you're rendering Cesium World Terrain then please ensure to read about Cesium Ion attribution. If you're rendering Cesium World Terrain, or any Bing Maps imageries then ensure you have a valid CesiumIon token applied. [JSFiddle example](https://jsfiddle.net/6tko03pd/) ## Listing and rendering Below is how you can list what data has been imported into your account, what default data there is, and how to render it. **Typescript** ```typescript import { ENVIRONMENT, ProjectViewTile, Tileset } from "bruce-models"; import { TileRenderEngine } from "bruce-cesium"; import * as Cesium from "cesium"; // Setup global defaults. ENVIRONMENT.PARAMS = { ...ENVIRONMENT.PARAMS, accountId: "your-account-id" }; /** * @Warning it is recommended to set a sessionId prior to making this request. * The GetList call will not include information on what Tilesets were excluded due to permissions. */ Tileset.GetList({}).then((data) => { // You can use the ".name" property and allow the user to select what they'd like to render. console.log("Tilesets", data.tilesets); // Imagery. const IMAGERY_TYPES = [Tileset.EType.GeoMap, Tileset.EType.EntitiesMap, Tileset.EType.ExternalGeoMap]; const imagery = data.tilesets.filter(x => IMAGERY_TYPES.includes(x.type)); // Terrain. const TERRAIN_TYPES = [Tileset.EType.Terrain, Tileset.EType.ExternalTerrain]; const terrain = data.tilesets.filter(x => TERRAIN_TYPES.includes(x.type)); }).catch((e) => { console.error(e); }); // Defaults that are always available. const defaultTerrains = ProjectViewTile.DefaultTerrains; const defaultImageries = ProjectViewTile.DefaultImagery; // Cannot be null in real use! // Check earlier docs for initialization of the Cesium viewer. const viewer: Cesium.Viewer = null; TileRenderEngine.Map.Navigator.Render({ tiles: [ { tilesetId: "your-imagery-tileset-id" } ], viewer: viewer }); TileRenderEngine.Terrain.Navigator.Render({ tile: { tilesetId: "your-terrain-tileset-id" }, viewer: viewer }); TileRenderEngine.Terrain.Navigator.Render({ tile: { // Substitute tilesetId with a default id if you'd like. tilesetId: ProjectViewTile.EDefaultTerrain.FlatTerrain }, viewer: viewer }); ``` **Javascript** ```javascript // Setup global defaults. BModels.ENVIRONMENT.PARAMS = { ...BModels.ENVIRONMENT.PARAMS, accountId: "your-account-id" }; /** * @Warning it is recommended to set a sessionId prior to making this request. * The GetList call will not include information on what Tilesets were excluded due to permissions. */ BModels.Tileset.GetList({}).then((data) => { // You can use the ".name" property and allow the user to select what they'd like to render. console.log("Tilesets", data.tilesets); // Imagery. const IMAGERY_TYPES = [BModels.Tileset.EType.GeoMap, BModels.Tileset.EType.EntitiesMap, BModels.Tileset.EType.ExternalGeoMap]; const imagery = data.tilesets.filter(x => IMAGERY_TYPES.includes(x.type)); // Terrain. const TERRAIN_TYPES = [BModels.Tileset.EType.Terrain, BModels.Tileset.EType.ExternalTerrain]; const terrain = data.tilesets.filter(x => TERRAIN_TYPES.includes(x.type)); }).catch((e) => { console.error(e); }); // Defaults that are always available. const defaultTerrains = BModels.ProjectViewTile.DefaultTerrains; const defaultImageries = BModels.ProjectViewTile.DefaultImagery; // Cannot be null in real use! // Check earlier docs for initialization of the Cesium viewer. const viewer = null; BEngine.TileRenderEngine.Map.Navigator.Render({ tiles: [ { tilesetId: "your-imagery-tileset-id" } ], viewer: viewer }); BEngine.TileRenderEngine.Terrain.Navigator.Render({ tile: { tilesetId: "your-terrain-tileset-id" }, viewer: viewer }); BEngine.TileRenderEngine.Terrain.Navigator.Render({ tile: { // Substitute tilesetId with a default id if you'd like. tilesetId: ProjectViewTile.EDefaultTerrain.FlatTerrain }, viewer: viewer }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webrenderterrain # Styling Entities (/webstyleentities) # Styling Entities One of the most important aspects of rendering your data is control over how it's presented. We've got a Style system that allows you to colour, extrude, and adjust in other ways based on many different properties. Within this article we'll cover how to manage Styles for Entity Menu Items. These notes do not apply to Tileset Menu Items. [JSFiddle example](https://jsfiddle.net/8c5qaysu/) Please refer to the API Reference "Entity Styles" article for a breakdown of settings and record management. ## Style records The most commonly used process is setting up a Style through our Operator app then pointing your Menu Item at the record's ID. **Typescript** ```typescript import { MenuItemCreator } from "bruce-cesium"; import { ObjectUtils, ZoomControl } from "bruce-models"; // If set to "0" then it'll load the default Style for the Entity Type. // Eg: if an Entity has the Entity Type "Building" then it'll use the Building's default Style. const someStyleId = 1; // Let's try render one of our Entity types using this record: MenuItemCreator.RenderMenuItems({ viewer: viewer, menuItems: [ { // This can be any string ID, just as long as it doesn't clash with other Menu Items. // If you're lazy and testing stuff, you can simply use the ID: "1" or similar. id: ObjectUtils.UId(), // Caption is useful in case a UI panel renders your Menu Item. Caption: "Rentals", Type: "BruceEntity", BruceEntity: { "EntityType.ID": "some-entity-type-id" }, CameraZoomSettings: [ { // Between 0 and 100,000 meters (to the camera) your Entity will be drawn as a point with your Style. MinZoom: 0, MaxZoom: 100000, DisplayType: ZoomControl.EDisplayType.Point, StyleID: someStyleId } ] } ] }); ``` **Javascript** ```javascript // If set to "0" then it'll load the default Style for the Entity Type. // Eg: if an Entity has the Entity Type "Building" then it'll use the Building's default Style. const someStyleId = 1; // Let's try render one of our Entity types using this record: BEngine.MenuItemCreator.RenderMenuItems({ viewer: viewer, menuItems: [ { // This can be any string ID, just as long as it doesn't clash with other Menu Items. // If you're lazy and testing stuff, you can simply use the ID: "1" or similar. id: BModels.ObjectUtils.UId(), // Caption is useful in case a UI panel renders your Menu Item. Caption: "Rentals", Type: "BruceEntity", BruceEntity: { "EntityType.ID": "some-entity-type-id" }, CameraZoomSettings: [ { // Between 0 and 100,000 meters (to the camera) your Entity will be drawn as a point with your Style. MinZoom: 0, MaxZoom: 100000, DisplayType: BModels.ZoomControl.EDisplayType.Point, StyleID: someStyleId } ] } ] }); ``` If you'd like to let the user pick a Style from what is available. You can request the information like so: **Typescript** ```typescript import { Style } from "bruce-models"; Style.GetList({}).then((data) => { // There are other kinds of Styles so let's ensure we only let the user pick from entity ones. const entityStyles = data.styles.filter(x => !x.Type || x.Type == Style.EType.Entity); console.log("Entity Styles", entityStyles); // Styles have a "Name" property for the user to choose from, // then an "ID" property for you to pass to the Menu Item. }); ``` **Javascript** ```javascript BModels.Style.GetList({}).then((data) => { // There are other kinds of Styles so let's ensure we only let the user pick from entity ones. const entityStyles = data.styles.filter(x => !x.Type || x.Type == BModels.Style.EType.Entity); console.log("Entity Styles", entityStyles); // Styles have a "Name" property for the user to choose from, // then an "ID" property for you to pass to the Menu Item. }); ``` ## Programmatic Styles We of course allow you to avoid having to deal with Style records and define your own programmatically. We will cover the Style syntax in a separate article. Below is an example of how to use it. **Typescript** ```typescript import { MenuItemCreator } from "bruce-cesium"; import { ObjectUtils, ZoomControl, PathUtils, Calculator } from "bruce-models"; const style = { pointStyle: { color: [ // Mapping based on 'rent' attribute. { value: { // The Wrap utility ensures the path will be readable by the style calculator. // If your attribute is nested, simply pass the path array into the function, eg: ["location", "longitude"]. field: PathUtils.Wrap(["rent"]), // The mapping type is a simple mapping of values to colours. // In this case we're looking for two specific rental costs and assigning them colours. values: [ { fieldValue: "£200", appliedValue: "rgba(50, 180, 50, 0.8)" }, { fieldValue: "£300", appliedValue: "rgba(50, 50, 180, 0.8)" } ] }, type: Calculator.EValueType.Mapping }, // Gradient based on 'location' -> 'longitude' attribute. { value: { field: PathUtils.Wrap(["location", "longitude"]), // Within this example I know the min and max values of the longitude attribute. // I am making it go from red to white as the points move along the longitude vector. points: [ { position: -1.099, color: "rgba(255, 0, 0, 0.8)" }, { position: -1.046, color: "rgba(255, 255, 255, 0.8)" } ] }, type: Calculator.EValueType.Gradient }, // Default if no above options resolved into anything. { value: "rgba(255, 255, 255, 0.4)", type: Calculator.EValueType.Color } ] } }; // Let's take our previous example and swap out some pieces. MenuItemCreator.RenderMenuItems({ viewer: viewer, menuItems: [ { "id": ObjectUtils.UId(), "Caption": "Rentals", "Type": "BruceEntity", "BruceEntity": { "EntityType.ID": "some-entity-type-id" }, CameraZoomSettings: [ { MinZoom: 0, MaxZoom: 100000, DisplayType: ZoomControl.EDisplayType.Point, // Set this to -1 to use a programmatic Style. StyleID: -1, // Now define a Style JSON. // This will outline a basic point Style with a colour. Style: style } ] } ] }); ``` **Javascript** ```javascript const style = { pointStyle: { color: [ // Mapping based on 'rent' attribute. { value: { // The Wrap utility ensures the path will be readable by the style calculator. // If your attribute is nested, simply pass the path array into the function, eg: ["location", "longitude"]. field: BModels.PathUtils.Wrap(["rent"]), // The mapping type is a simple mapping of values to colours. // In this case we're looking for two specific rental costs and assigning them colours. values: [ { fieldValue: "£200", appliedValue: "rgba(50, 180, 50, 0.8)" }, { fieldValue: "£300", appliedValue: "rgba(50, 50, 180, 0.8)" } ] }, type: BModels.Calculator.EValueType.Mapping }, // Gradient based on 'location' -> 'longitude' attribute. { value: { field: BModels.PathUtils.Wrap(["location", "longitude"]), // Within this example I know the min and max values of the longitude attribute. // I am making it go from red to white as the points move along the longitude vector. points: [ { position: -1.099, color: "rgba(255, 0, 0, 0.8)" }, { position: -1.046, color: "rgba(255, 255, 255, 0.8)" } ] }, type: BModels.Calculator.EValueType.Gradient }, // Default if no above options resolved into anything. { value: "rgba(255, 255, 255, 0.4)", type: BModels.Calculator.EValueType.Color } ] } }; // Let's take our previous example and swap out some pieces. BEngine.MenuItemCreator.RenderMenuItems({ viewer: viewer, menuItems: [ { "id": BModels.ObjectUtils.UId(), "Caption": "Rentals", "Type": "BruceEntity", "BruceEntity": { "EntityType.ID": "some-entity-type-id" }, CameraZoomSettings: [ { MinZoom: 0, MaxZoom: 100000, DisplayType: BModels.ZoomControl.EDisplayType.Point, // Set this to -1 to use a programmatic Style. StyleID: -1, // Now define a Style JSON. // This will outline a basic point Style with a colour. Style: style } ] } ] }); ``` ## Changing Styles Recently we've added the ability to change what Styles are enabled within a Menu Item. That way if your setup is user driven you don't have to re-enable the Menu Item to update your Entities. Below is a simple example where we change the Style of a Menu Item after 5 seconds. In this case we changed from Style ID 1 to Style ID 2. This will work with programmatic Styles too. **Typescript** ```typescript import { MenuItemCreator, ViewerUtils } from "bruce-cesium"; import { ObjectUtils, ZoomControl } from "bruce-models"; // Let's try render one of our Entity types using this record: MenuItemCreator.RenderMenuItems({ viewer: viewer, menuItems: [ { // This can be any string ID, just as long as it doesn't clash with other Menu Items. // If you're lazy and testing stuff, you can simply use the ID: "1" or similar. "id": ObjectUtils.UId(), // Caption is useful in case a UI panel renders your Menu Item. "Caption": "Rentals", "Type": "BruceEntity", "BruceEntity": { "EntityType.ID": "some-entity-type-id" }, CameraZoomSettings: [ { // Between 0 and 100,000 meters (to the camera) your Entity will be drawn as a point with your Style. MinZoom: 0, MaxZoom: 100000, DisplayType: ZoomControl.EDisplayType.Point, StyleID: 1 } ] } ] }).then((rendered) => { const manager = ViewerUtils.GetManager({ viewer: viewer }); const menuItem = manager.GetEnabledItem(rendered.menuItemIds[0]); setTimeout(() => { menuItem.renderManager.UpdateSettings({ zoomControl: [ { MinZoom: 0, MaxZoom: 100000, DisplayType: ZoomControl.EDisplayType.Point, StyleID: 2 } ], // Default is true. It'll run through rendered entities in batches and re-render them. queueRerender: true }); }, 5000); }); ``` **Javascript** ```javascript BEngine.ViewerUtils.InitViewer({ viewer: viewer }); BEngine.MenuItemCreator.RenderMenuItems({ viewer: viewer, menuItems: [ { "id": BModels.ObjectUtils.UId(), "Caption": "Rentals", "Type": "BruceEntity", "Children": [], "BruceEntity": { "EntityType.ID": "some-entity-type-id" }, CameraZoomSettings: [ { MinZoom: 0, MaxZoom: 100000, DisplayType: BModels.ZoomControl.EDisplayType.Point, StyleID: 1 } ] } ] }).then((rendered) => { const manager = BEngine.ViewerUtils.GetManager({ viewer: viewer }); const menuItem = manager.GetEnabledItem(rendered.menuItemIds[0]); setTimeout(() => { menuItem.renderManager.UpdateSettings({ zoomControl: [ { MinZoom: 0, MaxZoom: 100000, DisplayType: BModels.ZoomControl.EDisplayType.Point, StyleID: 2 } ], // Default is true. It'll run through rendered entities in batches and re-render them. queueRerender: true }); }, 5000); }); ``` If you'd like re-render your Entity in the scene so the Style is refreshed, read through the [Entity Interaction](/webentityinteraction) documentation. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webstyleentities # Styling Tilesets (/webstyletilesets) # Styling Tilesets One of the most important aspects of rendering your data is control over how it's presented. Styling for Tilesets is limited as they're an optimized way to render large amounts of data, but there are still some options available. Within this article we'll cover how to manage Styles for Tileset Menu Items. ## Style records The most commonly used process is setting up a Style through our Operator app then pointing your Menu Item at the record's ID. Tilesets are collections of Entities which may be different types. The Menu Item will let you map Entity Types to Styles, and you can also set a default Style for the Tileset as a whole. **Warning**, it is highly recommended to avoid Styles that require attributes for your Tileset as a whole as it will require larger amounts of data to be downloaded and processed. It's best to get as specific as you can with the Entity Type mapping. **Typescript** ```typescript import { MenuItemCreator } from "bruce-cesium"; import { ObjectUtils } from "bruce-models"; // If set to "0" then it'll load the default Style for the Entity Type. // Eg: if an Entity has the Entity Type "Building" then it'll use the Building's default Style. const someStyleId = 1; // Let's try render one of our Tilesets using this record: MenuItemCreator.RenderMenuItems({ viewer: viewer, menuItems: [ { // This can be any string ID, just as long as it doesn't clash with other Menu Items. // If you're lazy and testing stuff, you can simply use the ID: "1" or similar. id: ObjectUtils.UId(), // Caption is useful in case a UI panel renders your Menu Item. Caption: "Assembly-A", Type: "ModelTileset", tileset: { TilesetID: "my-tileset-id" }, // This is your fallback Style when no specific Entity Type mapping is found. // Set this to 0 so that it'll use the Entity Type's default Style. styleId: 0, StyleMapping: [ { "EntityTypeID": "some-entity-type-id", "StyleID": someStyleId } ], // This is a setting to control how the Style colour blends with the model's original colour. // Some textures don't blend very well with the Style colour so we can favour the Style colour more. StyleBlending: { // Hightlight = 0, replace = 1, mix = 2. blendMode: 2, // 1 = 100% style colour. 0 = original model colour. blendPercent: 0.8 } } ] }); ``` **Javascript** ```javascript // If set to "0" then it'll load the default Style for the Entity Type. // Eg: if an Entity has the Entity Type "Building" then it'll use the Building's default Style. const someStyleId = 1; // Let's try render one of our Tilesets using this record: BEngine.MenuItemCreator.RenderMenuItems({ viewer: viewer, menuItems: [ { // This can be any string ID, just as long as it doesn't clash with other Menu Items. // If you're lazy and testing stuff, you can simply use the ID: "1" or similar. id: BModels.ObjectUtils.UId(), // Caption is useful in case a UI panel renders your Menu Item. Caption: "Assembly-A", Type: "ModelTileset", tileset: { TilesetID: "my-tileset-id" }, // This is your fallback Style when no specific Entity Type mapping is found. // Set this to 0 so that it'll use the Entity Type's default Style. styleId: 0, StyleMapping: [ { "EntityTypeID": "some-entity-type-id", "StyleID": someStyleId } ], // This is a setting to control how the Style colour blends with the model's original colour. // Some textures don't blend very well with the Style colour so we can favour the Style colour more. StyleBlending: { // Hightlight = 0, replace = 1, mix = 2. blendMode: 2, // 1 = 100% style colour. 0 = original model colour. blendPercent: 0.8 } } ] }); ``` If you'd like to let the user pick a Style from what is available. You can request the information like so: **Typescript** ```typescript import { Style } from "bruce-models"; Style.GetList({}).then((data) => { // There are other kinds of Styles so let's ensure we only let the user pick from entity ones. const entityStyles = data.styles.filter(x => !x.Type || x.Type == Style.EType.Entity); console.log("Entity Styles", entityStyles); // Styles have a "Name" property for the user to choose from, // then an "ID" property for you to pass to the Menu Item. }); ``` **Javascript** ```javascript BModels.Style.GetList({}).then((data) => { // There are other kinds of Styles so let's ensure we only let the user pick from entity ones. const entityStyles = data.styles.filter(x => !x.Type || x.Type == BModels.Style.EType.Entity); console.log("Entity Styles", entityStyles); // Styles have a "Name" property for the user to choose from, // then an "ID" property for you to pass to the Menu Item. }); ``` ## Programmatic Styles We of course allow you to avoid having to deal with Style records and define your own programmatically. We will cover the Style syntax in a separate article. Below is an example of how to use it. Please note that the current version only lets you write programmatic Styles per-Entity-Type rather than the Tileset as a whole. **Typescript** ```typescript import { MenuItemCreator } from "bruce-cesium"; import { ObjectUtils, PathUtils, Calculator } from "bruce-models"; const style = { modelStyle: { // Set to customize so we directly know that we should be colouring your model. customize: true, fillColor: [ { value: { // The Wrap utility ensures the path will be readable by the style calculator. // If your attribute is nested, simply pass the path array into the function, eg: ["location", "longitude"]. field: PathUtils.Wrap(["Example"]), // The mapping type is a simple mapping of values to colours. // In this case we're looking for two specific example values and assigning them colours. values: [ { fieldValue: "A", appliedValue: "#DC143C" }, { fieldValue: "B", appliedValue: "#7FFF00" } ] }, type: Calculator.EValueType.Mapping }, // Optional default if no above options resolved into anything. // Remove if you don't want stuff coloured when the above mapping isn't found. { value: "rgba(255, 255, 255, 0.4)", type: Calculator.EValueType.Color } ] }; MenuItemCreator.RenderMenuItems({ viewer: viewer, menuItems: [ { id: ObjectUtils.UId(), Caption: "Assembly-A", Type: "ModelTileset", tileset: { TilesetID: "my-tileset-id" }, styleId: 0, StyleMapping: [ { EntityTypeID: "some-entity-type-id", // Set this to -1 to use a programmatic Style. StyleID: -1, // Define the Style JSON here. // This will outline a basic style which colours models where the attribute "Example" is set to "A" or "B". Style: style } ], StyleBlending: { blendMode: 2, blendPercent: 0.8 } } ] }); ``` **Javascript** ```javascript const style = { modelStyle: { // Set to customize so we directly know that we should be colouring your model. customize: true, fillColor: [ { value: { // The Wrap utility ensures the path will be readable by the style calculator. // If your attribute is nested, simply pass the path array into the function, eg: ["location", "longitude"]. field: BModels.PathUtils.Wrap(["Example"]), // The mapping type is a simple mapping of values to colours. // In this case we're looking for two specific example values and assigning them colours. values: [ { fieldValue: "A", appliedValue: "#DC143C" }, { fieldValue: "B", appliedValue: "#7FFF00" } ] }, type: BModels.Calculator.EValueType.Mapping }, // Optional default if no above options resolved into anything. // Remove if you don't want stuff coloured when the above mapping isn't found. { value: "rgba(255, 255, 255, 0.4)", type: BModels.Calculator.EValueType.Color } ] } }; BEngine.MenuItemCreator.RenderMenuItems({ viewer: viewer, menuItems: [ { id: BModels.ObjectUtils.UId(), Caption: "Assembly-A", Type: "ModelTileset", tileset: { TilesetID: "my-tileset-id" }, styleId: 0, StyleMapping: [ { EntityTypeID: "some-entity-type-id", // Set this to -1 to use a programmatic Style. StyleID: -1, // Define the Style JSON here. // This will outline a basic style which colours models where the attribute "Example" is set to "A" or "B". Style: style } ], StyleBlending: { blendMode: 2, blendPercent: 0.8 } } ] }); ``` If you'd like re-render your Entity in the scene so the Style is refreshed, read through the [Entity Interaction](/webentityinteraction) documentation. ## Changing Styles Recently we've added the ability to change what Styles are enabled within a Menu Item. That way if your setup is user driven you don't have to re-enable the Menu Item to update your Entities. Below is a simple example where we change the Style of a Menu Item after it's been created. **Typescript** ```typescript import { Calculator, PathUtils, Style } from "bruce-models"; const menuItem = manager.GetEnabledItem("my-menu-item-id"); const renderManager = menuItem.renderManager; // Style that will look at the "temperature" attribute and colour things red if it's between 60 and 200. // It will fall back to white if the condition is not met. // Pure white in the render engine means no colouring. const style = { ID: -1, Name: "Colour hot areas", Settings: { modelStyle: { customize: true, fillColor: [ { type: Calculator.EValueType.Mapping, value: { field: PathUtils.Wrap(["temperature]), values: [{ fieldValue: "60-200", appliedValue: "red" }] } }, { type: Calculator.EValueType.Input, value: "white" } ] } } as Style.IEntitySettings, Type: Style.EType.Entity }; renderManager.Styler.UpdateStyleMapping({ // No per Type options. styleMapping: [], // -1 means don't use default styles for unspecified Entity Types. fallbackStyleId: -1, // Let's style all Entities with the attribute. fallbackStyle: style }); ``` **Javascript** ```javascript const menuItem = manager.GetEnabledItem("my-menu-item-id"); const renderManager = menuItem.renderManager; // Style that will look at the "temperature" attribute and colour things red if it's between 60 and 200. // It will fall back to white if the condition is not met. // Pure white in the render engine means no colouring. const style = { ID: -1, Name: "Colour hot areas", Settings: { modelStyle: { customize: true, fillColor: [ { type: BModels.Calculator.EValueType.Mapping, value: { field: BModels.PathUtils.Wrap(["temperature]), values: [{ fieldValue: "60-200", appliedValue: "red" }] } }, { type: BModels.Calculator.EValueType.Input, value: "white" } ] } }, Type: BModels.Style.EType.Entity }; renderManager.Styler.UpdateStyleMapping({ // No per Type options. styleMapping: [], // -1 means don't use default styles for unspecified Entity Types. fallbackStyleId: -1, // Let's style all Entities with the attribute. fallbackStyle: style }); ``` If you'd like re-render your Entity in the scene so the Style is refreshed, read through the [Entity Interaction](/webentityinteraction) documentation. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webstyletilesets # Entity records (/webentityrecords) # Entity records If you're using the web libraries then you don't have to implement your own request handing for Nextspace records. Below we'll cover some basics about how to get, update, and create Entity records. The library supports a wide range of functions that may not be covered below. If a particular use-case is not covered, please contact us and we'll add it. ## Getting records If you've setup your environment by reading the getting started documentation then you should be good to go for communication with your Nextspace account. Below is a few code examples on getting records. **Typescript** ```typescript import { Entity } from "bruce-models"; // Get by known Entity ID. Entity.Get({ entityId: "my-entity-id" }).then((data) => { console.log("entity", data.entity); }).catch((e) => { // Forbidden, not-found, etc... console.error("error", e); }) // Get by known array of IDs. Entity.GetListByIds({ entityIds: ["my-entity-id-1", "my-entity-id-2"] }).then((data) => { console.log("entities", data.entities); }).catch((e) => { // Server error. console.error("error", e); }); // Get by known entity-type ID. Entity.GetList({ filter: { entityTypeId: "my-entity-type-id", pageSize: 100, pageIndex: 0 } }).then((data) => { console.log("entities", data.entities); }).catch((e) => { // Server error. console.error("error", e); }); ``` **Javascript** ```javascript // Get by known Entity ID. BModels.Entity.Get({ entityId: "my-entity-id" }).then((data) => { console.log("entity", data.entity); }).catch((e) => { // Forbidden, not-found, etc... console.error("error", e); }) // Get by known array of IDs. BModels.Entity.GetListByIds({ entityIds: ["my-entity-id-1", "my-entity-id-2"] }).then((data) => { console.log("entities", data.entities); }).catch((e) => { // Server error. console.error("error", e); }); // Get by known entity-type ID. BModels.Entity.GetList({ filter: { entityTypeId: "my-entity-type-id", pageSize: 100, pageIndex: 0 } }).then((data) => { console.log("entities", data.entities); }).catch((e) => { // Server error. console.error("error", e); }); ``` ## Update a record If you'd like to update a record it is standard practice to get the latest data before shipping your changes. We offer request parameters to only update attributes that you pass, but it's best to get the latest data and swap out the attributes you want changed when you want to save. If you'd like re-render your Entity in the scene so the Style is refreshed, read through the [Entity Interaction](/webentityinteraction) documentation. **Typescript** ```typescript import { Entity } from "bruce-models"; const myEntityData = { // Bruce is an old codeword for our API :) Bruce: { // ID and Entity Type ID are required when updating your record. // The Entity Type ID ensures we're working with the data-schema you expect. ID: "my-entity-id", "EntityType.ID": "my-entity-type-id" }, "myAttribute": "my-value" }; Entity.Update({ entity: myEntityData, // If true, will override any existing data. If false, only supplied attributes will be updated. // Ensure your data-schema matches the data well otherwise sub-attributes will be overridden regardless! override: false }).then((data) => { // The full record is returned back to you. console.log("entity", data.entity); }).catch((e) => { // Forbidden, server error, etc... console.error("error", e); }); ``` **Javascript** ```javascript const myEntityData = { // Bruce is an old codeword for our API :) Bruce: { // ID and Entity Type ID are required when updating your record. // The Entity Type ID ensures we're working with the data-schema you expect. ID: "my-entity-id", "EntityType.ID": "my-entity-type-id" }, "myAttribute": "my-value" }; BModels.Entity.Update({ entity: myEntityData, // If true, will override any existing data. If false, only supplied attributes will be updated. // Ensure your data-schema matches the data well otherwise sub-attributes will be overridden regardless! override: false }).then((data) => { // The full record is returned back to you. console.log("entity", data.entity); }).catch((e) => { // Forbidden, server error, etc... console.error("error", e); }); ``` ## Create a record Creating a record is the same as updating except you don't need to supply an ID. The server will generate one for you. **Typescript** ```typescript import { Entity } from "bruce-models"; const myEntityData = { Bruce: { ID: null, // Entity Type ID is required when creating your record. // This will ensure we're working with the data-schema you expect. "EntityType.ID": "my-entity-type-id" }, // Attributes are optional. Empty records are allowed. "myAttribute": "my-value", "myOtherAttribute": "my-other-value" }; Entity.Update({ entity: myEntityData }).then((data) => { // The created record is returned back to you. console.log("entity", data.entity); }).catch((e) => { // Forbidden, server error, etc... console.error("error", e); }); ``` **Javascript** ```javascript const myEntityData = { Bruce: { ID: null, // Entity Type ID is required when creating your record. // This will ensure we're working with the data-schema you expect. "EntityType.ID": "my-entity-type-id" }, // Attributes are optional. Empty records are allowed. "myAttribute": "my-value", "myOtherAttribute": "my-other-value" }; BModels.Entity.Update({ entity: myEntityData }).then((data) => { // The created record is returned back to you. console.log("entity", data.entity); }).catch((e) => { // Forbidden, server error, etc... console.error("error", e); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webentityrecords # Files (/webfiles) # Files Files in Nextspace come in three different forms. They are either Client Files, temporary files, and Tileset files. ## Client Files Client Files are the most common kind of file you will find your Nextspace account. You will be able to upload these yourself and tie them to Entity records as attachments. Client Files will also be used for the following: - Markup media - Bookmark thumbnails - Branding Here is how you can upload a Client File to your Nextspace account. This example will account for large files and use a multi-part upload when a size threshold is reached. **Typescript** ```typescript import { ENVIRONMENT, ClientFile } from "bruce-models"; // Setup global defaults. ENVIRONMENT.PARAMS = { ...ENVIRONMENT.PARAMS, accountId: "your-account-id", sessionId: "your-session-id" }; // Some file, eg: selected from a file input. Cannot be null. const file = null; ClientFile.Upload({ file: file }).then((data) => { console.log(data); }).catch((e) => { console.error(e); }); // Here is how you can monitor progress as it uploads. ClientFile.Upload({ file: file, onProgress: (progress) => { // Includes a % value and an "uploaded" boolean. console.log(progress); } }).then((data) => { console.log(data); }).catch((e) => { console.error(e); }); // The uploaded record carries its own download URL. ClientFile.Upload({ file: file }).then((data) => { console.log(data.clientFile.URL); }); ``` **Javascript** ```javascript // Setup global defaults. BModels.ENVIRONMENT.PARAMS = { ...BModels.ENVIRONMENT.PARAMS, accountId: "your-account-id", sessionId: "your-session-id" }; // Some file, eg: selected from a file input. Cannot be null. const file = null; BModels.ClientFile.Upload({ file: file }).then((data) => { console.log(data); }).catch((e) => { console.error(e); }); // Here is how you can monitor progress as it uploads. BModels.ClientFile.Upload({ file: file, onProgress: (progress) => { // Includes a % value and an "uploaded" boolean. console.log(progress); } }).then((data) => { console.log(data); }).catch((e) => { console.error(e); }); // The uploaded record carries its own download URL. BModels.ClientFile.Upload({ file: file }).then((data) => { console.log(data.clientFile.URL); }); ``` ## Entity attachments Entity attachments are Client Files that are tied to Entity records. These attachments are commonly seen in Navigator in the 'Media' and 'Attachments' tabs within the selected Entity panel. Here is how you can upload an attachment to your Nextspace Entity. It is recommended to keep attachments under 100MB in size. **Typescript** ```typescript import { ENVIRONMENT, EntityAttachment, ClientFile } from "bruce-models"; // Setup global defaults. ENVIRONMENT.PARAMS = { ...ENVIRONMENT.PARAMS, accountId: "your-account-id", sessionId: "your-session-id" }; // Some file, eg: selected from a file input. Cannot be null. const file = null; EntityAttachment.Upload({ file: file, entityId: "your-entity-id", // Replace with "photo" to upload into the Navigator 'media' tab. attachmentTypeId: "document" }).then((data) => { console.log(data); // Read the Client File record to get its download URL. const clientFileId = data.attachment["ClientFile.ID"]; ClientFile.Get({ fileId: clientFileId }).then((res) => { console.log(res.clientFile.URL); }); }).catch((e) => { console.error(e); }); ``` **Javascript** ```javascript // Setup global defaults. BModels.ENVIRONMENT.PARAMS = { ...BModels.ENVIRONMENT.PARAMS, accountId: "your-account-id", sessionId: "your-session-id" }; // Some file, eg: selected from a file input. Cannot be null. const file = null; BModels.EntityAttachment.Upload({ file: file, entityId: "your-entity-id", // Replace with "photo" to upload into the Navigator 'media' tab. attachmentTypeId: "document" }).then((data) => { console.log(data); // Read the Client File record to get its download URL. const clientFileId = data.attachment["ClientFile.ID"]; BModels.ClientFile.Get({ fileId: clientFileId }).then((res) => { console.log(res.clientFile.URL); }); }).catch((e) => { console.error(e); }); ``` ## Temporary files Temporary Files are files that are uploaded to Nextspace for a short period of time. They are used for the following: - Importing certain files - Exporting certain formats Here is how you can upload a temporary file to your Nextspace account. This example will account for large files and use a multi-part upload when a size threshold is reached. **Typescript** ```typescript import { ENVIRONMENT, ClientFile } from "bruce-models"; // Setup global defaults. ENVIRONMENT.PARAMS = { ...ENVIRONMENT.PARAMS, accountId: "your-account-id", sessionId: "your-session-id" }; // Some file, eg: selected from a file input. Cannot be null. const file = null; ClientFile.UploadTemp({ file: file }).then((data) => { console.log(data); }).catch((e) => { console.error(e); }); // Here is how you can monitor progress as it uploads. ClientFile.UploadTemp({ file: file, onProgress: (progress) => { // Includes a % value and an "uploaded" boolean. console.log(progress); } }).then((data) => { console.log(data); }).catch((e) => { console.error(e); }); ``` **Javascript** ```javascript // Setup global defaults. BModels.ENVIRONMENT.PARAMS = { ...BModels.ENVIRONMENT.PARAMS, accountId: "your-account-id", sessionId: "your-session-id" }; // Some file, eg: selected from a file input. Cannot be null. const file = null; BModels.ClientFile.UploadTemp({ file: file }).then((data) => { console.log(data); }).catch((e) => { console.error(e); }); // Here is how you can monitor progress as it uploads. BModels.ClientFile.UploadTemp({ file: file, onProgress: (progress) => { // Includes a % value and an "uploaded" boolean. console.log(progress); } }).then((data) => { console.log(data); }).catch((e) => { console.error(e); }); ``` ## Tileset files A Tileset file can be uploaded into a Tileset record to either process the file into streamable graphics, or to directly provide pre-process streamable graphics. For example a ".las" file can be uploaded into a Tileset record to be processed into a streamable point cloud. Or a ".b3dm" file can be uploaded into a Tileset record to directly provide a streamable 3D model. Tileset files are typically not tracked as they can come in large quantities and are not typically referenced outside of rendering the Tileset. Here is how you can upload a Tileset file to your Nextspace account. This example will account for large files and use a multi-part upload when a size threshold is reached. **WARNING**: Do not use these functions for 'Static' Tilesets as they're considered legacy and require a different upload process. **Typescript** ```typescript import { ENVIRONMENT, Tileset } from "bruce-models"; // Setup global defaults. ENVIRONMENT.PARAMS = { ...ENVIRONMENT.PARAMS, accountId: "your-account-id", sessionId: "your-session-id" }; // Some file, eg: selected from a file input. Cannot be null. const file = null; // Upload a source file for processing. Eg: a '.las' file. Tileset.UploadSrcFile({ tilesetId: "your-tileset-id", file: file }).then(() => { console.log("success!"); }).catch((e) => { console.error(e); }); // Upload a file for direct use. Eg: a '.b3dm' file. Tileset.UploadFile({ tilesetId: "your-tileset-id", file: file }).then(() => { console.log("success!"); }).catch((e) => { console.error(e); }); ``` **Javascript** ```javascript // Setup global defaults. BModels.ENVIRONMENT.PARAMS = { ...BModels.ENVIRONMENT.PARAMS, accountId: "your-account-id", sessionId: "your-session-id" }; // Some file, eg: selected from a file input. Cannot be null. const file = null; // Upload a source file for processing. Eg: a '.las' file. BModels.Tileset.UploadSrcFile({ tilesetId: "your-tileset-id", file: file }).then(() => { console.log("success!"); }).catch((e) => { console.error(e); }); // Upload a file for direct use. Eg: a '.b3dm' file. BModels.Tileset.UploadFile({ tilesetId: "your-tileset-id", file: file }).then(() => { console.log("success!"); }).catch((e) => { console.error(e); }); ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webfiles # Getting started (with Python) (/pythongettingstarted) # Getting started (with Python) Nextspace provides a Python library to interact with the Nextspace API. The library is available on PyPi and can be installed using pip. [Page link](https://pypi.org/project/bruce-models/) It is deliberately lighter than the web library and does not aim to match it feature for feature. What is covered is Entities and Entity Types, Data Lab queries and actions, Change Sets, historic data, Client Files including Value Maps, Scenarios, Sessions, Accounts and Pending Actions. ## Setup This library needs Python 3.8 or above. That floor exists to meet the interpreters partner installations actually ship with rather than to track upstream, so anything newer is fine. To install, simply use the following command: ```bash pip install bruce-models ``` The base install pulls in `requests` and nothing else. Reading a Value Map needs real array and image handling, which is too heavy to put on an install that only wants the API models, so it lives behind an extra. **Install it now if you plan to analyse Value Maps**, rather than finding out at the point of decoding a frame. ```bash pip install bruce-models[valuemap] ``` Everything else works without it. Importing the Value Map module on a base install succeeds too, and only the decoding fails, with a message naming the command above. See [FAQ: Analyzing Value Maps](/faqanalyzingvaluemapsinpython) for what it does. Verify the installation by importing the library in your Python script. ```python import bruce_models as BM ``` You will be using two API instances for your communication with the Nextspace API. The Guardian API and the Bruce API. The Guardian API is used for authentication and account management, while the Bruce API is your personal API for your account. Here is how you create an instance of each API: ```python import bruce_models as BM # Creating an instance of our Guardian API. # By default, this communicates to our production login server. guardian = BM.GuardianApi() # Creating an instance of our Bruce API. bruce = BM.BruceApi({ "account_id": "your-account-id", "session_id": "session-if-already-known", # Pass in your instance so it doesn't generate its own. # This is optional and an optimization. "guardian": guardian }) # Perform a test request to verify everything is correctly working. test = bruce.GET("test") print(test) ``` The `session_id` accepts either a session token from a login or a long-lived access token. See [Authentication](/pythonauth) for both. Identify your account with `account_id` and let the library resolve where its API lives. That lookup happens once, through Guardian, and it is what makes the same code work against any account in any region without changing a url. ## What is available Almost everything is reachable off the top-level module. The exception is noted below. ```python import bruce_models as BM # Connecting. BM.GuardianApi # Authentication and account management. BM.BruceApi # Your account's API. BM.Session # Logging in and out. BM.Account # Account records and where their API lives. # Records. BM.Entity # Entity records, and reading/writing their values. BM.EntityType # Entity Type records. BM.EntityHistoricData # Timestamped values on an Entity. BM.EntityIndex # Index definitions for an Entity Type. BM.Scenario # Scenario records. BM.Tileset # Tileset records. # Querying. BM.DataLabBuilder # Builds and runs Data Lab queries and actions. BM.DataLabPairing # Secondary queries, from DataLabBuilder.secondary(). BM.Ref # Compares against another attribute, or the primary. # Writing in bulk. BM.ChangeSetBuilder # Queues edits locally, then applies or saves them. BM.ChangeSet # The Change Set record itself. # Files. BM.ClientFile # File uploads and downloads. BM.ClientFileQueryBuilder # Finding files by purpose, data or layer. BM.Uploader # The multi-part upload used for large files. # Jobs and errors. BM.PendingAction # Monitoring server-side background jobs. BM.ApiError # A request that failed outright. Unauthorized, # Forbidden, NotFound and ServerError derive from it. BM.PartialFailure # A 2xx that reported some of the work did not happen. # Separate from ApiError: the request succeeded. # Helpers. BM.PathUtils # Attribute paths to and from their quoted form. BM.GeometryBuilder # Builds the shapes spatial criteria take. BM.Bounds # An Entity's extent, from its geometry and points. BM.GeoJson # GeoJSON in and out. BM.LRUCache # A bounded cache, for a walk that repeats work. ``` `ClientFileValueMap` is the one thing not exported from the top level, because it belongs to the optional extra. Import it by path: ```python from bruce_models.client_file.client_file_value_map import ClientFileValueMap ``` ## The command line The library is not only importable. Installing it also installs a `bruce` command, so the same models drive a command line that configures once and then answers one-off questions without a script: who a credential is, what it can reach, what an Account holds, and seeded end-to-end tests against an installation. There is a public Docker image too, for a machine with no Python on it. ```bash bruce --help ``` Anything configured there is read by the library as well, so a script picks up the same defaults. See [CLI: Getting started](/cligettingstarted) and [CLI: Docker](/clidocker). --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/pythongettingstarted # How do I gather Entities in Python? (/faqentitygatheringinpython) # How do I gather Entities in Python? It is common to want to get all Entity records in Python for processing or analysis. A Data Lab query does the paging for you. Hand `each_page` a callback and it reads one page at a time, so nothing but the current page is held no matter how large the Entity Type is. Each page carries its own position, so progress needs no counting of your own. You will want to install our Python library to facilitate this process. ```bash pip install bruce-models ``` **Python** ```python import bruce_models as BM ENTITY_TYPE_ID = "YOUR_ENTITY_TYPE_ID" # Either a session token from a login or a long-lived access token. TOKEN = "YOUR_TOKEN" # Setup the API instance to communicate with your account. bruce = BM.BruceApi({ "account_id": "{accountId}", "session_id": TOKEN }) # The selection to read. Nothing is requested until you walk it. query = BM.DataLabBuilder(bruce, page_size=1000).entity_type(ENTITY_TYPE_ID) # How many there are, without transferring any of them. print(f"{query.count():,} Entities to gather") all_entities = [] def gather(page): """ Handles one page of Entities. """ all_entities.extend(page.items) print(f"page {page.number}: {len(page)} Entities, {page.seen:,} so far") # Reads a page at a time, so only one page is in flight. walked = query.each_page(gather) print(f"Total Entities collected: {len(all_entities):,} " f"over {walked['pages']} page(s)") ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/faqentitygatheringinpython # How do I gather a full Entity ontology in Python? (/faqentityontologyinpython) # How do I gather a full Entity ontology in Python? It is common to want to get all Entity records and associated concepts in Python for processing or analysis. Here is a simple example that will perform a Data Lab action to export the results to JSON. The export result will be a ZIP file containing JSON files (100k Entities per file) for a query and related concepts of interest. Attachments and LODs will include their download URLs and thumbnail URLs if available. You will want to install our Python library to facilitate this process. ```bash pip install bruce-models ``` **Python** ```python import bruce_models as BM ENTITY_TYPE_ID = "YOUR_ENTITY_TYPE_ID" # Either a session token from a login or a long-lived access token. TOKEN = "YOUR_TOKEN" # Setup the API instance to communicate with your account. bruce = BM.BruceApi({ "account_id": "{accountId}", "session_id": TOKEN }) # The selection to export. Nothing is requested until an action runs. query = BM.DataLabBuilder(bruce).entity_type(ENTITY_TYPE_ID) def start_job(query): """ Starts a Data Lab job to export Entities and their related concepts. """ response = query.run_action_export( "export-json", expand="EntityType,LOD,Source,Relation,Attachment" ) return BM.DataLabBuilder.action_id(response) def await_job(bruce, action_id): """ Awaits for the Data Lab job to complete. """ result = BM.PendingAction.on_completion( bruce, action_id, on_progress=lambda a: print(f"Job status: {a.get('Status')} {a.get('Progress')}%") ) action = result.get("action") # A failed job returns rather than raising, so the messages are available. if action.get("Status") != BM.EStatus.COMPLETE: for message in result.get("messages"): print(message.get("Message")) raise Exception(f"Job failed with status: {action.get('Status')}") return action job_id = start_job(query) action = await_job(bruce, job_id) # The export lands in the account's temporary store. print("Download URL:", BM.DataLabBuilder.export_url(action)) print("File name:", BM.DataLabBuilder.export_file_name(action)) ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/faqentityontologyinpython # How do I load and update Entities in batches in Python? (/faqbatchentityupdatesinpython) # How do I load and update Entities in batches in Python? Reading an Entity Type in one request and writing it back in one request both stop working once the Type is large. The fix is the same in each direction: work a batch at a time. The example below walks an Entity Type a page at a time, queues a change per record, and writes the queue out in batches at the end. A Change Set built locally holds the edits, merges repeated edits to one Entity, and spills to disk if the walk queues more than memory should hold, so the run stays flat no matter how large the Type is. A failure costs you one batch rather than the whole run, and the report names what did not land. You will want to install our Python library to facilitate this process. ```bash pip install bruce-models ``` **Python** ```python import bruce_models as BM ENTITY_TYPE_ID = "YOUR_ENTITY_TYPE_ID" # Either a session token from a login or a long-lived access token. TOKEN = "YOUR_TOKEN" # The attribute being changed, as path segments. ATTR_PATH = ["status"] BATCH_SIZE = 500 bruce = BM.BruceApi({ "account_id": "{accountId}", "session_id": TOKEN }) # The selection to walk. Nothing is requested until you walk it. query = BM.DataLabBuilder(bruce, page_size=BATCH_SIZE).entity_type(ENTITY_TYPE_ID) # Collects the edits locally. Nothing is sent while it fills. changes = BM.ChangeSetBuilder(bruce) def consider(entity): """ Queues a change for one Entity, or leaves it alone. """ if BM.Entity.get_value(entity, ATTR_PATH) == "Active": return values = {} BM.Entity.set_value(values, ATTR_PATH, "Active") # Reads the ID and Entity Type off the record for you. changes.queue(entity, values) def read(page): """ Handles one page of Entities. """ for entity in page.items: consider(entity) print(f"read page {page.number}, {page.seen:,} seen, " f"{len(changes):,} queued") query.each_page(read) def saving(state): """ Reports each batch of writes as it settles. """ print(f"[{state['percent']:>3}%] saved {state['processed']:,} " f"of {state['requested']:,}") # Writes the queue out in batches. The API overlays what it is sent # onto each record, so only the attribute above changes. report = changes.apply(batch_size=BATCH_SIZE, on_progress=saving) print(f"{report['succeeded']:,} updated, {report['failed_count']:,} failed") for entity_id, reason in list(report["failed"].items())[:5]: print(f" {entity_id}: {reason}") ``` ## Things worth knowing **Only send what changed.** Writing back every record you read costs the same as writing back the handful that differ, and it re-indexes records that did not need it. Queueing nothing for a record that already holds the value, as above, is cheaper than deciding it server side. **Reading before writing.** The example queues every change during the walk and writes afterwards, so nothing shifts underneath the paging. Writing as you go is faster to first result, but if the change stops a record matching the filter, the pages move while you are reading them. **Use set_value rather than assigning into the dictionary.** Attributes address by quoted path, and some of them are mid-migration between two locations in the record. The helper writes the right one. **High volume writes.** If you are pushing many batches back to back and do not need each response immediately, the API accepts a `Defer` flag that queues the save instead of blocking on it. Pass it as a request param when throughput matters more than confirmation. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/faqbatchentityupdatesinpython # How do I find Entities that overlap another Entity Type in Python? (/faqoverlappingentitiesinpython) # How do I find Entities that overlap another Entity Type in Python? "Which pipes run through this building" and "which sensors sit inside this zone" are the same question: find the Entities of one Type that overlap the Entities of another. Data Lab answers it with two selections. The primary selection describes what you are measuring against, the secondary selection describes what you want back, and a spatial criterion in the secondary compares each candidate against the primary. The secondary selection is the one that runs, so it is the set you get. The library builds both selections for you. Build a query per Entity Type, pair them with `secondary`, and add the spatial criterion. Below, the matches are walked and then stamped with an attribute through a Change Set. You will want to install our Python library to facilitate this process. ```bash pip install bruce-models ``` **Python** ```python import bruce_models as BM # What we are measuring against, and what we want back. PRIMARY_TYPE_ID = "YOUR_CONTAINER_ENTITY_TYPE_ID" SECONDARY_TYPE_ID = "YOUR_TARGET_ENTITY_TYPE_ID" # The attribute stamped on each match, as path segments. ATTR_PATH = ["inside_building"] # Primary Entities compared per join. BATCH = 50 # Either a session token from a login or a long-lived access token. TOKEN = "YOUR_TOKEN" bruce = BM.BruceApi({ "account_id": "{accountId}", "session_id": TOKEN }) containers = BM.DataLabBuilder(bruce).entity_type(PRIMARY_TYPE_ID) targets = BM.DataLabBuilder(bruce).entity_type(SECONDARY_TYPE_ID) # The secondary Entities that touch or overlap a primary. # Use within_primary() instead for fully covered only. overlapping = containers.secondary(targets, batch=BATCH).intersecting_primary() print(f"{overlapping.count():,} matches") changes = BM.ChangeSetBuilder(bruce) def stamp(primary_id, secondaries): """ Handles the matches for one primary Entity. """ for entity in secondaries: values = {} BM.Entity.set_value(values, ATTR_PATH, primary_id) changes.queue(entity, values) # An Entity inside two overlapping primaries is answered once per # primary, and the queue merges those into one edit per Entity. matched = overlapping.each_group(stamp) print(f"{matched['primaries']:,} primaries matched something, " f"{matched['matches']:,} matches, {len(changes):,} to update") report = changes.apply() print(f"{report['succeeded']:,} updated, {report['failed_count']:,} failed") ``` ## Choosing the spatial comparison Three attribute paths address an Entity's spatial data rather than one of your own attributes, and each answers a different question: `"Bruce"/"Boundaries"` is the bounding box. Cheapest to compare, and the right choice when a box is a good enough answer, which it usually is for "roughly inside this building". `"Bruce"/"VectorGeometry"` is the exact shape. Use it when the difference between the shape and its box matters, and expect it to cost more. `"Bruce"/"Location"` is the single point. Use it with `DISTANCETO` and a `Distance` in metres for "within 50 metres of". The spatial operators are `INTERSECTS` (the shapes touch or overlap), `INSIDE` and `CONTAINS` (the primary fully covers the candidate), `BOUNDSOVERLAP` (a fast, loose box overlap) and `DISTANCETO`. ## Things worth knowing **The secondary selection is what comes back.** Swapping which Type is primary and which is secondary changes the answer, not just the wording. Put the set you want to act on in the secondary. **Updating the set you are paging through.** The example queues every edit during the walk and applies them afterwards, so the join cannot shift underneath the paging even when the change would stop a record matching. **Doing it all server side.** If you only need to stamp an attribute on the matches, the pairing can do the whole thing without returning the records: `overlapping.run_action_set_attribute(path, value)`. That returns a Pending Action to follow rather than a list, and avoids moving the Entities over the network at all. It also takes a `Ref` as the value, which is how you copy something off the primary onto each match. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/faqoverlappingentitiesinpython # How do I push historic data to Entities in Python? (/faqpushhistoricdatainpython) # How do I push historic data to Entities in Python? Historic records let an Entity keep its previous values against a date and time, so you can ask what it looked like at any point rather than only what it looks like now. Backfilling readings from another system is the usual reason to write them yourself. ## First, connect an attribute to a date-time History is keyed on one of your own date-time attributes. In the Entity Type's Data Schema, set `HistoricKey` on each attribute you want tracked, pointing at that date-time attribute. That date-time is the historic key, and the attributes pointing at it are the historic attributes. Nothing is recorded until that link exists, so this is the step to check first when a write appears to do nothing. See the [Entity historic data documentation](/apientityhistoricdata) for the full definition. ```json { "Key": "root", "Name": "Root", "Type": "Structure", "Structure": [ { "Key": "reading_taken", "Name": "Reading taken", "Type": "Datetime" }, { "Key": "temperature", "Name": "Temperature", "Type": "Double", "HistoricKey": "reading_taken" } ] } ``` For an attribute nested inside a `Structure`, the `HistoricKey` is slash separated from the root. Eg: `readings/taken`. With that in place, updating an Entity with a new value in the date-time attribute records the previous state automatically. The rest of this page is about writing records directly, which is what a backfill needs. You will want to install our Python library to facilitate this process. ```bash pip install bruce-models ``` **Python** ```python import bruce_models as BM ENTITY_TYPE_ID = "YOUR_ENTITY_TYPE_ID" # Either a session token from a login or a long-lived access token. TOKEN = "YOUR_TOKEN" # The date-time attribute history is keyed on, as path segments. HISTORIC_KEY = ["reading_taken"] BATCH_SIZE = 500 bruce = BM.BruceApi({ "account_id": "{accountId}", "session_id": TOKEN }) # Readings to load, keyed by the Entity they belong to. # In a real backfill this comes from your own system. readings_by_entity = { "entity-id-one": [ ("2026-01-01T09:00:00Z", 21.4), ("2026-01-02T09:00:00Z", 22.1) ], "entity-id-two": [ ("2026-01-01T09:00:00Z", 18.9) ] } def build_records(readings_by_entity): """ Turns the readings into historic records. """ records = [] for entity_id, readings in readings_by_entity.items(): for date_time, temperature in readings: records.append(BM.EntityHistoricData.build_record( entity_id, HISTORIC_KEY, date_time, # The recorded values are shaped like the Entity itself. { "reading_taken": date_time, "temperature": temperature } )) return records def push(bruce, records): """ Writes the records a batch at a time. """ written = 0 for start in range(0, len(records), BATCH_SIZE): batch = records[start:start + BATCH_SIZE] res = BM.EntityHistoricData.update(bruce, batch) written += len(res.get("Items") or []) # A record that could not be written is reported rather than failing the request. for warning in (res.get("Warning") or []): print(warning) return written records = build_records(readings_by_entity) print(f"pushing {len(records)} records") print(f"wrote {push(bruce, records)}") # Reading them back, which returns the same shape that went in. for record in BM.EntityHistoricData.iterate( bruce, list(readings_by_entity.keys()), attr_key=HISTORIC_KEY ): values = record.get("Entity") or {} print(record.get("Entity.ID"), record.get("DateTime"), values.get("temperature")) ``` ## Recording where the Entity was, too A record keeps a full snapshot of the platform managed values as well as your attributes, so a moving Entity can keep its past positions. Pass them as `internal` and they are nested under the record's `Bruce` key. ```python record = BM.EntityHistoricData.build_record( "entity-id-one", ["surveyed"], "2026-01-01T09:00:00Z", {"surveyed": "2026-01-01T09:00:00Z"}, internal={"Location": {"latitude": -36.8485, "longitude": 174.7633, "altitude": 0}} ) ``` ## Reading it back as the Entity Once written, the records are what a point-in-time read overlays. Ask for an Entity at a date and time and the nearest matching record replaces its values, reported in the `Outline` like any other source. ```python entity = BM.Entity.get( bruce, "entity-id-one", historic_point="2026-01-01T12:00:00Z" ) # The Outline says which record was used and what it contributed. for source in (entity.get("Bruce").get("Outline") or []): if source.get("DateTime"): print("values from", source.get("DateTime"), "keyed on", source.get("ForeignKey")) ``` ## Things worth knowing **Write the key attribute into the values.** The date-time named by the historic key should appear in the recorded values as well as in the record's own `DateTime`. A record missing it still saves, but reading the Entity back at that point in time will not show the date it came from. **There is a record limit per Entity.** The default is 1,000 records per Entity ID unless an upgraded limit is discussed. Past that, the oldest are dropped, so a long backfill at a fine interval can quietly evict its own earliest records. **Writing a record does not change the Entity.** Its current values are untouched, which is the point. Update the Entity itself if you also want the latest state to move. **Scenarios.** Passing a `scenario` writes the series as a variant rather than as real world data, and a Scenario identifies its own records, so the date-time and attribute become optional there. **Removing a bad backfill.** Two options. `delete_records` takes the records themselves, so the ones you just wrote or just read are the ones removed. `delete` takes the same filter as a read, so the range you wrote is the range you clear; it requires the attribute and both ends of the range, which is what stops an unbounded delete. ```python # The write response returns what landed, so it can be handed straight back. res = BM.EntityHistoricData.update(bruce, records) BM.EntityHistoricData.delete_records(bruce, res.get("Items") or []) ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/faqpushhistoricdatainpython # How do I analyse a Value Map in Python? (/faqanalyzingvaluemapsinpython) # How do I analyse a Value Map in Python? A Value Map is a Client File holding one attribute as a raster over a geographic area and a series of times, with a generation block on the record describing how to read it: what the numbers mean and in what units, the extent they cover, the tiles they are cut into, and where every raster sits in the blob. A simulation writes one per attribute, so a flood model might publish water level, velocity and wave height as separate Value Maps over the same area. The question this answers is "what did the event touch". The Value Map says what the values were and where, and a Data Lab query says which of your Entities were there, so the two together say which records the event reached and when. Reading one needs the optional extra, which a base install does not have. ```bash pip install bruce-models[valuemap] ``` ## Finding a Value Map Value Maps are Client Files, so they are found the same way as any other file. Each one names the attribute and the source it was generated from under `Identity` in its generation block, and those two together identify it. **Python** ```python import bruce_models as BM # Not exported from the package root, so it is imported by path. from bruce_models.client_file.client_file_value_map import ClientFileValueMap # Either a session token from a login or a long-lived access token. TOKEN = "YOUR_TOKEN" bruce = BM.BruceApi({ "account_id": "{accountId}", "session_id": TOKEN }) # Every Value Map on the account, newest first. query = (BM.ClientFileQueryBuilder(bruce) .purpose(BM.ClientFile.PURPOSE_VALUE_MAP_SEARCH) .newest_first()) for record in query.files(): generation = (record.get("Data") or {}).get("generation") or {} identity = generation.get("Identity") or {} series = generation.get("Series") or {} print(f"{record.get('ID')} " f"attribute={identity.get('Attribute')} " f"dataset={identity.get('Dataset')} " f"times={len(series.get('Times') or [])}") # When you know the source and attribute, ask for that one directly. one = BM.ClientFile.find_generated_value_map( bruce, entity_type_source_id=YOUR_SOURCE_ID, attribute="water_level" ) ``` ## Reading what it holds Opening one downloads the blob once and reads the generation block off the record. From there everything is local, so walking every frame costs no requests at all. Check `is_complete` before you read anything. It says whether every timestep that was asked for was actually written, and a series with a gap in the middle of the event otherwise looks exactly like a shorter series. Cells that report the same value in every frame carry nothing a change can be read from, whether that is a permanent feature or an artefact of the model, and they are excluded for you. **Python** ```python value_map = ClientFileValueMap.open(bruce, VALUE_MAP_FILE_ID) print(f"{len(value_map)} frames of {value_map.attribute} in {value_map.units}") print(f"{value_map.grid.width} x {value_map.grid.height} texels") print(f"values {value_map.value_range[0]:.3f} to {value_map.value_range[1]:.3f}") if not value_map.is_complete: print(f"only {value_map.written_frames} of " f"{value_map.requested_frames} timesteps were written") # Stated by you, since what counts as present depends on the attribute. THRESHOLD = 0.0 # The frames worth reading: the event arriving, its worst, and it passing. chosen = value_map.frames_of_interest(THRESHOLD) print("frames of interest:", list(chosen)) # Which texels one frame holds a value over, and what was in them. over = value_map.mask_above(THRESHOLD, frame=chosen[0]) values, covered = value_map.frame(chosen[0]) print(f"{int(over.sum()):,} texels over the threshold") # Texels holding a value in every frame, which is what never receded. print(f"{int(value_map.always_above(THRESHOLD).sum()):,} always over it") ``` ## Your own numbers, where they were corrected Values are delivered in whatever reference the platform draws them against, which is not always the one your data was authored in. Where a correction was applied the Value Map carries it per texel, so the reading you supplied is recoverable exactly rather than described in prose. `value_source` says whether anything was applied.`frame(index, source=True)` takes it back off, and the same flag runs through `mask_above`, `peak_at` and`mean_at`, so a threshold stated against your own data is compared against your own data. On a Value Map that declares no correction the two are the same numbers. **Python** ```python print(value_map.vertical_datum, value_map.transform, value_map.source_range) delivered, covered = value_map.frame(0) mine, mine_covered = value_map.frame(0, source=True) # A level you know from your own data, measured against your own data. reached = value_map.mask_above(1.25, frame=0, source=True) ``` ## Sampling one place When the question is about a site rather than a whole frame, sample it directly.`sample_at` reads one position and`series_at` reads that position through the whole series, each returning the delivered number and your own alongside each other, so a script can report either or both without deciding up front which one it works in. Both read only the tile the position falls in, so watching one site through a long series costs a fraction of walking the frames. `sample_of` and`series_of` do the same for an Entity, which covers several texels and so reports a peak and a mean rather than a single reading. A position outside the raster comes back as nothing at all, while one inside it that was never sampled comes back with `covered` false and no value. Those are different answers and it is worth reporting them differently. **Python** ```python reading = value_map.sample_at(174.76, -36.85, frame=0) if reading is None: print("outside the modelled area") elif not reading["covered"]: print("inside it, but nothing was modelled there") else: print(f"{reading['time']}: {reading['value']:.3f} {reading['units']} " f"(yours: {reading['source_value']:.3f})") # The same place through every timestep. series = value_map.series_at(174.76, -36.85) worst = max((r for r in series if r["value"] is not None), key=lambda r: r["value"]) print(f"peaked at {worst['value']:.3f} on {worst['time']}") # One record, which covers several texels. summary = value_map.sample_of({"Bruce": item}) print(f"{summary['covered']} of {summary['texels']} texels modelled, " f"peak {summary['peak']}, mean {summary['mean']}, " f"peak in your own numbers {summary['source_peak']}") ``` ## What the event reached An Entity resolves to the texels it covers, and the raster does not move, so list the Entities once, index them by their texels, and every frame after that is answered locally. The cost then follows the size of the event rather than the number of records, and the match is exact against the raster. `shapes_above` is the other way round: it traces each area of a frame into a Bruce geometry you can send to Data Lab as a spatial criterion. Reach for it when you want the outline itself, to draw or to hand to something else. **Python** ```python import numpy PARCEL_TYPE_ID = "YOUR_ENTITY_TYPE_ID" parcels = BM.DataLabBuilder(bruce, page_size=1000).entity_type(PARCEL_TYPE_ID) timestamps = value_map.timestamps() # Listed once. The Bruce select is enough, since only the identity and # where it sits are needed. records, by_texel = [], {} for page in parcels.pages("Bruce"): for item in page.items: texels = value_map.texels_of({"Bruce": item}) if not texels: continue position = len(records) records.append(({"Bruce": item}, texels)) for texel in texels: by_texel.setdefault(texel, []).append(position) print(f"{len(records):,} records on the raster") # Each reading is its own edit, so a record keeps one per frame. changes = BM.ChangeSetBuilder(bruce, allow_multi_same_entity=True) for index in chosen: when = timestamps[index] values, covered = value_map.frame(index) over = value_map.mask_above(THRESHOLD, frame=index) # Walked from the texels over the threshold rather than over every record. reached = set() rows, columns = numpy.nonzero(over) for row, column in zip(rows.tolist(), columns.tolist()): reached.update(by_texel.get((row, column), ())) for position in reached: entity, texels = records[position] level = value_map.peak_at(texels, values=values, covered=covered) if level is None: continue reading = {} BM.Entity.set_value(reading, "flood/time", when) BM.Entity.set_value(reading, "flood/level", level) changes.queue(entity, reading) print(f"frame {index} ({when}): {len(changes):,} readings") print(f"{len(changes.entity_ids):,} Entities reached") # Written as ordinary updates, since a saved Change Set holds one item # per Entity and these are a series per record. report = changes.apply() print(f"{report['succeeded']:,} readings saved") ``` ## Things worth knowing **Nothing states what the numbers mean, so the threshold is yours to name.** The Value Map publishes what its numbers are worth and in what units, not whether a value counts as present. Zero is the line for something measured up from whatever is beneath it, such as a depth, and is merely wherever the datum sits for a height. Leave the threshold out and one is read off the distribution instead, which separates whichever region dominates the range rather than the event you are looking for. **A texel that was never sampled is not a reading of zero.** Every raster carries its own coverage, which is the second value `frame` returns, and a record over unsampled ground has nothing to report. Coverage belongs to the raster you read it from, so never mask one raster's numbers with another's coverage. **The outline can never be finer than a texel.** Areas are traced off the raster, so the shape you query with steps around whole cells. A texel can be hundreds of metres across, which is much larger than a building or a parcel. **Values are quantised over the archive's own range.** Depending on how wide that range is, a value is packed into either one channel or two, so the step is the range divided by 255 or by 65,535. Read the numbers through the library rather than off the pixels: the same picture read as one channel where it holds two is wrong by up to the whole range. **Water that never recedes is still water.** A record standing in a permanent feature reads as covered in every frame, so a count below the length of the series is a record the event arrived at. The reverse is weaker: a full count only says some part of it was covered throughout, which is true both of a record wholly inside the feature and of one straddling its edge. Decide which of the two you are reporting before writing anything, because both are legitimate answers to different questions. **Repeated readings on one Entity need allow_multi_same_entity.** A Change Set built without it merges edits per Entity, so a series of timestamped readings would collapse to the last one. A builder created with it can only be applied, not saved as a Change Set record, because a record holds one item per Entity. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/faqanalyzingvaluemapsinpython # Authentication (/pythonauth) # Authentication Requests are authenticated with a token, which is either a session token from a login or a long-lived access token generated for your account. Both are set the same way and the library sends either as a bearer token, so your code does not need to know which kind it holds. ## Using an access token An access token needs no login. Hand it to the API instances directly. ```python import bruce_models as BM TOKEN = "your-access-token" guardian = BM.GuardianApi({ "session_id": TOKEN }) bruce = BM.BruceApi({ "account_id": "your-account-id", "guardian": guardian, "session_id": TOKEN }) ``` ## Logging in with credentials A login returns a session, whose `ID` is the token to use for subsequent requests. ```python import bruce_models as BM guardian = BM.GuardianApi() session = BM.Session.login( guardian, username="your-username", password="your-password", account_id="your-account-id" ) token = session["ID"] # Apply it to both instances. guardian.set_session_id(token) bruce = BM.BruceApi({ "account_id": "your-account-id", "guardian": guardian, "session_id": token }) ``` When your user or account enforces multi-factor authentication, supply the code alongside the credentials. ```python import bruce_models as BM session = BM.Session.login( guardian, username="your-username", password="your-password", account_id="your-account-id", # Your MFA/TOTP code. code="123456" ) ``` The username can be your user ID, username, or email address. Omitting it entirely means the password is treated as an access token, which is another way to turn a token into a session. ## Ending a session Logging out invalidates the session attached to the instance. An access token is unaffected, since it is not a session. ```python import bruce_models as BM BM.Session.logout(guardian) ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/pythonauth # Entities (/pythonentityrequests) # Entities Once you've configured your API instances, you can start making requests to retrieve and manipulate Entity records. Here are a few examples on how to retrieve one or many Entity records. ```python import bruce_models as BM # Getting an Entity by ID. entity = BM.Entity.get(bruce, "entity-id") # Getting an Entity by ID with a specific scenario. entity = BM.Entity.get(bruce, "entity-id", scenario="scenario-id-or-key") # Getting a list of Entities under a specific Entity Type. entities = BM.Entity.get_list( bruce, entity_type_id="entity-type-id", page_index=0, page_size=50 ).get("Items") # Getting a list of Entities under a specific Entity Type with a specific scenario. entities = BM.Entity.get_list( bruce, entity_type_id="entity-type-id", scenario="scenario-id-or-key", page_index=0, page_size=50 ).get("Items") ``` Here is how you can update or create Entity records. ```python import bruce_models as BM # If the ID inside the Entity record is not supplied (Bruce/ID), then a new record will be created. # New records require an Entity Type ID to be specified (Bruce/EntityType.ID). # Updating an Entity record. updated = BM.Entity.update(bruce, entity) # Updating an Entity with a specific scenario. # Note that a scenario can be specified within the Entity JSON itself. # The parameter takes precedence over the JSON. updated = BM.Entity.update(bruce, entity, scenario="scenario-id-or-key") # Updating a list of Entities. updated = BM.Entity.update_list(bruce, entities).get("Items") # Updating a list of Entities with a specific scenario. # Note that a scenario can be specified within the Entity JSON itself. # The parameter takes precedence over the JSON. updated = BM.Entity.update_list(bruce, entities, scenario="scenario-id-or-key").get("Items") ``` API responses typically include trace, warnings, and errors alongside the returned data. For singular Entity requests, these are available under the `Bruce` property, for lists they are available at the root level as the Entity data is underneath the `Items` property. ```python import bruce_models as BM entity = BM.Entity.get(bruce, "entity-id") # Please note that these properties are only available when there is something to report. # Access them directly when it comes to lists of Entities. Eg: response.get("Error"). # Errors encountered that didn't stop the request. # This usually includes errors related to propagating changes to external data sources. errors = entity.get("Bruce").get("Error") # Warnings encountered that didn't stop the request. # An example is failing to create/update a Historic record because the date couldn't be resolved. warnings = entity.get("Bruce").get("Warning") # Trace information to see how long certain operations took. # If you are experiencing slowness, this can give insight into where the bottleneck is. # Eg: an external source update was slow. trace = entity.get("Bruce").get("Trace") ``` ## Reading and writing attribute values Attribute paths are quoted and slash separated, which is what lets a single segment contain a dot or a space. `BM.PathUtils` converts between that form and a plain list, and the value helpers walk a record for you rather than making you index into nested dictionaries. ```python import bruce_models as BM entity = BM.Entity.get(bruce, "entity-id") # Both forms are accepted. name = BM.Entity.get_value(entity, '"Bruce"/"Name"') name = BM.Entity.get_value(entity, [ "Bruce", "Name" ]) # Convert between the wire form and a list of segments. segments = BM.PathUtils.parse('"location"/"latitude"') # [ "location", "latitude" ] path = BM.PathUtils.wrap([ "location", "latitude" ]) # '"location"/"latitude"' # Writes create any missing intermediate objects. # Note that this changes your local copy only, it does not save the record. BM.Entity.set_value(entity, '"MyAttribute"', "my value") BM.Entity.remove_value(entity, '"MyAttribute"') # Save it when you are done. updated = BM.Entity.update(bruce, entity) ``` A missing attribute reads as `None`. A stored `0`, empty string or `False` is returned as-is, so you can tell an empty value apart from an absent one. ## Where the data came from When an Entity draws attributes from external sources, its internal data carries an `Outline` describing which source contributed what. It arrives on its own for a request by ID, or when an Entity Type is part of the filter, so there is nothing to enable. The baseline entry is where the record itself came from, and the remaining entries list the attribute paths each source supplied. Sources that are not editable should not be offered for editing in your own tooling. ```python import bruce_models as BM entity = BM.Entity.get(bruce, "entity-id") outline = entity.get("Bruce").get("Outline") or [] for source in outline: print(source.get("Source.Name"), source.get("Kind")) # The base record came from here, so no attribute list is given. if source.get("Baseline"): continue # Attribute paths are in the quoted form, so parse before using them. for raw_path in (source.get("Attribute") or []): path = BM.PathUtils.parse(raw_path) value = BM.Entity.get_value(entity, path) print(" ", path, "=", value, "editable:", source.get("Editable")) ``` ## Historic data Entity Types configured with historic data keep previous values against a date/time. Pass a point in time and the nearest matching historic record replaces the Entity contents. If nothing matches, the current record is returned unchanged. A historic read reports itself in the `Outline` described above, as another source among any others. That entry carries the `DateTime` the values came from, and a `ForeignKey` naming the attribute the date/time was read from. ```python import bruce_models as BM # A single Entity as it was at that point in time. entity = BM.Entity.get( bruce, "entity-id", historic_point="2026-01-01T00:00:00Z" ) # The same for a list. entities = BM.Entity.get_list( bruce, entity_type_id="entity-type-id", historic_point="2026-01-01T00:00:00Z" ).get("Items") # A historic source reports itself in the Outline, alongside any other source. # Source.Name is "Historic data", or "Historic Scenario data" under a scenario. for source in (entity.get("Bruce").get("Outline") or []): if not source.get("DateTime"): continue print("Values from", source.get("DateTime")) # ForeignKey is the attribute holding the date/time for this record. print(" keyed on", BM.PathUtils.parse(source.get("ForeignKey"))) for raw_path in (source.get("Attribute") or []): path = BM.PathUtils.parse(raw_path) print(" ", path, "=", BM.Entity.get_value(entity, path)) ``` ## Historic records directly Overlaying a point in time answers "what did this Entity look like then". To work with the records themselves, reading a series or writing one, use `BM.EntityHistoricData`. It covers the whole surface, so there is no need to pick between endpoints. A record is shaped like the Entity it belongs to: your attribute values sit at the root of `Entity`, and platform managed values such as the location sit under its `Bruce` key. That means the same object comes back from a read as goes into a write. ```python import bruce_models as BM ATTR_KEY = ["reading_taken"] # Reading a page of records. Paths can be passed as segments or as a quoted string, # and an attribute nested in a Structure is just more segments. page = BM.EntityHistoricData.get_list( bruce, ["entity-id", "other-entity-id"], attr_key=ATTR_KEY, date_time_from="2026-01-01T00:00:00Z", date_time_to="2026-01-31T23:59:59Z", order_by="DateTime", order_dir="ASC", page_size=1000 ) for record in (page.get("Items") or []): values = record.get("Entity") or {} print(record.get("DateTime"), values.get("temperature")) # Walking every match instead of one page, which pages for you. records = list(BM.EntityHistoricData.iterate( bruce, ["entity-id"], attr_key=ATTR_KEY )) # Records arrive interleaved when more than one Entity was asked for. by_entity = BM.EntityHistoricData.group_by_entity_id(records) for entity_id, entity_records in by_entity.items(): print(entity_id, len(entity_records), "records") ``` Before paging through a long range, ask where records actually exist. `get_analysis` returns counts across the matched range rather than the records themselves. ```python analysis = BM.EntityHistoricData.get_analysis( bruce, ["entity-id"], attr_key=ATTR_KEY ) for bucket in (analysis.get("Items") or []): print(bucket) ``` Writing a record is how you add history the platform did not capture itself, such as a backfill from another system. `build_record` assembles one, wrapping the attribute path and nesting any platform managed values. Note that the date/time attribute named by `attr_key` should also be present in the values, since that is the attribute the record is keyed on. ```python records = [ BM.EntityHistoricData.build_record( "entity-id", ATTR_KEY, "2026-01-01T09:00:00Z", {"reading_taken": "2026-01-01T09:00:00Z", "temperature": 21.4} ), # A record can carry where the Entity was, not only what it held. BM.EntityHistoricData.build_record( "entity-id", ATTR_KEY, "2026-01-02T09:00:00Z", {"reading_taken": "2026-01-02T09:00:00Z", "temperature": 19.1}, internal={"Location": {"latitude": -36.8485, "longitude": 174.7633, "altitude": 0}} ) ] res = BM.EntityHistoricData.update(bruce, records) print("wrote", len(res.get("Items") or []), "records") # A record that could not be written is reported rather than failing the request, # so check this instead of assuming every record landed. for warning in (res.get("Warning") or []): print(warning) ``` There are two ways to delete. Hand back the records you read to remove exactly those, or describe a range with the same filter a read takes. ```python # Naming the records. Only the fields identifying each one are sent, so the # values that came back from the read are dropped rather than shipped again. BM.EntityHistoricData.delete_records(bruce, records) # Or a whole range. Outside a Scenario the attribute and both ends of the range # are required, and this raises before sending anything if one is missing, which # is what stops an accidental unbounded delete. BM.EntityHistoricData.delete( bruce, ["entity-id"], attr_key=ATTR_KEY, date_time_from="2026-01-01T00:00:00Z", date_time_to="2026-01-31T23:59:59Z" ) ``` A record is identified by its Entity, its attribute and its date/time together, which is why a record read back is enough to delete it. Every one of these accepts a `scenario`, so a series can be read, written or cleared inside a Scenario without touching the real world records. A Scenario identifies its own records, so writing into one makes the date/time and attribute optional. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/pythonentityrequests # DataLab queries (/pythondatalabqueries) # DataLab queries `DataLabBuilder` builds the same queries the API reference documents, without hand-writing the request body. Every criterion returns the builder, so they chain, and nothing is requested until you read the selection. Criteria combine with `AND` by default. Pass `logic="OR"` to any of them to change that, and use the grouping helpers below when precedence matters. ## Basic query / Entity Type criteria Most queries start with an Entity Type. Pass `include_children` to take its sub-types too. ```python import bruce_models as BM query = BM.DataLabBuilder(bruce, page_size=1000).entity_type(ENTITY_TYPE_ID) # Including sub-types. query = BM.DataLabBuilder(bruce).entity_type(ENTITY_TYPE_ID, include_children=True) ``` ## Criteria: Attributes Attributes address by slash separated path. The named helpers cover the common operators, and `where` takes any operator the API accepts. ```python query = (BM.DataLabBuilder(bruce) .entity_type(ENTITY_TYPE_ID) .equals("status", "Active") .greater_than("height", 10) .between("installed", "2020-01-01", "2024-12-31") .starts_with("name", "Pump") .is_in("region", ["North", "South"]) .is_not_null("serial")) # Anything the named helpers do not cover. query = query.where("condition", "NOTEQUAL", "Retired") # Comparing two attributes on the same Entity rather than against a value. query = query.where_attribute("actual", "GREATERTHAN", "expected") ``` ## Criteria: Tags ```python # Entities carrying every one of these Tags. query = query.where_tagged([405, 406]) # Or any one of them. query = query.where_tagged([405, 406], mode="any") ``` ## Criteria: User ```python query = query.where_created_by(USER_ID) # By when rather than by whom. UpdatedTime is the other column. query = query.where_created("GREATERTHAN", "2024-01-01") query = query.where_created("BETWEEN", "2024-01-01", "2024-06-30", column="UpdatedTime") ``` ## Criteria: LOD ```python # Entities that have a Level of Detail. query = query.where_lod() # Narrowed to a kind, or to a level. query = query.where_lod(lod_type="MESH") query = query.where_lod(level=2, level_operator="GREATERTHAN") ``` ## Criteria: Assembly ```python # The roots of assemblies. query = query.where_assembly_root() # Everything under a root, or everything above a child. query = query.where_under_root(ROOT_ENTITY_ID) query = query.where_above_child(CHILD_ENTITY_ID) ``` ## Criteria: Relations and attachments ```python # Entities that have any relation, or one of a type. query = query.where_relation() query = query.where_relation(relation_type=RELATION_TYPE_ID) # Related to one specific Entity. query = query.where_related_to(ENTITY_ID, direction="reverse") # Entities carrying an attachment, or a comment. query = query.where_attachment() query = query.where_comment(contains="inspect") ``` ## Criteria: Geometry The spatial criteria take a Bruce geometry, which `GeometryBuilder` constructs. A Value Map also answers with geometries, so an area of a simulation goes straight in here. ```python box = BM.GeometryBuilder.box(174.50, -37.10, 175.05, -36.60) query = query.within(box) query = query.intersecting(shape) query = query.near(point, metres=500) # Entities that do or do not have a location at all. query = query.has_geometry() query = query.missing_geometry() ``` ## Criteria: Reference to another DataLab query ```python # Joins this selection to a saved query on a shared attribute. query = query.where_attribute_join( SAVED_QUERY_ID, input_attribute_path="asset_code", target_attribute_path="code" ) ``` ## Criteria: '()' groups A group is a branch built from the same builder. `group_any` joins the branches with OR, `group_all` with AND, and the group as a whole joins the rest of the query by its own logic. ```python base = BM.DataLabBuilder(bruce).entity_type(ENTITY_TYPE_ID) # status == "Active" AND (region == "North" OR region == "South") query = base.group_any( base.branch().equals("region", "North"), base.branch().equals("region", "South") ).equals("status", "Active") ``` ## Reading the selection Nothing is requested while the query is being built. These are the calls that go to the API, and each pages underneath so a large selection is never held in full. ```python # How many match, without transferring any of them. total = query.count() # A page at a time, with the position carried on the page. def on_page(page): print(f"page {page.number}: {len(page)} records, {page.seen:,} so far") # Returning False stops the walk. walked = query.each_page(on_page) print(walked["pages"], walked["records"], walked["stopped_early"]) # A record at a time, without caring about page boundaries. query.each(lambda entity: print(BM.Entity.get_value(entity, "Bruce/ID"))) # IDs only, which is the cheapest way to stream a large selection. query.each_id(print) # Iterators, for when a loop reads better than a callback. for entity in query.entities(): pass for page in query.pages(): pass # Ordering matters for anything that pages. ordered = query.order_by("name").expand("EntityType,Source") # A Data Schema inferred from the matches, answered in one request. schema = query.schema() ``` ## Things worth knowing **The builder is immutable.** Every criterion returns a new builder rather than changing the one you called it on, so a base query can be branched into several without them affecting each other. **Order before you page.** Paging an unordered selection can repeat or skip records if the underlying data changes while you walk it. `snapshot_ids` takes the whole ID list up front when that matters more than memory. **Ask for a count before a walk.**`count` transfers nothing, so it is the cheap way to decide whether a selection is worth reading at all. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/pythondatalabqueries # DataLab actions (/pythondatalabactions) # DataLab actions An action runs server side against everything a query matches, so nothing is transferred to change it. Build the selection as on the [DataLab queries](/pythondatalabqueries) page, then call one of the `run_action_*` methods on it. Every action answers with a Pending Action rather than a result. `DataLabBuilder.action_id` reads its ID out of the response, and `PendingAction.on_completion` waits for it. ```python import bruce_models as BM query = BM.DataLabBuilder(bruce).entity_type(ENTITY_TYPE_ID).equals("status", "Draft") response = query.run_action_set_attribute("status", "Active") action_id = BM.DataLabBuilder.action_id(response) result = BM.PendingAction.on_completion( bruce, action_id, on_progress=lambda a: print(f"{a.get('Status')} {a.get('Progress')}%") ) action = result.get("action") # A failed job returns rather than raising, so the messages are available. if action.get("Status") != BM.EStatus.COMPLETE: for message in result.get("messages"): print(message.get("Message")) ``` ## Action: Set attribute value ```python query.run_action_set_attribute("status", "Active") # The type the value is written as, when it is not a string. query.run_action_set_attribute("height", 12.5, attribute_type="number") ``` ## Action: Update Tags ```python query.run_action_add_layers([405, 406]) query.run_action_remove_layers([405]) # Replaces whatever the Entities carried. query.run_action_set_layers([405, 406]) ``` ## Action: Create Entity Attachments Attaches one already uploaded Client File to every matching Entity. ```python query.run_action_attach_file( client_file_id=CLIENT_FILE_ID, attachment_type_id=ATTACHMENT_TYPE_ID, group="Inspections" ) ``` ## Action: Export JSON The file lands in the account's temporary store. Its name and url arrive on the finished Pending Action. ```python response = query.run_action_export( "export-json", expand="EntityType,LOD,Source,Relation,Attachment" ) action = BM.PendingAction.on_completion( bruce, BM.DataLabBuilder.action_id(response) ).get("action") print(BM.DataLabBuilder.export_url(action)) print(BM.DataLabBuilder.export_file_name(action)) ``` ## Action: Export CSV The formats are `export-csv`, `export-xlsx`, `export-json` and `export-nsx`. Each takes a different subset of the settings, and one that would be ignored is refused rather than read as a filter that is quietly not applied. ```python query.run_action_export("export-csv", expand="EntityType") # export-nsx takes no expand, and says so rather than dropping it. query.run_action_export("export-nsx") ``` ## Action: Delete Entities ```python # Deletes everything the selection matches, so check count() first. print(f"about to delete {query.count():,} Entities") query.run_action_delete() ``` ## Action: Data Transformation Moves Entities to another Entity Type, either wholesale or split by the value of an attribute. ```python # Everything matched moves to one target Type. query.run_action_transfer_to_type( source_entity_type_id=SOURCE_TYPE_ID, target_entity_type_id=TARGET_TYPE_ID, update_schema=True ) # Or split by an attribute, one target Type per distinct value. query.run_action_transfer_by_attribute( source_entity_type_id=SOURCE_TYPE_ID, attribute_path="category" ) ``` ## Anything else `run_action` sends an action body as given, for anything the named helpers do not cover yet. ```python query.run_action({"ActionType": "some-new-action", "Setting": "value"}) ``` ## Things worth knowing **An action runs against the whole selection.** There is no page size and no partial run, so a mistake in a criterion is a mistake applied to every match. `count` costs nothing and is worth calling first. **A failed job does not raise.**`on_completion` returns on `FAILED` and `CANCELLED` as well as success, because the messages explaining why are what you usually want. Check `Status` to tell them apart. **Changing what you are also reading.** An action that changes an attribute the query filters on changes what the query matches. Read every page first if you need both. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/pythondatalabactions # DataLab secondary queries (/pythondatalabsecondaryqueries) # DataLab secondary queries A secondary query answers "which of these relate to those". Build a selection per Entity Type, pair them with `secondary`, and add a criterion comparing each candidate against the primary it is being tested against. The selection you pass in becomes the secondary, and the secondary is what comes back. Swapping which Type is primary and which is secondary changes the answer, not just the wording. ## Basics Primaries are compared in batches, since the join is done per primary. `batch` sets how many go into each request. ```python import bruce_models as BM buildings = BM.DataLabBuilder(bruce).entity_type(BUILDING_TYPE_ID) sensors = BM.DataLabBuilder(bruce).entity_type(SENSOR_TYPE_ID) # Sensors that sit inside a building. inside = buildings.secondary(sensors, batch=50).within_primary() # Or ones that merely touch or overlap it. touching = buildings.secondary(sensors).intersecting_primary() print(f"{inside.count():,} matches across every primary") print(f"{inside.count_for(BUILDING_ID):,} in one building") ``` ## Reading the matches Read the matches grouped by the primary they belong to, or on their own when it does not matter which primary produced them. ```python def on_group(primary_id, secondaries): print(f"{primary_id}: {len(secondaries)} matches") matched = inside.each_group(on_group) print(matched["primaries"], matched["matches"]) # The same thing as an iterator. for primary_id, secondaries in inside.groups(): pass # Just the matches, without saying which primary they belong to. for entity in inside.entities(): pass ``` A primary that matched nothing is not reported, so the primary count here can be lower than the number of primaries in the selection. An Entity inside two overlapping primaries is answered once per primary, so walking the matches on their own can repeat it. ## Criteria against the primary Spatial criteria are the common case, but any criterion can compare against the primary by using a `Ref` as the value. `Ref("path", on=Ref.PRIMARY)` reads that attribute off the matching primary row, which only means anything in a secondary selection. ```python # Sensors whose owner matches the building's owner. same_owner = (buildings.secondary(sensors) .equals("owner", BM.Ref("owner", on=BM.Ref.PRIMARY))) # Whether the pairing is tied to the primary at all. print(inside.is_related()) ``` ## Action: Set attribute value Runs against every matched secondary. The value can be a `Ref` reading the primary, which is how you copy something across the join without moving the records over the network. ```python # Stamp a constant onto every match. inside.run_action_set_attribute("inside_building", True, attribute_type="boolean") # Or copy the building's name onto each sensor it contains. inside.run_action_set_attribute("building_name", BM.Ref("name", on=BM.Ref.PRIMARY)) ``` ## Action: Save match count Writes how many secondaries each primary matched onto the primary itself. ```python inside.run_action_save_secondary_count("sensor_count", skip_self_match=True) ``` ## Action: Relationships Creates or removes a relationship between each primary and the secondaries it matched, following a Relationship Mapping. ```python inside.run_action_relate(RELATIONSHIP_MAPPING_ID, copy_location=True) inside.run_action_remove_relationships(RELATIONSHIP_MAPPING_ID) ``` ## Things worth knowing **Actions here answer with a Pending Action**, the same as an ordinary DataLab action. Read the ID with `DataLabBuilder.action_id` and wait with `PendingAction.on_completion`. **Batch size is a cost, not a limit.** It sets how many primaries are joined per request, so a large batch means fewer, heavier requests. The default suits most selections. **Reading before writing.** Queue edits during the walk and apply them afterwards through a `ChangeSetBuilder`, so the join cannot shift underneath the paging when the change would stop a record matching. The [overlapping Entities FAQ](/faqoverlappingentitiesinpython) works through a full example. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/pythondatalabsecondaryqueries # Entity Types (/pythonentitytyperequests) # Entity Types Once you've configured your API instances, you can start making requests to retrieve and manipulate Entity Type records. For now, you can only retrieve Entity Type records. ```python import bruce_models as BM # Listing all Entity Types in your account. entity_types = BM.EntityType.get_list(bruce).get("Items") # Getting a specific Entity Type by its ID. entity_type = BM.EntityType.get(bruce, "entity-type-id") ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/pythonentitytyperequests # Files (/pythonfiles) # Files Client Files are the most common kind of file in your Nextspace account. You can upload these yourself and tie them to Entity records as attachments. They are also used for markup media, bookmark thumbnails, and branding. ## Uploading Pass a path and the library handles the rest. Small files go up in a single request, and anything larger is split into parts automatically, so you do not need to know or check the threshold yourself. ```python import bruce_models as BM # Upload a file and get its Client File record back. client_file = BM.ClientFile.upload(bruce, "/path/to/my-file.png") print(client_file.get("ID")) # An optional purpose groups the file. Some Nextspace purposes carry extra meaning. client_file = BM.ClientFile.upload( bruce, "/path/to/my-file.png", purpose="My Purpose" ) ``` Large uploads can take a while, so you can follow their progress. The callback receives a percentage and an `uploaded` flag, which is only true on the final call. ```python import bruce_models as BM def on_progress(progress): print(f"{progress['percent']}% uploaded={progress['uploaded']}") client_file = BM.ClientFile.upload( bruce, "/path/to/large-model.brz", on_progress=on_progress ) ``` ## Downloading Every Client File record carries its own absolute `URL`, with the file extension already applied. Use it directly rather than assembling one from the ID and your base url. ```python import bruce_models as BM import requests client_file = BM.ClientFile.get(bruce, "client-file-id") print(client_file.get("URL")) print(client_file.get("MIMEType")) print(client_file.get("OriginalFileName")) print(client_file.get("OriginalLength")) # The url needs the same authentication as any other request. response = requests.get( client_file.get("URL"), headers={ "Authorization": f"Bearer {your_token}" } ) with open("downloaded.png", "wb") as handle: handle.write(response.content) ``` ## Managing ```python import bruce_models as BM # Change a file's purpose after the fact. BM.ClientFile.update_purpose(bruce, "client-file-id", "My New Purpose") # Delete one or many files. BM.ClientFile.delete(bruce, [ "client-file-id", "another-client-file-id" ]) ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/pythonfiles # Pending Actions (Server Jobs) (/pythonpendingactions) # Pending Actions (Server Jobs) A Pending Action is the record of a background process running on the server. Endpoints that start long-running work, such as imports, exports and Value Map generation, return one of these instead of making you wait, and it is the handle you use to follow that work to completion. See [Pending Actions](/apipendingaction) in the API reference for the underlying endpoints. ## Waiting for completion Rather than polling yourself, hand the ID to `on_completion`. It blocks until the action reaches a terminal state and returns both the final record and every progress message it saw along the way. ```python import bruce_models as BM result = BM.PendingAction.on_completion(bruce, action_id) action = result.get("action") print(action.get("Status")) print(action.get("Result")) for message in result.get("messages"): print(message.get("Message")) ``` A failed or cancelled action returns normally rather than raising, because the messages are usually what you need in order to find out why. Check `Status` to tell the outcomes apart. ```python import bruce_models as BM result = BM.PendingAction.on_completion(bruce, action_id) action = result.get("action") if action.get("Status") == BM.EStatus.COMPLETE: print("Done:", action.get("Result")) elif action.get("Status") == BM.EStatus.FAILED: print("Failed. Messages:") for message in result.get("messages"): print(" ", message.get("Message.Type"), message.get("Message")) ``` You can follow progress as it happens, and give up after a while instead of waiting indefinitely. ```python import bruce_models as BM result = BM.PendingAction.on_completion( bruce, action_id, # Seconds between status checks. Defaults to 3. interval=5, # Raises a TimeoutError once exceeded. Waits indefinitely when omitted. timeout=600, on_progress=lambda a: print(f"{a.get('Progress')}%"), on_message=lambda m: print(m.get("Message")) ) ``` ## Reading records directly The `Result` property is always a dictionary. The API returns it as a string by default, which is usually but not always stringified JSON, so this library requests the parsed form and saves you guessing. A result that was not JSON arrives as `{ "Output": "..." }`. ```python import bruce_models as BM # A single record. action = BM.PendingAction.get(bruce, action_id) print(action.get("Progress"), action.get("Status")) print(action.get("Result")) # Recent records, optionally filtered by status. actions = BM.PendingAction.get_list(bruce, start_index=0, amount=20).get("Items") in_progress = BM.PendingAction.get_list( bruce, status=BM.EStatus.IN_PROGRESS ).get("Items") # Progress messages, newest first. messages = BM.PendingAction.get_messages(bruce, action_id, amount=30) # Ask the server to stop the work. BM.PendingAction.cancel(bruce, action_id) ``` ## Statuses `BM.EStatus` holds the available statuses, and `BM.EStatus.FINISHED` is the set of terminal ones so you do not have to list them yourself. ```python import bruce_models as BM BM.EStatus.IN_PROGRESS BM.EStatus.COMPLETE BM.EStatus.FAILED BM.EStatus.CANCELLED # Every status other than IN_PROGRESS. if action.get("Status") in BM.EStatus.FINISHED: print("Nothing more to wait for.") # Message types, useful for hiding debug output from end users. BM.EMessageType.INFO BM.EMessageType.STATUS BM.EMessageType.WARNING BM.EMessageType.ERROR ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/pythonpendingactions # Getting started (with the CLI) (/cligettingstarted) # Getting started (with the CLI) Installing the [Python library](/pythongettingstarted) also installs a `bruce` command. It is the same library underneath, so anything the CLI does can be scripted against the models, and anything you configure once is picked up by both. It exists for the work that does not deserve a script: checking who a credential is and where it points, looking an Account up, listing managed indexes, following an audit, and running seeded end-to-end tests against an installation. ```bash pip install bruce-models bruce --help bruce --version ``` There is also a [public Docker image](/clidocker), if you would rather not install Python at all. ## Logging in and out `login` stores a default Account and credential so the other commands need no arguments. It accepts either an access token or a username and password. ```bash # Asks for whatever it still needs. bruce login # Or give it up front. bruce login -e UAT -a your-account -t YOUR_ACCESS_TOKEN # Forget the stored credential. bruce logout ``` Every command takes the same overrides, so a stored default can always be stepped around for one call: `-e/--env` (DEV, STG, UAT or PROD), `-a/--account`, `-t/--token`, and `--base-url` / `--guardian-url` for a private installation that is not where the environment implies. ## Using a .env file Settings can come from a key=value file instead of the stored config, which is the usual choice for CI or for a machine that should hold no credential of its own. A `.env` in the working directory is read automatically. ```bash NXT_ENV=UAT NXT_ACCOUNT=your-account NXT_TOKEN=YOUR_ACCESS_TOKEN # For a private installation, where the environment does not imply the host. # NXT_BASE_URL=https://your-account.api.example.com/ # NXT_GUARDIAN_URL=https://login.example.com/ ``` To keep reading from a file somewhere else, name it once at login. Only the path is stored, never the credential inside it. ```bash bruce login --env-file ./deploy/uat.env ``` The layers stack, each one beating the one before it: the stored config file, then a `.env` in the current directory, then a named env file, then the `NXT_*` environment variables, then anything passed on the command line. ## Checking where you are pointed `status` resolves the stored settings and reports who they belong to, which Account they reach, and **the Hosting Locations that credential manages**. It also names where each setting came from, so a surprising value can be traced to the layer that set it. ```bash bruce status # The same thing for a script. bruce status --json ``` A credential that manages no Hosting Locations reports so rather than failing. Hosting management is what lets `bruce test` build a temporary Account to test in, so this is the command to run first if that is your intent. ## Running tests `test` runs seeded end-to-end checks: it creates data, exercises a journey through the API, checks the result and then cleans up after itself. Run it with no target to see what can be tested with your credential. ```bash # What can be tested, and what each test needs. bruce test # Choose interactively, against an Account. bruce test -a your-account # Or name the selection: a number, a list, a range, a tag, or all. bruce test 0 -a your-account bruce test 0,2 -a your-account bruce test 0-2 -a your-account bruce test core -a your-account bruce test all -a your-account ``` The tags are `core`, `import`, `assembly`, `tileset`, `gis`, `historic` and `scale`, and they combine with numbers in the same argument. Testing a Hosting Location instead of an Account builds a temporary Account, runs there, and removes it afterwards. That is the way to check an installation itself rather than the data already in one account. ```bash # Pick from the Hosting Locations you manage. bruce test all -hl # Or name one by Key or numeric ID. bruce test core -hl your-location-key ``` `--rows` changes how much data is generated, and `--keep` leaves the generated Entity Type and file behind when you want to look at what a test built. `--cleanup` is the other direction, removing what a test would otherwise keep. ## Other commands ```bash # An Account's outline, or creating and deleting one. bruce account show your-account bruce account create bruce account delete # The managed attribute indexes on an Account. bruce index list -a your-account bruce index register -a your-account bruce index delete -a your-account # Follow the audit as it happens. bruce audit -a your-account -f ``` Creating and deleting an Account always ask which one and always confirm, whatever default is configured. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/cligettingstarted # Running the CLI in Docker (/clidocker) # Running the CLI in Docker The CLI is published as a public image, so an installation can be checked from any machine with Docker and no Python of its own. It is the same `bruce` command described in [Getting started](/cligettingstarted), as the image's entrypoint. ```bash docker pull nextspace/bruce-cli:latest docker run --rm nextspace/bruce-cli --help ``` `latest` follows the newest release. Every release is also tagged with the library version it contains, which is what to pin in CI so a run is repeatable. ```bash docker run --rm nextspace/bruce-cli:0.1.59 --version ``` ## Giving it credentials The image holds no configuration, so settings come in per run. An env file is the usual way, and it keeps the token off the command line and out of the shell history. ```bash NXT_ENV=UAT NXT_ACCOUNT=your-account NXT_TOKEN=YOUR_ACCESS_TOKEN ``` Pass it with `--env-file`. Note this is Docker's own flag reading the file into the container's environment, which is not the same as the CLI's `--env-file` option, though the file format is the same. ```bash docker run --rm --env-file ./uat.env nextspace/bruce-cli status ``` Individual variables work too, for a CI runner that already holds them as secrets. ```bash docker run --rm \ -e NXT_ENV=UAT \ -e NXT_ACCOUNT=your-account \ -e NXT_TOKEN="$NXT_TOKEN" \ nextspace/bruce-cli status --json ``` ## Running the core tests This is the check worth wiring into a pipeline: run the core suite against a Hosting Location, which builds a temporary Account, exercises the journeys and removes it afterwards. It proves the installation works rather than proving one account's data does. ```bash docker run --rm --env-file ./uat.env \ nextspace/bruce-cli test core -hl your-location-key ``` Against a specific Account instead, which tests in the data that is already there: ```bash docker run --rm --env-file ./uat.env \ nextspace/bruce-cli test core -a your-account ``` `core` is the tag covering the CSV, BRZ, GeoJSON and historic/Scenario journeys. Swap it for `all` to include the assembly alignment and scale tests, or for a number, list or range to run one thing. `bruce test` on its own lists what a credential can run. The command exits non-zero when a test fails, so a pipeline step needs nothing else to gate on it. ## Things worth knowing **The container runs as a non-root user** (UID 10001) with its home at `/home/bruce`. Nothing is written outside it, and `--rm` means a run leaves nothing behind at all. **Nothing is stored between runs.** There is no `bruce login` to do first, because the container is gone afterwards. Mount a config directory at `/home/bruce/.config` if you want a login to persist across runs. **A private installation needs its urls.** The environment name only implies a host for Nextspace-hosted environments. Set `NXT_GUARDIAN_URL`, and `NXT_BASE_URL` if the Account lookup should be skipped, otherwise the lookup goes to the public Guardian and resolves nothing. **Test data is real data.** The tests create Entity Types, files and Entities in whatever they are pointed at, and clean up after themselves. Point them at a Hosting Location or a test Account rather than at production. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/clidocker # Plugins (/webnavigatorplugins) # Plugins Plugins are custom code you can write for the Navigator. They are written for a particular context, for example written for the cursor-bar where you can write your own "click" handling. Plugins are written in Javascript with an optional HTML file as well. You can add, edit, and enable plugins through the Navigator project view dashboard. ## File layout A Plugin is a zipped folder that includes an `index.js` and`index.html` file. The HTML file (if present) will automatically be added to the context's HTML container. It will also be automatically removed when disposed. Here is an example of a Plugin's index.js file. ```javascript // Ensure you have a Run function that takes a params object. function Run(params) { console.log("Plugin was run", params); // HTML container the plugin is sitting in. // This may not exist if it's run as a background script! const container = params.container; // The element of the plugin itself. // This will be sitting inside the container element. // This will not exist if either the container or HTML template is not present. const element = params.element; // The plugin's own ID and record. The record carries Settings, // which is where per-account configuration belongs. const pluginId = params.pluginId; const plugin = params.plugin; // Ensure to return a dispose function. return () => { console.log("Plugin was disposed"); }; } ``` Here is an example of a Plugin's index.html file. Please note the usage of an HTML ID for the styles and root element. It is highly recommended to work with a very unique ID to avoid conflicts with other Plugins and the Navigator itself. ```html
My plugin!
``` ## Plugin Locations A Plugin's location decides where it can be loaded, what container it gets, and which params it is handed. | Location | Description | | | --- | --- | --- | | Cursor bar NAVIGATOR_CURSOR_BAR | The cursor bar is the toolbar found in the top-left corner of the Navigator. It is responsible for actions the user performs when clicking on the scene or Entities in the scene. The Plugin loads when the user enables the tool and is disposed when they disable it, so a cursor Plugin needs no state of its own to know whether it is active. | | | Info View NAVIGATOR_INFO_VIEW | The Info View panel is the dialog that appears when an Entity is selected. This location replaces that panel's contents, so the Plugin is responsible for presenting the Entity itself. | | | Selected Entity panel tab NAVIGATOR_INFO_VIEW_ENTITY | Adds a tab alongside Navigator's own tabs in the selected Entity panel, rather than replacing it. This is usually what you want: the user keeps the built-in views and gains yours. | | | Selected Multi-Entity panel tab NAVIGATOR_INFO_VIEW_MULTI_ENTITY | The same, for when several Entities are selected at once. Read the selection from the visual register rather than expecting a single Entity. | | | Sidebar NAVIGATOR_SIDE_BAR | The side bar is the toolbar found in the left side of Navigator which holds Menu Items, Bookmarks, and Scene Tree. Set the Plugin setting "panelWidth" to control the panel's width. See Side panel width below. | | | Background NAVIGATOR_BACKGROUND | Plugins that are set to load in the background will load automatically when the page loads without the user needing to click any buttons. A background Plugin is reloaded when the Project View or the active Bookmark changes, and is the only location handed the view and bookmark params. | | | AI Tool AI_TOOL | Not a piece of UI. The Plugin is offered to the AI agent as a callable tool and exports Invoke rather than Run. See the AI Tools page. | | | Operator OPERATOR | Loads inside the Operator management portal rather than Navigator. The params described below are Navigator's, so do not assume a viewer here. | | Setting a location is not enough on its own. A Plugin must also be**enabled on the Project View** that is being viewed, which is what Navigator checks before loading anything. A Plugin that appears to do nothing is almost always a Plugin that was never enabled on the view. ## Plugin Params Below are what parameters are passed into the Plugin's Run function in Navigator. ```typescript function Run(params) { console.log("Plugin was run", params); const pluginParams = params.pluginParams; // Plugin record itself. // Useful for accessing the settings object within it. const plugin = params.plugin; // Reference to the Cesium viewer. const viewer = pluginParams.viewer; // Reference to the Menu Item manager and visual register. // This lets you manage Menu Items and know what is currently rendered in the scene. const manager = pluginParams.menuItemManager; const register = pluginParams.visualRegister; // Reference to Cesium, bruce-models, and bruce-cesium NPM libraries. const Cesium = pluginParams.Cesium; const BModels = pluginParams.BModels; const BEngine = pluginParams.BEngine; // The current context, without needing a request for any of it. // 'getters' is already pointed at this account and session. const getters = pluginParams.getters; const session = pluginParams.session; const account = pluginParams.account; const version = pluginParams.version; // Reference to function to call so you can force the plugin to dispose and the tool to be disabled. const close = pluginParams.close; // close(); // Reference to function to call to open/close a Navigator side-panel. // Options are: "dashboard", "menu-items", "bookmarks", "scene-tree", "user", "scene-config", "entity-editor" // "comments", "markup", and "walkthrough". // If you have an active side-bar plugin, you can open it through "PLUGIN_" eg: "PLUGIN_1234abcd". const setPanelActive = pluginParams.setPanelActive; // Open a panel: // setPanelActive("bookmarks", true); // Close a panel: // setPanelActive("bookmarks", false); // Reference to function to call to select or deselect Entities. const select = pluginParams.select; /* * select({ * entityIds: ["a", "b"] * }) */ // Reference to function to call to refresh Entities in the scene/panels. const refreshData = pluginParams.refreshData; /* * refreshData({ * entityIds: ["a", "b"] * }); */ // Reference to function to call to toggle "liveness" of Entities. // When an Entity is live, it periodically refreshes its data. const updateLiveData = pluginParams.updateLiveData; /* * Replace current live Entity IDs with given IDs. * updateLiveData({ * entityIds: ["a", "b"] * }); */ /* * Add given Entity IDs to the current live Entities. * updateLiveData({ * entityIds: ["a", "b"], * action: true * }); */ /* * Remove given Entity IDs to the current live Entities. * updateLiveData({ * entityIds: ["a", "b"], * action: false * }); */ return () => { console.log("Plugin was disposed"); }; } ``` ## Params by location Not every location is handed every param. The params above are present everywhere in Navigator; the ones below depend on where the Plugin runs, so check for a param rather than assuming it. Params marked unstable are the ones Navigator happens to expose today and may change. | Param | Where | What it is | | --- | --- | --- | | setPanelWidth | Side bar | Changes the panel width at runtime, taking the same value as the panelWidth setting. Affects the current session only. | | setFocusedAttachment | Info View locations | Opens Navigator's own attachment viewer on an attachment, with its siblings, so a Plugin does not need to build media browsing itself. Called as setFocusedAttachment(attachment, siblings). | | view, viewId | Background | The active Project View record and its ID. A background Plugin is reloaded when this changes. | | bookmark, bookmarkId, enableBookmark | Background | The active Bookmark, its ID, and a call to activate one. A background Plugin is also reloaded when the Bookmark changes. | | getCursor unstable | Background | Returns the cursor tool the user currently has active, so a background Plugin can react to it. | ## The Plugin record Every Plugin is handed its own record as `params.plugin`. It is worth knowing what is on it, because several fields exist specifically so a Plugin does not have to be edited per account or per deployment. | Field | What it does | | --- | --- | | Settings | Arbitrary JSON, passed to the Plugin when it loads. This is where an Entity Type ID, an endpoint, a threshold or a colour belongs, so one Plugin serves every account with no code change. Also carries the reserved panelWidth, and tool for AI Tools. | | Enabled | Turns a Plugin off without unpicking it from every Project View that references it. | | Group | An arbitrary grouping you can query against, so a whole related set can be retrieved and enabled at once. This is how an embedded Navigator can be told to enable a group of Plugins by URL param, and have that set change without the URL changing. | | Version | Bumped every time a new code bundle is uploaded, and used as the cache key for the Plugin's code. See Caching while you develop below. | | Icon, IconSource | The button icon, either a Font Awesome class (FONT_AWESOME) or a Client File ID (CLIENT_FILE) for your own image. For background Plugins the icon is only for identification in admin panels. | | IsLoginRequired | Requires a logged-in user. | | IsAccessRestricted | Restricts the Plugin to users holding the UIPlugin_ permission. Only checked alongside IsLoginRequired. | ## Side panel width A side bar Plugin's panel is 300px wide by default. The `panelWidth` setting takes either a fixed CSS width or a range, and a range is what makes the panel user-resizable. ```json { "panelWidth": "500px" } { "panelWidth": "300px-900px" } { "panelWidth": "300px-900px-480px" } ``` A single value is a fixed width. Two values are a minimum and a maximum, which the user can drag between. Three values add the width to open at. A Plugin can also change this while it runs through the `setPanelWidth` param, which affects the current session only and does not rewrite the setting. ## Caching while you develop A Plugin's code file is cached against its `Version`, which is bumped on upload. That is right for users and awkward while iterating, because a browser that already holds a version will keep serving it. Navigator has a user flag for exactly this. Turn on**Disable plugin cache** on your own user while developing, and every load refetches the code. Leave it off in normal use: it costs a request per Plugin per load. ## Worked examples The FAQ pages in this section answer the cases most Plugins start from: making API requests, letting the user pick a position in the scene, and rendering directly to the Cesium viewer. The AI Tools page covers the agent-callable location, which uses a different entry point entirely. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webnavigatorplugins # Plugins - Developer environment (/webnavigatorpluginsdevenv) # Plugins - Developer environment We're working to make plugin development easier as it's a tedious process to write, zip, and upload a plugin every time you want to test it. This first iteration will include an example plugin that comes with a deployment script, and some typing that will help guide your IDE with the plugin's context. [Download starter kit](/res/samples/plugin-dev-env.zip) The setup and usage of the starter kit is detailed in the README file that comes with the download. However, here are some screenshots of the expected results. ![](/res/plugins/dev-env-config.png) Above is the expected config you will need to know. See above how my IDE is suggesting the plugin's context. I can 'ctrl + left-click' to jump to the definition of the plugin's context. A quick command to deploy changes. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webnavigatorpluginsdevenv # How do I make API requests from a Plugin? (/faqapirequestsinplugins) # How do I make API requests from a Plugin? A Plugin never installs or imports anything. Navigator hands it the already-authenticated libraries through `pluginParams`, so a request from inside a Plugin is the same call you would write in a web app, minus the setup. Two params matter here. `BModels` is the whole `bruce-models` namespace, which is where the per-record utilities live. `getters` is an `ApiGetters` instance already pointed at the current account, environment and session, and it is how you reach a raw API instance for anything the library has no utility for. ## Using the library namespaces Prefer a namespace over a hand-rolled request. Every namespace resolves the API instance itself, caches consistently with the rest of Navigator, and returns typed records, so a Plugin reading an Entity sees exactly what the Info View sees. Note that every namespace function takes an optional `api`. Omitting it is correct inside Navigator: the library falls back to the same instance the app is already using. Pass one only when you deliberately want a different account or environment. **Entity** ```javascript async function Run(params) { const { BModels, visualRegister } = params.pluginParams; const { Entity, EntityType } = BModels; const selectedIds = visualRegister.GetSelected(); if (!selectedIds.length) { return () => {}; } // One Entity, with its Type expanded in the same request. const { entity } = await Entity.Get({ entityId: selectedIds[0], expandEntityType: true, expandAttachments: true }); // Bruce holds the system attributes, Data holds the schema attributes. console.log("ID", entity.Bruce.ID); console.log("Type", entity.Bruce["EntityType.ID"]); // Reads a nested attribute without hand-walking the object. const status = Entity.GetValue({ entity: entity, path: "Data.Condition.Status" }); console.log("Status", status); // The display name Navigator itself would show. const { entityType } = await EntityType.Get({ entityTypeId: entity.Bruce["EntityType.ID"] }); console.log("Name", Entity.CalculateName({ entity: entity, type: entityType, defaultToId: true })); // Writing back. Only the attributes you supply are changed // because override is false. Entity.SetValue({ entity: entity, path: "Data.Condition.Status", value: "INSPECTED" }); await Entity.Update({ entity: entity, override: false }); // Tells Navigator the record moved, so the scene and panels reread it. params.pluginParams.refreshData({ entityIds: [entity.Bruce.ID] }); return () => {}; } ``` **Entity list** ```javascript async function Run(params) { const { Entity } = params.pluginParams.BModels; const ENTITY_TYPE_ID = params.plugin.Settings?.entityTypeId; // A count first, so a Plugin can refuse a selection that is too // large before it transfers any of it. const { totalCount } = await Entity.GetList({ analysis: true, filter: { entityTypeId: ENTITY_TYPE_ID } }); console.log(totalCount + " Entities of this Type"); // Paged read. getNextPage is handed back on each response, so // you never construct the next request yourself. let page = await Entity.GetList({ filter: { entityTypeId: ENTITY_TYPE_ID, pageSize: 500, pageIndex: 0, // Attribute conditions. Index the attribute for this to be fast. entityTypeConditions: { "Data.Condition.Status": "FAULTY" } } }); const all = []; while (page) { all.push(...(page.entities ? page.entities : [])); page = page.nextPage ? await page.getNextPage() : null; } console.log("Read " + all.length + " Entities"); // Restricting to the ground area the camera currently covers. const area = await params.pluginParams.BEngine.ViewGroundArea.GetViewArea( params.pluginParams.viewer ); const visible = await Entity.GetList({ filter: { entityTypeId: ENTITY_TYPE_ID, bounds: area.bounds } }); console.log("Visible", visible.entities?.length); return () => {}; } ``` **Entity Type** ```javascript async function Run(params) { const { EntityType } = params.pluginParams.BModels; // Every Type in the account. Settings, which carry the data schema, // are stripped by default to keep the payload small, so ask for them // only when you intend to read the schema. const { entityTypes } = await EntityType.GetList({ expandSettings: true }); entityTypes.forEach((type) => { console.log(type.ID, type.Name); }); // Types under one parent, which is how you walk an ontology branch. const { entityTypes: children } = await EntityType.GetList({ parentTypeId: entityTypes[0].ID }); console.log(children.length + " child Types"); // A single Type, and how many Entities it holds. const { entityType } = await EntityType.Get({ entityTypeId: entityTypes[0].ID }); const count = await EntityType.Count({ entityTypeId: entityType.ID }); console.log(entityType.Name + " holds " + count); return () => {}; } ``` ## Custom requests Not every endpoint has a namespace utility, and a new endpoint always lands in the API before it lands in the library. For those, take the API instance directly and call the verb yourself. This is the same instance every namespace uses underneath, so the account header, session header, base URL and cache are already correct. `getters.GetBruceApi()` returns the Bruce API for the current account. Paths are appended to the API's base URL and take no leading slash. The base URL already carries the version root, so most paths are bare, for example `entitytypes` or `entity/{id}`. Some newer endpoints are addressed under an explicit `v3/` prefix; the Swagger reference is the place to confirm which. **Raw requests** ```javascript async function Run(params) { const { getters } = params.pluginParams; // The Bruce API for the account Navigator is currently viewing. const api = getters.GetBruceApi(); // GET. The second argument is the request params object, where // urlParams becomes the query string. const types = await api.GET("entitytypes", { urlParams: { "expandSettings": "yes" } }); console.log(types); // A path that has no namespace utility yet. const links = await api.GET("entity/SOME_ENTITY_ID/links"); console.log(links); // POST with a JSON body. await api.POST("entity/SOME_ENTITY_ID", { Data: { Note: "Written by a Plugin" } }); // PUT and DELETE take the same shape. // await api.PUT("some.endpoint", body); // await api.DELETE("some.endpoint"); return () => {}; } ``` **Non-library requests** ```javascript async function Run(params) { const { getters } = params.pluginParams; const api = getters.GetBruceApi(); await api.Loading; // Where you need fetch itself, for a streamed response, a binary // body, or an endpoint that does not answer JSON, build the URL and // the session header off the API instead of assembling either // by hand. ConstructUrl carries the account, which the base URL // may hold as a query param rather than a path. const url = api.ConstructUrl({ url: "entities/ExportCSV", urlParams: { "entityTypeId": "SOME_ENTITY_TYPE_ID" } }); const headers = {}; headers[api.GetSessionHeader()] = api.GetSessionId(); const res = await fetch(url, { headers: headers }); const csv = await res.text(); console.log(csv.length + " bytes of CSV"); return () => {}; } ``` **Other APIs** ```javascript async function Run(params) { const { getters, session, account } = params.pluginParams; // Guardian, for users, sessions and permissions. const guardian = getters.GetGuardianApi(); // Bruce Global, for cross-account surfaces. const global = getters.GetGlobalApi(); // Another account, where a Plugin deliberately reads across one. // The session must have access to it. const other = getters.GetBruceApi({ accountId: "SOME_OTHER_ACCOUNT_ID" }); // The current context, without a request. console.log("Account", account?.Name); console.log("Session user", session?.["User.ID"]); console.log("Account ID", getters.GetAccountId()); return () => {}; } ``` ## Caching Namespace reads are cached, which is what keeps Navigator responsive when several surfaces ask for the same record. A Plugin that has just written a record, or that is polling something that changes underneath it, needs to say so. ```javascript const { BModels, getters } = params.pluginParams; // Bypasses the cache for this one read. const { entity } = await BModels.Entity.Get({ entityId: "SOME_ENTITY_ID", req: { noCache: true } }); // Drops a record from the cache, so every later reader refetches it. const api = getters.GetBruceApi(); api.Cache.Remove(BModels.Entity.GetCacheKey({ entityId: "SOME_ENTITY_ID" })); ``` Namespaces that write, such as `Entity.Update`, already clear what they invalidate. Clear the cache yourself only after a raw `POST` that the library does not know changed anything. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/faqapirequestsinplugins # How do I let the user pick a position in the scene? (/faqscenepickerinplugins) # How do I let the user pick a position in the scene? This is the most common shape a Plugin takes: the user clicks something in the 3D scene, a marker appears where they clicked, and the Plugin does something with that position. Placing an asset, dropping a note, starting a measurement and reading a coordinate are all the same three steps. A Cursor bar Plugin is the right location for it. Enabling the tool starts the pick, and the Plugin's dispose function ends it, so the user turning the tool off cleans up without the Plugin tracking any state of its own. ## Resolving a screen position to a world position Cesium offers several ways to turn a mouse position into a world position and they do not agree: picking the globe ignores tilesets, picking the ellipsoid ignores terrain, and picking a primitive misses ground. `bruce-cesium` resolves all of that in one place, as `DrawingUtils.GetAccuratePosition`. ```javascript const { BEngine } = params.pluginParams; // pos2d is a Cesium.Cartesian2 in canvas pixels. // Returns a Cesium.Cartesian3 in absolute coords or null when nothing was found. const pos3d = BEngine.DrawingUtils.GetAccuratePosition(viewer, pos2d); // The third argument restricts the resolve to picked geometry only, // so a click that misses a model or tileset returns nothing rather. const onGeometryOnly = BEngine.DrawingUtils.GetAccuratePosition(viewer, pos2d, true); ``` ## A complete picker The pattern below tracks the cursor while the tool is enabled, places a marker on click, and disposes everything it created. **index.js** ```javascript /** * Cursor bar Plugin. * Click in the scene to place a marker and read the position back. */ function Run(params) { const { viewer, Cesium, BEngine, close } = params.pluginParams; const { Cartes } = params.pluginParams.BModels; const { GetAccuratePosition } = BEngine.DrawingUtils; // Everything the Plugin adds to the scene, so dispose can be // one loop rather than a list of special cases. const created = []; // Last valid world position under the cursor. let hovered = null; /** * Adds a point at a world position and returns its Cesium entity. */ function placeMarker(pos3d, label) { const marker = viewer.entities.add({ position: pos3d, point: { pixelSize: 12, color: Cesium.Color.fromCssColorString("#33B1FF"), outlineColor: Cesium.Color.WHITE, outlineWidth: 2, // Keeps the marker visible through geometry in front of it. disableDepthTestDistance: Number.POSITIVE_INFINITY }, label: { text: label, font: "14px sans-serif", fillColor: Cesium.Color.WHITE, showBackground: true, pixelOffset: new Cesium.Cartesian2(0, -24), disableDepthTestDistance: Number.POSITIVE_INFINITY } }); created.push(marker); viewer.scene.requestRender(); return marker; } const handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas); // Track the cursor. handler.setInputAction((e) => { if (!Cartes.ValidateCartes2(e.endPosition)) { return; } const pos3d = GetAccuratePosition(viewer, e.endPosition); if (Cartes.ValidateCartes3(pos3d)) { hovered = pos3d; } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); // Place on click. handler.setInputAction(async (e) => { if (!Cartes.ValidateCartes2(e.position)) { return; } const pos3d = GetAccuratePosition(viewer, e.position); if (!Cartes.ValidateCartes3(pos3d)) { return; } // Degrees and metres, which is what an API call wants. const carto = Cesium.Cartographic.fromCartesian(pos3d); const lon = Cesium.Math.toDegrees(carto.longitude); const lat = Cesium.Math.toDegrees(carto.latitude); placeMarker(pos3d, lat.toFixed(6) + ", " + lon.toFixed(6)); await onPicked({ lat, lon, alt: carto.height, pos3d }); }, Cesium.ScreenSpaceEventType.LEFT_CLICK); // Right click ends the tool. close() disables the cursor. handler.setInputAction(() => { close(); }, Cesium.ScreenSpaceEventType.RIGHT_CLICK); /** * Whatever the Plugin exists to do with a picked position. */ async function onPicked(picked) { console.log("Picked", picked); } return () => { handler.destroy(); created.forEach((entity) => viewer.entities.remove(entity)); created.length = 0; viewer.scene.requestRender(); }; } ``` **Clamping to the ground** ```javascript async function groundedPosition(params, pos3d) { const { viewer, Cesium, BEngine } = params.pluginParams; const carto = Cesium.Cartographic.fromCartesian(pos3d); const terrain = await BEngine.DrawingUtils.GetTerrainHeight({ pos3d: pos3d, viewer: viewer }); // The sample can fail, in which case keep the picked height // rather than dropping the marker to the ellipsoid. if (terrain.error) { return pos3d; } return Cesium.Cartesian3.fromRadians( carto.longitude, carto.latitude, terrain.height ); } ``` **Picking an Entity instead** ```javascript function Run(params) { const { viewer, Cesium, visualRegister, select } = params.pluginParams; const handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas); handler.setInputAction((e) => { const { regos } = visualRegister.GetRegosFromCursor({ cursor: e.position }); if (!regos.length) { return; } const rego = regos[0]; console.log(rego.entityId, rego.entityTypeId, rego.name); // Drive Navigator's selection rather than tracking your own. select({ entityIds: [rego.entityId] }); }, Cesium.ScreenSpaceEventType.LEFT_CLICK); // The current selection at any time, and a subscription to it. console.log(visualRegister.GetSelected()); const unsubscribe = visualRegister.OnUpdate.Subscribe((data) => { console.log("Selection changed", data); }); return () => { handler.destroy(); unsubscribe?.(); }; } ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/faqscenepickerinplugins # How do I render directly to the Cesium viewer from a Plugin? (/faqcesiumrenderinginplugins) # How do I render directly to the Cesium viewer from a Plugin? Not every graphic is an Entity. A coverage overlay, a site boundary, a route line, a heads-up label, a debug grid: these are decoration, they are not records, and they should not be selectable, styled, isolated or hidden by Navigator's own controls. For those, draw straight onto the Cesium viewer. A Plugin is handed the live `viewer` and the whole `Cesium`namespace, so anything the CesiumJS documentation describes is available. What follows is the part that is specific to Navigator. ## Three rules **Request a render.** Navigator runs Cesium on demand rather than at a fixed frame rate, so the scene is only redrawn when something asks for it. Call: `viewer.scene.requestRender()`to refresh the Cesium view. **Dispose everything.** A Plugin's dispose function is the only cleanup that runs. Anything added to `viewer.entities`,`viewer.scene.primitives` or`viewer.dataSources` outlives the Plugin otherwise, and the user has no way to remove it short of reloading. **Stay out of the register.** Do not add decoration through the Menu Item manager or the visual register. Those exist to map graphics back to Entity records, and a graphic registered there becomes selectable. ## Choosing where to add it Cesium offers three surfaces and the choice matters more for a Plugin than for an app, because a Plugin shares the scene with everything Navigator has already drawn. | Surface | When to use it | | --- | --- | | viewer.entities | A handful of graphics, described declaratively. Points, labels, billboards, polylines, polygons, models. The easiest surface, and the one to reach for first. These are Cesium entities and have nothing to do with Nextspace Entities. | | viewer.dataSources | A named group you want to show, hide or remove as one, or content loaded from GeoJSON, KML or CZML. Add a CustomDataSource and put your entities in it, then removing the data source removes the lot. | | viewer.scene.primitives | Volume, or a custom appearance or shader. Thousands of markers through a billboard or label collection, a ground overlay, geometry you have batched yourself. More work per graphic and much cheaper per graphic. | **A data source group** ```javascript /** * Background Plugin that draws a boundary and a set of labels as one * removable group. */ async function Run(params) { const { viewer, Cesium } = params.pluginParams; const source = new Cesium.CustomDataSource("MyPluginOverlay"); await viewer.dataSources.add(source); const ring = params.plugin.Settings?.boundary; source.entities.add({ polygon: { hierarchy: new Cesium.PolygonHierarchy( Cesium.Cartesian3.fromDegreesArray(ring) ), material: Cesium.Color.fromCssColorString("#33B1FF").withAlpha(0.25), // Drapes over terrain and tilesets rather than cutting through them. classificationType: Cesium.ClassificationType.BOTH }, polyline: { positions: Cesium.Cartesian3.fromDegreesArray(ring), width: 3, material: Cesium.Color.fromCssColorString("#33B1FF"), clampToGround: true } }); viewer.scene.requestRender(); return () => { viewer.dataSources.remove(source, true); viewer.scene.requestRender(); }; } ``` **Many markers, cheaply** ```javascript async function Run(params) { const { viewer, Cesium, BModels } = params.pluginParams; const labels = viewer.scene.primitives.add(new Cesium.LabelCollection()); const points = viewer.scene.primitives.add(new Cesium.PointPrimitiveCollection()); const { entities } = await BModels.Entity.GetList({ filter: { entityTypeId: params.plugin.Settings?.entityTypeId, pageSize: 5000 } }); entities.forEach((entity) => { const loc = entity.Bruce?.Location; if (!loc) { return; } const pos = Cesium.Cartesian3.fromDegrees( loc.longitude, loc.latitude, loc.altitude ); points.add({ position: pos, pixelSize: 8, color: Cesium.Color.ORANGE }); labels.add({ position: pos, text: String(entity.Bruce?.Name ? entity.Bruce.Name : ""), font: "12px sans-serif", distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 400) }); }); viewer.scene.requestRender(); return () => { viewer.scene.primitives.remove(labels); viewer.scene.primitives.remove(points); viewer.scene.requestRender(); }; } ``` **Redrawing on camera move** ```javascript function Run(params) { const { viewer, Cesium, menuItemManager } = params.pluginParams; const group = new Cesium.PrimitiveCollection(); viewer.scene.primitives.add(group); /** * Rebuilds the overlay for the current camera. */ function redraw() { group.removeAll(); const height = viewer.camera.positionCartographic.height; console.log("Rebuilding overlay at " + Math.round(height) + "m"); // Build primitives into 'group' here. viewer.scene.requestRender(); } const unsubscribe = menuItemManager.Monitor.Updated().Subscribe(() => { redraw(); }); redraw(); return () => { unsubscribe?.(); viewer.scene.primitives.remove(group); viewer.scene.requestRender(); }; } ``` **A value that changes** ```javascript function Run(params) { const { viewer, Cesium } = params.pluginParams; let position = Cesium.Cartesian3.fromDegrees(174.76, -36.85, 20); const marker = viewer.entities.add({ // The second argument declares the value is not constant. position: new Cesium.CallbackProperty(() => position, false), point: { pixelSize: 14, color: Cesium.Color.CYAN, // Draws over geometry in front of it, so a marker is // never lost inside a model. disableDepthTestDistance: Number.POSITIVE_INFINITY } }); // Moving it is now one assignment plus one render request. const timer = setInterval(() => { const carto = Cesium.Cartographic.fromCartesian(position); position = Cesium.Cartesian3.fromRadians( carto.longitude + Cesium.Math.toRadians(0.0001), carto.latitude, carto.height ); viewer.scene.requestRender(); }, 100); return () => { clearInterval(timer); viewer.entities.remove(marker); viewer.scene.requestRender(); }; } ``` ## Things that will bite **Do not destroy the viewer, or replace its terrain provider, imagery layers, camera controller or clock.** Navigator owns those, and a Plugin changing one changes the whole scene for every other surface. Where a Plugin genuinely needs a global change, restore the previous value in its dispose function. **Check the viewer is alive before touching it.** A Plugin can be disposed while an asynchronous read is in flight, and the Project View can be torn down under it. Guard the resume path. ```javascript // Both checks, because a destroyed viewer throws on nearly every property. if (!viewer || viewer.isDestroyed()) { return; } ``` **Prefer clamping to guessing a height.**`clampToGround` for lines and `classificationType` for areas let Cesium drape the graphic over whatever is actually there. Computing a height yourself puts the graphic underground as soon as the scene has terrain or a tileset the Plugin did not expect. **Give collections a stable identity.** Where a Plugin can be loaded more than once, in two Project Views or as both a cursor tool and a background script, name its data source and check for an existing one before adding a second. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/faqcesiumrenderinginplugins # Plugins - AI Tools (/webnavigatorpluginsaitools) # Plugins - AI Tools An AI Tool Plugin is a Plugin the AI agent can call. Set a Plugin's run location to**AI Tool** and it stops being a piece of UI: it has no container, no HTML, and no button. Instead it is offered to the agent alongside Navigator's built-in tools, and the agent decides when to call it and with what arguments. This is the way to teach the agent something only your account knows: a lookup against a site system, a calculation over your own attributes, a rule about your own data. You write the function, and the agent works out when it is the right one to reach for. ## Invoke, not Run A UI Plugin exports `Run(params)` and returns a dispose function. An AI Tool exports `Invoke(args, context)` and returns a result. There is nothing to dispose, because there is nothing on screen. ```javascript /** * Called by the agent. * args is model-generated and matches Settings.tool.inputSchema. * The return value is serialized to JSON and handed back to the agent, so return data rather than a sentence. */ async function Invoke(args, context) { // Per-account values, from the Plugin's Settings.config. const config = context.config; // The Project View that was active when the agent called. const viewId = context.viewId; // Aborts when the declared timeout elapses. const signal = context.signal; return { answer: 42 }; } ``` Both forms may live in the same file. Only the one the location calls for is used, so a Plugin can present a panel and expose the same capability to the agent. ## Declaring the tool The agent cannot see your code, only what the Plugin declares. That declaration lives in the Plugin's `Settings` under a`tool` key, and Operator refuses to save an AI Tool without it. **Settings** ```json { "tool": { "description": "Returns the maintenance backlog for a building, newest first. Use when asked what work is outstanding on a building.", "inputSchema": { "type": "object", "properties": { "buildingId": { "type": "string", "description": "Entity ID of the building to report on." }, "limit": { "type": "integer", "description": "Maximum records to return. Defaults to 20." } }, "required": ["buildingId"], "additionalProperties": false }, "annotations": { "sideEffect": "none", "timeoutMs": 20000 } }, "config": { "entityTypeId": "maintenance_task" } } ``` **index.js** ```javascript /** * Returns the outstanding maintenance for one building. */ async function Invoke(args, context) { // Arguments come from the model and are not trusted. // Validate before use, exactly as you would with user input. const buildingId = typeof args?.buildingId === "string" ? args.buildingId.trim() : ""; if (!buildingId) { throw new Error("buildingId is required."); } const limit = Number.isInteger(args?.limit) ? Math.min(Math.max(args.limit, 1), 100) : 20; // An AI Tool is handed no libraries, so a request is a plain // fetch. Everything it needs to build one, the base URL, the // Entity Type and the token, comes from config rather than // from the code. const { baseUrl, sessionHeader, token, entityTypeId } = context.config; const url = baseUrl + "entities?entityTypeId=" + encodeURIComponent(entityTypeId) + "&pageSize=" + limit; const headers = {}; headers[sessionHeader] = token; const res = await fetch(url, { headers: headers, // Threading the signal is what makes the timeout stop work. signal: context.signal }); if (!res.ok) { throw new Error("Lookup failed with status " + res.status); } const data = await res.json(); // Return the smallest shape that answers the question. Everything // returned is spent as agent context, and an oversized result is // refused outright. const items = data.Items ? data.Items : []; return { buildingId: buildingId, count: items.length, tasks: items.map((entity) => ({ id: entity.Bruce.ID, name: entity.Bruce.Name, priority: entity.Data?.Priority })) }; } ``` Note what an AI Tool is not given. There is no `pluginParams`, so no viewer, no visual register, and no `BModels` or `BEngine`: those are passed to a UI Plugin by its host, and an AI Tool has no host. An AI Tool sees its arguments, its context, and the browser's own APIs. Anything it needs to reach an API with, a base URL, a header name and a token, belongs in `Settings.config`, which is also what keeps one Plugin working across accounts without a code change. ## Rules the platform enforces These are refused at save time rather than at call time, so a mistake here shows up in Operator, not in a conversation with the agent. | Rule | Why | | --- | --- | | The Plugin ID becomes the tool name | The agent calls it as plugin_. The ID may hold letters, numbers, hyphens and underscores only. | | A description is required | The tool name is an opaque ID and carries no meaning, so the description is the only thing the agent can select on. Without one the tool is registered and never chosen. Set tool.description, or fill in the Plugin's Name and Description. Say what it returns and when to use it. | | inputSchema must be an inline object schema | Its type must be object, and $ref, $defs, allOf and not are refused: each silently drops the tool out of strict mode, so the arguments stop being structurally guaranteed and nothing reports that it happened. Inline the schema instead. | | annotations.sideEffect | Either none, or external for a reviewed read-only external request. AI Tools are read-only in this release, so a tool that writes is refused. | | annotations.timeoutMs | A positive number of milliseconds. Defaults to 15000 and is clamped to 120000, so no tool can pin a call open. The whole operation is inside that budget, source fetch included. | | The result must be JSON and under 100,000 characters | A result that cannot be serialized, or that is larger than the cap, is reported to the agent as a failure rather than truncated. Return a summary and let the agent ask again for detail. | ## What to expect at call time The Plugin's code runs in the user's own browser with the user's own permissions, and the Plugin record is re-read server side with that user's session immediately before the call is forwarded. A Plugin the user cannot see is a Plugin the agent cannot call on their behalf, regardless of what any browser has cached. Timeouts free the caller, not the Plugin. Honouring `context.signal` is up to you, and aborting a signal cannot stop JavaScript that is already blocking the browser thread: a Plugin that blocks stalls Navigator itself. Thread the signal into every request you make. ```javascript async function Invoke(args, context) { // Passing the signal through is what makes a timeout actually stop work. const res = await fetch(context.config?.endpoint, { signal: context.signal }); // A long loop should check it too, since nothing will interrupt one. for (const item of bigList) { if (context.signal?.aborted) { throw new Error("Aborted."); } } return await res.json(); } ``` Throwing is the correct way to report a problem. The failure is turned into a standard envelope carrying a reason the agent can act on, so an `Error`whose message says what was wrong with the arguments lets the agent correct itself and call again. ## Before enabling one in production An AI Tool is the one Plugin location where nobody clicks a button to run the code. Review it as you would review anything triggered automatically: - Validate every argument. They are model-generated, they are not trusted, and a Plugin is the last thing standing between them and your API. - Keep the tool narrow. One tool that answers one question is chosen correctly far more often than one that does six things behind a mode argument. - Return only what the answer needs, both because the cap is real and because everything returned is spent as agent context. - Restrict access where the data warrants it, through the Plugin's login and permission settings, rather than assuming the agent is a safe caller. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/webnavigatorpluginsaitools # Annotated documents (/operatoranndocuments) # Annotated documents Annotated documents are files with hot-spots that let you traverse between 2D and 3D views. Upload a PDF or SVG file in Operator to create one. Once an annotated document is made, you can drop a .HIT file onto a .SVG document, or a .XML onto a PDF document to automatically create the hot-spots. Hot-spots can also be manually created on selected areas on the document. Once a hot-spot is made and associated with an Entity, the document will appear in the `Attachments` tab in Navigator when the Entity is selected. Below is an example of a .HIT file that can be dropped onto a .SVG file to create hot-spots. ```text 100.1 10 my_key1 'This is a description of a hotspot' 200.2 50 my_key2 'This is another description of a hotspot' ``` The order of the columns is `X-coordinate`, `Y-coordinate`, `foreign-key`, and `Description`. The description is optional. You will be able to perform a query to match the foreign-keys with a selected Entity attribute to link the hot-spots to Entities. You can automatically include the Entity ID by adding `(Nextspace Entity ID: abcdef)` to the end of the description. For example: ```text 100.1 10 my_key1 'This is a description of a hotspot (Nextspace Entity ID: abcdef)' ``` When hot-spotting a PDF, the .XML file will look like this: ```xml ``` The primary difference between .HIT and .XML is that .XML will specify what page of the PDF the hot-spot is on. If unspecified, we assume page index 0. If no width and height are specified then we assume the hot-spot coordinates don't need adjustmnet to the target document proportions. If you need samples of .HIT or .XML files, you can manually place them then export the hot-spot file from Operator. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/operatoranndocuments # IBM Maximo (/maximo) # IBM Maximo We've developed a microservice and set of Nextspace Plugins to help integrate with a Maximo instance. We currently support retrieval of Asset, Work Order, and Service Request records from a configured Maximo instance. When combined with our plugins, you can visualize the Work Orders and Service Requests related to any rendered assets (Entities) in Navigator scenes. This experience can be embedded with an iframe into your Maximo installation, or run through Navigator. View in 3D from Maximo View in 3D from Nextspace Navigator Create from Nextspace Asset Selected asset can be visualized in 3D through the Nextspace Maximo connector. Data assumed to exist in both Maximo and Nextspace with a common foreign-key value to link them. Selected asset's Maximo attributes can be viewed through Entity panels or used to style the scene. Data assumed to exist in both Maximo and Nextspace with a common foreign-key value to link them. You can create Assets in Nextspace and the system will automatically generate matching records in Maximo. Records in Maximo or Nextspace can be manually assigned a foreign-key value to link them between the systems. Work Order Work Orders related to the selected asset or selected asset's site can be visualized through the Nextspace Maximo connector if the asset records exist in Nextspace. Work Orders can be visualized in 3D using the Nextspace plugins if corresponding asset records exist in Nextspace. Work Orders created through Nextspace can be visualized without a corresponding asset record. You can create Work Orders in Nextspace and the system will automatically generate matching records in Maximo. Records in Maximo or Nextspace can be manually assigned a foreign-key value to link them between the systems. Service Request Service Requests related to the selected asset or selected asset's site can be visualized through the Nextspace Maximo connector if the asset records exist in Nextspace. Service Requests can be visualized in 3D using the Nextspace plugins if corresponding asset records exist in Nextspace. Service Requests created through Nextspace can be visualized without a corresponding asset record. You can create Service Requests in Nextspace and the system will automatically generate matching records in Maximo. Records in Maximo or Nextspace can be manually assigned a foreign-key value to link them between the systems. Note that available actions may vary based on your Maximo access token's permissions. ## Setup prerequisites Before you can use the Maximo microservice, you will need to have a Maximo instance running and accessible over the internet. You will be prompted for a "URL" and "Access Token" when setting up the Data Source in Operator. These values are typically provided by your Maximo administrator. For details within setup instructions. The Nextspace Microservice will either retrieve full records from Maximo, or pull attribute data based on a user-defined foreign-key. It is recommended to first ensure there is a set of common asset records between your Maximo instance and Navigator scenes. There must be a common attribute value to tie the Maximo and Nextspace records together. If you don't have a set of common records, you are able to manually assign the Maximo's record ID in Nextspace to link the records together. If you are making demo data to test the integration, it is highly recommended to create a new site in Maximo to avoid unrelated "BEDFORD" data from being retrieved. ## Setup instructions To get started, create a new "Simple Microservice" Data Source in your Nextspace account through Operator. ![](/res/maximo/setup_new_source.png) ![](/res/maximo/setup_new_source_login.png) You will need to provide a "Database / URL" and "Access Token" value to proceed. The URL typically ends with `/api/os/`. ![](/res/maximo/setup_new_source_selection.png) Once you select what data you want to retrieve, you can continue to the individual instructions below for each data type. ## Setup instructions: Asset Assets are typically set up to pull attribute data from Maximo based on a common attribute value. You are supplementing information to existing Nextspace records with this setup. The setup will ask you what Entity Type to supplement, however you can configure other Entity Types after the setup without repeating the whole process. ![](/res/maximo/setup_new_source_mapping.png) Here is a basic mapping that will retrieve the "location" and "orgid" values from Maximo based on the common "assetnum" attribute. When there is an "assetnum" value, the other values will attempt to be retreived from Maximo. It is recommended to use either "assetnum" or "location" as the foreign-key. ![](/res/maximo/setup_new_source_str.png) You will want to select a "Pass-Through" strategy with "Provides attributes for existing Entities" for this configuration. You have now configured this data source. You should see the Maximo attribute values appearing when you view Entity records. To configure another Entity Type, open it from the "Entity ► Entity Types" panel and add the new Data Source to it. ![](/res/maximo/setup_new_source_asset_again_type.png) ![](/res/maximo/setup_new_source_asset_again.png) Ensure to select the "Attribute" strategy with "Pass-Through". You will also need to repeat the Data Mapping configuration. It is recommended to keep the foreign-key attribute consistent between your Entity Types so that the same configuration can be used when utilizing our plugins. ## Setup instructions: Work Order / Service Request The Work Order and Service Request setup is the same so we'll cover both at once. Unlike the Asset setup, we will be retrieving full records instead of supplementing attributes to existing Nextspace data. This setup will ensure that the first page of Entities retrieved will be the latest Work Order and Service Request records from Maximo. ![](/res/maximo/setup_new_source_mapping_wo.png) Unlike the asset setup, it is recommended to create a new Entity Type so you can copy over all the available attributes from Maximo. The setup may suggest a foreign-key so ensure to double-check it. For Work Orders we recommend the "wonum" attribute, and for Service Requests we recommend the "ticketid" attribute. ![](/res/maximo/setup_new_source_str_wo.png) For this setup, you will want to select "Pass-Through" with "Provides Entities" to ensure the latest full records are always retrieved. ## Propagate updates If your access key has the permission to create/update records in Maximo, then you can enable the "Propagate updates" feature in the Entity Type Data Source settings. Certain records in Maximo will require a few default values, it is recommended to use Entity Type default values to set these. This is typically the `orgid`, `siteid`, and `status` (for Work Orders). You can also set a `description` so your Work Orders don't have empty titles. With this setup, you can create Entity records in Navigator or Operator and corresponding ones will be made in Maximo. Edits will attempt to propagate to the external system (such as changes to the description). ![](/res/maximo/default_values.png) ## Plugins ![](/res/maximo/plugins.png) To now best utilize your setup, we recommend reaching out about our Maximo plugins. These are installed into your Nextspace account. We provide three plugins: - Maximo Work Orders - List recent Work Orders and visualize them in Navigator scenes. - Maximo Service Requests - List recent Service Requests and visualize them in Navigator scenes. - Maximo Asset - Visualize Asset data related to the selected Entity. Note that these plugins are currently tailored to assembly/BIM data, however they can be tailored for general use (GIS, etc). For best utilization, we recommend working to embed our Navigator and plugins into your Maximo installation as an iframe. See the [Embed Quick Start](https://embed.nextspace.host/quick-start) for more information on how to embed Navigator. Buttons in the plugins will send window messages for your iframe logic to handle. For example to open specific records in Maximo. To start handling these actions once you retrieve the plugins, listen to window post-messages and see how they are structured. ## Plugin setup Once you retrieve the plugins, they will require configuration settings to properly understand your data structure. These settings can be configured within the selected plugin's details in Operator. The "Maximo Asset Details" plugin requires these settings: ```javascript { "panelWidth": "400px", "typeIds": { // Entity Type ID for Work Orders. "workOrders": "your-work-order-entity-type-id", }, "types": { "asset": { // Nextspace attribute path to the "assetnum" attribute. "keyPath": "attr-path-to-assetnum", // Nextspace attribute path to the "location" attribute. "keyPathLocation": "attr-path-to-location", }, "workOrder": { // Nextspace attribute path to the "wonum" attribute. "keyPath": "attr-path-to-wonum", // Nextspace attribute path to the "assetnum" attribute. "keyPathAsset": "attr-path-to-asset-num", // Nextspace attribute path to the "description" attribute. "keyPathDescription": "attr-path-to-description", // Nextspace attribute path to the "location" attribute. "keyPathLocation": "attr-path-to-location", // Nextspace attribute path to the "priority" attribute. "keyPathPriority": "attr-path-to-priority", // Nextspace attribute path to the "reportdate" attribute. "keyPathReportedDate": "attr-path-to-reportdate", // Nextspace attribute path to the "site" attribute. "keyPathSite": "attr-path-to-site", // Nextspace attribute path to the "status" attribute. "keyPathStatus": "attr-path-to-status", } } } ``` The "Maximo Service Requests" plugin requires these settings: ```javascript { "panelWidth": "400px", "typeIds": { // Entity Type ID for Service Requests. "serviceRequests": "your-service-request-entity-type-id", }, "types": { "asset": { // Nextspace attribute path to the "assetnum" attribute. "keyPath": "attr-path-to-assetnum", // Nextspace attribute path to the "location" attribute. "keyPathLocation": "attr-path-to-location", }, "serviceRequest": { // Nextspace attribute path to the "ticketid" attribute. "keyPath": "attr-path-to-ticketid", // Nextspace attribute path to the "assetnum" attribute. "keyPathAsset": "attr-path-to-assetnum", // Nextspace attribute path to the "description" attribute. "keyPathDescription": "attr-path-to-description", // Nextspace attribute path to the "location" attribute. "keyPathLocation": "attr-path-to-location", // Nextspace attribute path to the "priority" attribute. "keyPathPriority": "attr-path-to-priority", // Nextspace attribute path to the "reportdate" attribute. "keyPathReportedDate": "attr-path-to-reportdate", // Nextspace attribute path to the "site" attribute. "keyPathSite": "attr-path-to-site", // Nextspace attribute path to the "status" attribute. "keyPathStatus": "attr-path-to-status", } } } ``` The "Maximo Asset Details" plugin requires these settings: ```javascript { "typeIds": { // Entity Type ID for Work Orders. "workOrders": "your-work-order-entity-type-id", }, "types": { "asset": { // Nextspace attribute path to the "assetnum" attribute. "keyPath": "attr-path-to-assetnum", // Nextspace attribute path to the "location" attribute. "keyPathLocation": "attr-path-to-location", }, "workOrder": { // Nextspace attribute path to the "wonum" attribute. "keyPath": "attr-path-to-wonum", // Nextspace attribute path to the "assetnum" attribute. "keyPathAsset": "attr-path-to-asset-num", // Nextspace attribute path to the "description" attribute. "keyPathDescription": "attr-path-to-description", // Nextspace attribute path to the "location" attribute. "keyPathLocation": "attr-path-to-location", // Nextspace attribute path to the "priority" attribute. "keyPathPriority": "attr-path-to-priority", // Nextspace attribute path to the "reportdate" attribute. "keyPathReportedDate": "attr-path-to-reportdate", // Nextspace attribute path to the "site" attribute. "keyPathSite": "attr-path-to-site", // Nextspace attribute path to the "status" attribute. "keyPathStatus": "attr-path-to-status", } } } ``` --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/maximo # Unreal Engine 5 (/unreal5) # Unreal Engine 5 We have an experimental Unreal 5 project that we are happy to share with anyone who wants to try build from it. If you see reference to Unreal Engine 4 in this documentation, it is referring to the same project. We had originally made it for Unreal Engine 4 but have since upgraded it to Unreal Engine 5. ## Setup instructions The zip includes markdown documentation on how to build, run, and publish the project. Start by downloading the code repo from here: Login to reveal. Once downloaded, make a folder in `C:/Dev` and unzip the contents so it matches the screenshot below. ![](/res/unreal/unzipped.png) Open the `Docs/SETUP_1_prerequisites.md` file in your favourite markdown viewer to get started. I am using VS code. ![](/res/unreal/markdown.png) Now proceed to run through the setup steps to build and modify the plugins we use, then finally the project itself. --- Urls on this page are resolved for account `{accountId}`. Site index: http://docs.nextspace.host/llms.txt · whole site in one file: http://docs.nextspace.host/llms-full.txt Human-readable version of this page: http://docs.nextspace.host/unreal5