@thatopen/services 0.16.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/built-in/index.d.ts +1043 -0
- package/dist/cli.js +15 -5
- package/dist/core/client.d.ts +11 -4
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +27 -19
- package/dist/types/files.d.ts +2 -0
- package/docs/builtin/paths.json +16 -0
- package/package.json +2 -1
- package/src/built-in/GISManager/example.ts +326 -0
- package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +5 -2
- package/src/built-in/UIManager/src/gis-panel/example.ts +163 -0
- package/src/built-in/UIManager/src/gis-properties-panel/example.ts +168 -0
- package/src/built-in/UIManager/src/map-panel/example.ts +189 -0
- package/src/built-in/UIManager/src/models-list/example.ts +61 -2
- package/src/built-in/UIManager/src/settings-panel/example.ts +5 -3
- package/src/built-in/UIManager/src/viewer-toolbar/example.ts +143 -19
- package/src/built-in/index.ts +1051 -0
- package/src/core/examples/hidden-files.ts +7 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/* MD
|
|
2
|
+
## GISManager
|
|
3
|
+
---
|
|
4
|
+
Georeferenced BIM projects need their surroundings: the real terrain and
|
|
5
|
+
buildings around the model, and the project's GIS layers (parcels, networks,
|
|
6
|
+
boundaries) drawn at the right place in the 3D scene and on a 2D map.
|
|
7
|
+
|
|
8
|
+
GISManager is the component that owns all of that. It streams Cesium Ion 3D
|
|
9
|
+
Tiles (Google Photorealistic 3D Tiles by default) around the project origin,
|
|
10
|
+
loads vector layers (shapefile zip or GeoJSON) declared in the project's
|
|
11
|
+
layer manifest, and renders a 2D minimap with markers.
|
|
12
|
+
|
|
13
|
+
This tutorial covers initializing the manager from the platform client;
|
|
14
|
+
enabling the 3D tiles; the layer manifest and how heights, styles and
|
|
15
|
+
coordinate systems are read; selecting and focusing features; and the 2D
|
|
16
|
+
minimap with its markers. The recommended UI is provided by `top-gis-panel`,
|
|
17
|
+
`top-map-panel` and `top-gis-properties-panel`.
|
|
18
|
+
|
|
19
|
+
GISManager reads the project origin from `ProjectManager`, so initialize
|
|
20
|
+
`ProjectManager` first.
|
|
21
|
+
|
|
22
|
+
### 🖖 Importing our libraries
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import * as OBC from "@thatopen/components";
|
|
26
|
+
import * as OBF from "@thatopen/components-front";
|
|
27
|
+
import * as BUI from "@thatopen/ui";
|
|
28
|
+
import * as THREE from "three";
|
|
29
|
+
import * as FRAGS from "@thatopen/fragments";
|
|
30
|
+
import { PlatformClient, GISManager, ProjectManager } from "@thatopen/services";
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
/* MD
|
|
34
|
+
### 🚀 Initializing GISManager
|
|
35
|
+
Both components are set up through the platform client.
|
|
36
|
+
`client.setup(...)` resolves the shared `OBC.Components` instance.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const client = PlatformClient.fromPlatformContext();
|
|
40
|
+
|
|
41
|
+
const { components } = (await client.setup(
|
|
42
|
+
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
43
|
+
{ uuid: ProjectManager.uuid },
|
|
44
|
+
{ uuid: GISManager.uuid },
|
|
45
|
+
)) as { components: OBC.Components };
|
|
46
|
+
|
|
47
|
+
await components.get(ProjectManager).init(client);
|
|
48
|
+
|
|
49
|
+
const gis = components.get(GISManager);
|
|
50
|
+
|
|
51
|
+
/* MD
|
|
52
|
+
`init(client)` restores the project origin from `ProjectManager` and reads
|
|
53
|
+
the project's layer manifest (see below). It never throws: with no project or
|
|
54
|
+
no stored file the manager just keeps its defaults, and an empty layer list.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
await gis.init(client);
|
|
58
|
+
console.log("layers:", gis.vectorLayersConfig);
|
|
59
|
+
|
|
60
|
+
/* MD
|
|
61
|
+
### 🌍 3D tiles
|
|
62
|
+
Streaming the tiles needs a Cesium Ion token. The component does not persist
|
|
63
|
+
the token anywhere: your application provides it (from its own settings, or
|
|
64
|
+
typed by the user in `top-gis-panel`) before enabling the tiles.
|
|
65
|
+
|
|
66
|
+
`enableTiles()` resolves once the first tiles are loaded and rejects with a
|
|
67
|
+
readable message if the token is invalid or has no access to the tileset (the
|
|
68
|
+
same message is also fired through `onLoadError`).
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
gis.onLoadError.add((message) => console.warn("GIS tiles:", message));
|
|
72
|
+
|
|
73
|
+
gis.apiToken = "<your Cesium Ion token>";
|
|
74
|
+
await gis.enableTiles();
|
|
75
|
+
|
|
76
|
+
/* MD
|
|
77
|
+
`onStateChanged` fires whenever the token, origin, tiles visibility, tiles
|
|
78
|
+
offset or tiles opacity change, with a plain snapshot of all of them.
|
|
79
|
+
`setTilesOpacity(0.5)` makes the tiles see-through (1 = opaque), which lets
|
|
80
|
+
you see vector layers that run underground. `disableTiles()` unloads
|
|
81
|
+
everything.
|
|
82
|
+
|
|
83
|
+
On hosts whose engine supports the deferred postproduction pipeline, the
|
|
84
|
+
tiles and the vector layers are drawn through an occlusion pass, which is what
|
|
85
|
+
makes the see-through effect possible. On other hosts they are added to the
|
|
86
|
+
scene directly and stay opaque. You do not need to do anything for this: the
|
|
87
|
+
component detects it.
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
gis.onStateChanged.add((state) => console.log("tiles visible:", state.tilesVisible));
|
|
91
|
+
gis.setTilesOpacity(0.6);
|
|
92
|
+
|
|
93
|
+
/* MD
|
|
94
|
+
A small East/North/Up correction (in metres) can be applied to the tiles to
|
|
95
|
+
compensate for the tileset's own positioning error. Persist it with
|
|
96
|
+
`saveTilesOffset()`; it is stored in the same file as the layer manifest and
|
|
97
|
+
restored by `init`.
|
|
98
|
+
*/
|
|
99
|
+
|
|
100
|
+
gis.onOffsetSaveComplete.add((success) => console.log("offset saved:", success));
|
|
101
|
+
gis.setTilesOffset(0.5, -0.3, 0);
|
|
102
|
+
gis.saveTilesOffset();
|
|
103
|
+
|
|
104
|
+
/* MD
|
|
105
|
+
### 🗂️ The layer manifest
|
|
106
|
+
The vector layers of a project are declared in one JSON file,
|
|
107
|
+
`{GISManager.uuid}.json`, stored loose in the project's `__project_data`
|
|
108
|
+
folder. `init(client)` reads it into `gis.vectorLayersConfig`; it only holds
|
|
109
|
+
metadata (the geometry stays in the layer's own file, referenced by `fileId`
|
|
110
|
+
and downloaded when the layer is loaded).
|
|
111
|
+
|
|
112
|
+
There is no user interface to register layers yet: the manifest is written by
|
|
113
|
+
your application (or by hand). The component only reads it, and rewrites it
|
|
114
|
+
(keeping `layers` as they are) when the tiles offset is saved.
|
|
115
|
+
|
|
116
|
+
Each entry of `layers` has:
|
|
117
|
+
|
|
118
|
+
- `id` — unique id, used by every API below.
|
|
119
|
+
- `name` — display name (shown by the panels).
|
|
120
|
+
- `fileId` — id of the layer's file in the project.
|
|
121
|
+
- `type` — `"shapefile"` (a zip with the `.shp`, `.dbf`, `.prj`… files) or
|
|
122
|
+
`"geojson"`. If it is missing or unrecognised the downloaded bytes decide:
|
|
123
|
+
a zip signature means shapefile, a leading `{` means GeoJSON.
|
|
124
|
+
- `style` — `stroke` (CSS colour), `stroke-width` (pixels), `fill` (CSS
|
|
125
|
+
colour, polygons) and `fill-opacity` (0 to 1). All optional.
|
|
126
|
+
- `height` — optional, how features get their height (next section).
|
|
127
|
+
|
|
128
|
+
Here is a complete manifest with a points layer whose vertices carry their
|
|
129
|
+
own height, and a lines layer whose heights come from its attributes:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"layers": [
|
|
134
|
+
{
|
|
135
|
+
"id": "manholes",
|
|
136
|
+
"name": "Manholes",
|
|
137
|
+
"fileId": "<file id of manholes.zip>",
|
|
138
|
+
"type": "shapefile",
|
|
139
|
+
"style": { "stroke": "#ffb300", "stroke-width": 6 },
|
|
140
|
+
"height": { "source": "geometry" }
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"id": "pipes",
|
|
144
|
+
"name": "Pipes",
|
|
145
|
+
"fileId": "<file id of pipes.geojson>",
|
|
146
|
+
"type": "geojson",
|
|
147
|
+
"style": { "stroke": "#1e88e5", "stroke-width": 3 },
|
|
148
|
+
"height": {
|
|
149
|
+
"source": "attributes",
|
|
150
|
+
"startProperty": "z_start",
|
|
151
|
+
"endProperty": "z_end",
|
|
152
|
+
"startDepthProperty": "depth_start",
|
|
153
|
+
"endDepthProperty": "depth_end",
|
|
154
|
+
"nodeLayers": ["manholes"],
|
|
155
|
+
"nodeSnapDistance": 0.5
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
],
|
|
159
|
+
"tilesOffset": { "east": 0.5, "north": -0.3, "up": 0 }
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Once `init` has read it, every layer can be loaded and unloaded by id.
|
|
164
|
+
`loadVectorLayer` downloads and parses the file; it never throws, failures
|
|
165
|
+
are reported through `onVectorLayerError`.
|
|
166
|
+
*/
|
|
167
|
+
|
|
168
|
+
gis.onLayersLoaded.add((layers) => console.log("manifest read:", layers.length, "layers"));
|
|
169
|
+
gis.onVectorLayerError.add(({ id, message }) => console.warn(`Layer ${id}:`, message));
|
|
170
|
+
|
|
171
|
+
await gis.loadVectorLayer("manholes");
|
|
172
|
+
await gis.loadVectorLayer("pipes");
|
|
173
|
+
|
|
174
|
+
console.log("loaded:", gis.vectorLayers.isLayerLoaded("pipes"));
|
|
175
|
+
|
|
176
|
+
gis.unloadVectorLayer("manholes");
|
|
177
|
+
|
|
178
|
+
/* MD
|
|
179
|
+
### 📏 Layer heights
|
|
180
|
+
The `height` option of a layer decides at what height its features are drawn:
|
|
181
|
+
|
|
182
|
+
- **No `height`** — the layer is drawn flat, at the origin's height.
|
|
183
|
+
- **`{ "source": "geometry" }`** — every vertex is placed at its own Z (the
|
|
184
|
+
third GeoJSON coordinate). If any vertex of the layer has no Z, the whole
|
|
185
|
+
layer is drawn flat instead (with a console warning).
|
|
186
|
+
- **`{ "source": "attributes", … }`** — for line features only (other
|
|
187
|
+
geometry types are drawn flat). A line gets a height at its first vertex
|
|
188
|
+
and at its last one, and the vertices in between are interpolated along
|
|
189
|
+
the line. Each end is resolved in this order:
|
|
190
|
+
1. its own height property (`startProperty` / `endProperty`);
|
|
191
|
+
2. otherwise the surface height of the nearest node minus its depth
|
|
192
|
+
property (`startDepthProperty` / `endDepthProperty`). Nodes are the
|
|
193
|
+
points with a Z found in the layers listed in `nodeLayers`, and
|
|
194
|
+
"nearest" means within `nodeSnapDistance` metres (0.5 by default) of
|
|
195
|
+
the line end;
|
|
196
|
+
3. otherwise the height resolved for the other end (the line is drawn
|
|
197
|
+
level);
|
|
198
|
+
4. otherwise the surface height of the nearest node.
|
|
199
|
+
|
|
200
|
+
A property counts as missing when it is not a finite number or is exactly
|
|
201
|
+
`0`. Rules 1 and 2 are tried on both ends before rule 3 is applied, so a
|
|
202
|
+
copied height never comes from another copy. A line for which no rule can
|
|
203
|
+
give a height to both ends is not drawn (one console warning per layer
|
|
204
|
+
summarizes how many).
|
|
205
|
+
|
|
206
|
+
All heights are orthometric (the usual survey heights above the geoid). The
|
|
207
|
+
component adds the project's geoid undulation, taken from the `ProjectManager`
|
|
208
|
+
CRS, to place them in the scene, and re-places the loaded layers if it changes.
|
|
209
|
+
*/
|
|
210
|
+
|
|
211
|
+
/* MD
|
|
212
|
+
### 🧭 GeoJSON coordinate systems
|
|
213
|
+
Shapefiles are read with their `.prj`. For GeoJSON layers, the coordinate
|
|
214
|
+
system comes from the legacy `crs` member of the file:
|
|
215
|
+
|
|
216
|
+
- Longitude/latitude (`CRS84`, or `EPSG:4326`) is read as it is.
|
|
217
|
+
- `EPSG:3857` and the UTM zones (`EPSG:326xx`, `327xx`, `258xx` and `269xx`)
|
|
218
|
+
are reprojected to longitude/latitude when the layer loads.
|
|
219
|
+
- Any other declared CRS is rejected, and so are projected coordinates in a
|
|
220
|
+
file that declares no `crs`.
|
|
221
|
+
|
|
222
|
+
A rejected layer is not loaded, and the reason (for example `Unsupported CRS
|
|
223
|
+
"EPSG:2154"…`) is delivered through `onVectorLayerError`, which is what the
|
|
224
|
+
GIS panel shows as a toast.
|
|
225
|
+
*/
|
|
226
|
+
|
|
227
|
+
/* MD
|
|
228
|
+
### 🖱️ Selection and focus
|
|
229
|
+
Clicking a feature in the 3D view selects it. `onFeatureSelected` delivers
|
|
230
|
+
the selection (`layerId`, `layerName`, `featureIndex`, `geometryType` and the
|
|
231
|
+
feature's `properties`), or `null` when the selection is cleared. The current
|
|
232
|
+
selection can also be read at any time from `gis.vectorLayers.selection`.
|
|
233
|
+
|
|
234
|
+
Your application can select a feature too, optionally flying the camera to
|
|
235
|
+
it. `onFocusRequested` fires with the bounding sphere the camera is sent to,
|
|
236
|
+
which is how the 2D map follows.
|
|
237
|
+
*/
|
|
238
|
+
|
|
239
|
+
gis.onFeatureSelected.add((selection) => {
|
|
240
|
+
if (!selection) return console.log("selection cleared");
|
|
241
|
+
console.log(selection.layerName, selection.geometryType, selection.properties);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
gis.onFocusRequested.add(({ layerId, featureIndex, sphere }) => {
|
|
245
|
+
console.log("focusing", layerId, featureIndex, "radius:", sphere.radius);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
gis.selectVectorFeature("pipes", 0, { focus: true });
|
|
249
|
+
gis.vectorLayers.clearSelection();
|
|
250
|
+
|
|
251
|
+
/* MD
|
|
252
|
+
### 🗺️ The 2D minimap
|
|
253
|
+
`gis.minimap` renders a top-down, north-up image of the 3D tiles that
|
|
254
|
+
`top-map-panel` draws together with the vector layers, the camera and the
|
|
255
|
+
markers.
|
|
256
|
+
|
|
257
|
+
`captureMap()` takes the picture. The 3D tiles must be loaded (without them
|
|
258
|
+
the image is empty). The captured area is the bounding box of all the
|
|
259
|
+
manifest's layers, whether they are displayed or not; with no usable layers it
|
|
260
|
+
is a square of `2 * halfExtent` metres (250 by default) centred on the origin.
|
|
261
|
+
The image has a fixed side of `resolution` pixels (2048 by default, limited by
|
|
262
|
+
the GPU), so the larger the area, the coarser the map.
|
|
263
|
+
*/
|
|
264
|
+
|
|
265
|
+
gis.minimap.onCaptured.add((capture) => console.log("map captured:", capture.size, "px"));
|
|
266
|
+
|
|
267
|
+
gis.minimap.halfExtent = 400;
|
|
268
|
+
gis.minimap.resolution = 4096;
|
|
269
|
+
const capture = await gis.minimap.captureMap();
|
|
270
|
+
document.body.appendChild(capture.image);
|
|
271
|
+
|
|
272
|
+
/* MD
|
|
273
|
+
`worldToMap` converts a scene position into normalized image coordinates
|
|
274
|
+
(0 to 1, origin at the top-left), and `mapToWorld` does the opposite for a
|
|
275
|
+
horizontal plane at a given height. Both return `null` until a map has been
|
|
276
|
+
captured.
|
|
277
|
+
*/
|
|
278
|
+
|
|
279
|
+
const point = gis.vectorLayers.geoToWorld(gis.latitude, gis.longitude, gis.height);
|
|
280
|
+
const onMap = gis.minimap.worldToMap(point);
|
|
281
|
+
console.log("origin on the map:", onMap);
|
|
282
|
+
console.log("back to the scene:", onMap && gis.minimap.mapToWorld(onMap.x, onMap.y, point.y));
|
|
283
|
+
|
|
284
|
+
/* MD
|
|
285
|
+
### 📍 Minimap markers
|
|
286
|
+
Markers are HTML elements drawn over the map by `top-map-panel`. They live in
|
|
287
|
+
the minimap, not in the panel, so they survive the panel being recreated.
|
|
288
|
+
|
|
289
|
+
A marker has an `id`, a scene `position` (read on every redraw, so changing
|
|
290
|
+
it moves the marker), an optional `label` and an optional `createElement`
|
|
291
|
+
factory. Without the factory the panel draws a default badge with the label
|
|
292
|
+
and reports its clicks through `onMarkerClicked`. With it, the marker is your
|
|
293
|
+
own element and it owns its click behaviour. It is a factory and not an
|
|
294
|
+
element because each panel needs its own DOM node.
|
|
295
|
+
|
|
296
|
+
`geoToWorld(lat, lon, height)` turns a geographic position into a scene
|
|
297
|
+
position.
|
|
298
|
+
*/
|
|
299
|
+
|
|
300
|
+
gis.minimap.onMarkersChanged.add(({ kind, id }) => console.log("markers:", kind, id));
|
|
301
|
+
gis.minimap.onMarkerClicked.add((id) => console.log("clicked:", id));
|
|
302
|
+
|
|
303
|
+
gis.minimap.addMarker({
|
|
304
|
+
id: "site-office",
|
|
305
|
+
label: "Site office",
|
|
306
|
+
position: gis.vectorLayers.geoToWorld(gis.latitude, gis.longitude, gis.height),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
gis.minimap.addMarker({
|
|
310
|
+
id: "crane",
|
|
311
|
+
position: new THREE.Vector3(-20, 0, 35),
|
|
312
|
+
createElement: () => {
|
|
313
|
+
const pin = document.createElement("div");
|
|
314
|
+
pin.textContent = "🏗️";
|
|
315
|
+
pin.style.fontSize = "1.4rem";
|
|
316
|
+
pin.style.cursor = "pointer";
|
|
317
|
+
pin.onclick = () => console.log("crane clicked");
|
|
318
|
+
return pin;
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
gis.minimap.updateMarker("site-office", { label: "Main office" });
|
|
323
|
+
console.log("markers:", [...gis.minimap.markers.keys()]);
|
|
324
|
+
|
|
325
|
+
gis.minimap.removeMarker("crane");
|
|
326
|
+
gis.minimap.clearMarkers();
|
|
@@ -73,8 +73,11 @@ app.elements = {
|
|
|
73
73
|
};
|
|
74
74
|
|
|
75
75
|
app.layouts = {
|
|
76
|
-
// Each layout defines its own CSS grid template.
|
|
77
|
-
//
|
|
76
|
+
// Each layout defines its own CSS grid template. The app mounts the UNION
|
|
77
|
+
// of every layout's areas ONCE and switching layouts toggles which are
|
|
78
|
+
// visible (the rest stay mounted with display:none), so panel state
|
|
79
|
+
// survives a switch - element factories run on mount, not per layout.
|
|
80
|
+
// To react to a switch, listen for the `top:layout-changed` event.
|
|
78
81
|
viewer: {
|
|
79
82
|
label: "Viewer",
|
|
80
83
|
icon: "solar:3d-square-bold",
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/* MD
|
|
2
|
+
## gis-panel
|
|
3
|
+
---
|
|
4
|
+
A georeferenced project is much easier to review with its real surroundings
|
|
5
|
+
around the model, and with the GIS layers of the site (networks, parcels,
|
|
6
|
+
boundaries) switched on and off from a list. Streaming the terrain and
|
|
7
|
+
buildings needs a Cesium Ion token and a way to load and unload them, and
|
|
8
|
+
fine-tuning their position needs a few controls.
|
|
9
|
+
|
|
10
|
+
`gis-panel` is a ready-made web component that delivers all of that. It
|
|
11
|
+
connects to `GISManager` automatically and gives the user the token input, the
|
|
12
|
+
button that loads the 3D map, the position fine-tune and the list of the
|
|
13
|
+
project's vector layers.
|
|
14
|
+
|
|
15
|
+
This tutorial covers the prerequisites before mounting the panel; dropping it
|
|
16
|
+
into a `top-app` layout next to a 3D viewer; and a breakdown of everything the
|
|
17
|
+
panel handles automatically so you know what you do not need to implement
|
|
18
|
+
yourself.
|
|
19
|
+
|
|
20
|
+
By the end, you'll have the GIS controls of your project running in your
|
|
21
|
+
application with a single line of markup.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { html } from "lit";
|
|
25
|
+
import * as THREE from "three";
|
|
26
|
+
import * as OBC from "@thatopen/components";
|
|
27
|
+
import * as OBF from "@thatopen/components-front";
|
|
28
|
+
import * as FRAGS from "@thatopen/fragments";
|
|
29
|
+
import * as BUI from "@thatopen/ui";
|
|
30
|
+
import { PlatformClient, UIManager, GISManager, ProjectManager } from "@thatopen/services";
|
|
31
|
+
import type { App } from "../app/index";
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/* MD
|
|
35
|
+
### ✅ Prerequisites
|
|
36
|
+
|
|
37
|
+
Three conditions must be met before the panel mounts:
|
|
38
|
+
|
|
39
|
+
1. **`UIManager` must be in the setup call.** It registers all platform web
|
|
40
|
+
components, including `<top-gis-panel>` itself. Without it the element is
|
|
41
|
+
unknown to the browser and renders as an empty box.
|
|
42
|
+
|
|
43
|
+
2. **`ProjectManager.init(client)` must have resolved.** `GISManager` takes
|
|
44
|
+
the project origin from it: without it the 3D tiles and the vector layers
|
|
45
|
+
are placed around a default origin instead of the project's.
|
|
46
|
+
|
|
47
|
+
3. **`GISManager.init(client)` must have resolved**, after `ProjectManager`.
|
|
48
|
+
The panel reads the manager's state when it is
|
|
49
|
+
connected to the document (the token, the tiles state, the tiles offset and
|
|
50
|
+
the layer list), then follows its events. Without `init`, the manifest is
|
|
51
|
+
never read, so the layer list stays empty, and the tiles offset cannot be
|
|
52
|
+
saved.
|
|
53
|
+
|
|
54
|
+
Both are satisfied by awaiting the two `init`s, in that order, before
|
|
55
|
+
appending `top-app` to the document, which is the pattern shown below.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
const client = PlatformClient.fromPlatformContext();
|
|
59
|
+
|
|
60
|
+
const { components } = (await client.setup(
|
|
61
|
+
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
62
|
+
{ uuid: UIManager.uuid },
|
|
63
|
+
{ uuid: ProjectManager.uuid },
|
|
64
|
+
{ uuid: GISManager.uuid },
|
|
65
|
+
)) as { components: OBC.Components };
|
|
66
|
+
|
|
67
|
+
components.get(UIManager).init();
|
|
68
|
+
|
|
69
|
+
// Order matters: GISManager reads the origin that ProjectManager has loaded.
|
|
70
|
+
await components.get(ProjectManager).init(client);
|
|
71
|
+
await components.get(GISManager).init(client);
|
|
72
|
+
|
|
73
|
+
/* MD
|
|
74
|
+
### 🖥️ Wiring the panel
|
|
75
|
+
|
|
76
|
+
Add `gis-panel` as a named area in `app.elements` and reference it in your
|
|
77
|
+
layout template. A narrow column on the left of the 3D viewer is the natural
|
|
78
|
+
arrangement: the tiles and the layers are drawn in the viewer's scene.
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
const app = document.createElement("top-app") as unknown as App;
|
|
82
|
+
|
|
83
|
+
app.setup = (waitUntil) => {
|
|
84
|
+
waitUntil(
|
|
85
|
+
(async () => {
|
|
86
|
+
const fragments = components.get(OBC.FragmentsManager);
|
|
87
|
+
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
88
|
+
fragments.init(await FRAGS.toClassicWorker(workerUrl), { classicWorker: true });
|
|
89
|
+
})(),
|
|
90
|
+
"Fragments Core",
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return { components, client };
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
app.elements = {
|
|
97
|
+
gis: () => html`<top-gis-panel></top-gis-panel>`,
|
|
98
|
+
viewer: () => html`<top-viewer><top-viewer-tools></top-viewer-tools></top-viewer>`,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
app.layouts = {
|
|
102
|
+
main: {
|
|
103
|
+
label: "Main",
|
|
104
|
+
icon: "mdi:earth",
|
|
105
|
+
template: `"gis viewer" 1fr / 22rem 1fr`,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
app.layout = "main";
|
|
110
|
+
|
|
111
|
+
const container = document.getElementById("that-open-app") ?? document.body;
|
|
112
|
+
container.appendChild(app);
|
|
113
|
+
document.body.style.margin = "0";
|
|
114
|
+
|
|
115
|
+
/* MD
|
|
116
|
+
### 🤖 What the panel manages automatically
|
|
117
|
+
|
|
118
|
+
Once mounted, `gis-panel` takes ownership of the following without any
|
|
119
|
+
further code from you:
|
|
120
|
+
|
|
121
|
+
**Cesium Ion token** — a password field holds the token. It is written to
|
|
122
|
+
`GISManager.apiToken` as the user types. The panel does not store it: if
|
|
123
|
+
you want it to survive a reload, set `gis.apiToken` yourself before the
|
|
124
|
+
panel mounts.
|
|
125
|
+
|
|
126
|
+
**Loading the 3D map** — the "Load GIS Map" button calls `enableTiles()`
|
|
127
|
+
(disabled until there is a token) and turns into "Unload GIS Map", which calls
|
|
128
|
+
`disableTiles()`. While the tiles load the button shows its loading state. A
|
|
129
|
+
success toast confirms the load, and a failure shows the readable message
|
|
130
|
+
coming from `onLoadError` (invalid token, no access to the tileset, rate
|
|
131
|
+
limit, no connection).
|
|
132
|
+
|
|
133
|
+
**Tiles fine-tune offset** — three sliders (East, North, Up, in metres) move
|
|
134
|
+
the tiles through `setTilesOffset`. Once the offset differs from the saved
|
|
135
|
+
one, a "Save offset" button appears. It calls `saveTilesOffset()`, which
|
|
136
|
+
writes the offset into the project's layer manifest, and a toast reports
|
|
137
|
+
whether the save worked. The offset is restored by `GISManager.init` the next
|
|
138
|
+
time the project is opened.
|
|
139
|
+
|
|
140
|
+
**Vector layers** — a table lists the layers declared in the manifest
|
|
141
|
+
(`vectorLayersConfig`), each with a checkbox. Checking it loads the layer
|
|
142
|
+
(`loadVectorLayer`), unchecking it removes it (`unloadVectorLayer`), and the
|
|
143
|
+
row shows "(loading…)" in between. If a layer cannot be read, for example an
|
|
144
|
+
unsupported coordinate system, an error toast carries the reason reported by
|
|
145
|
+
`onVectorLayerError`. The list updates by itself if the manifest is read
|
|
146
|
+
after the panel mounts.
|
|
147
|
+
|
|
148
|
+
**Registering layers** — the panel only lists and toggles the layers the
|
|
149
|
+
manifest already declares. There is no user interface to upload a shapefile or
|
|
150
|
+
a GeoJSON file, name it and style it yet: your application writes the
|
|
151
|
+
manifest (see the `GISManager` tutorial for its format).
|
|
152
|
+
|
|
153
|
+
**Origin** — the project origin is not edited here. It belongs to
|
|
154
|
+
`ProjectManager`, and it is set from the "Coordinates" section of the settings
|
|
155
|
+
panel. The tiles and the layers follow it live.
|
|
156
|
+
|
|
157
|
+
**Notifications** — every toast is a `top:notification` event that bubbles up
|
|
158
|
+
to `top-app`, which displays it.
|
|
159
|
+
|
|
160
|
+
**Disconnection cleanup** — when the panel is removed from the DOM it
|
|
161
|
+
unsubscribes from every `GISManager` event. The 3D map and the layers stay
|
|
162
|
+
loaded.
|
|
163
|
+
*/
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/* MD
|
|
2
|
+
## gis-properties-panel
|
|
3
|
+
---
|
|
4
|
+
A GIS feature (a pipe, a parcel, a manhole) is useless without its
|
|
5
|
+
attributes: diameter, material, owner, installation date. Users want to
|
|
6
|
+
click a feature, in the 3D view or on the map, and read them.
|
|
7
|
+
|
|
8
|
+
`gis-properties-panel` is a ready-made web component that delivers exactly
|
|
9
|
+
that. It connects to `GISManager` automatically and shows the attribute table of
|
|
10
|
+
the selected vector feature, with a filter and readable values.
|
|
11
|
+
|
|
12
|
+
This tutorial covers the prerequisites before mounting the panel; dropping it
|
|
13
|
+
into a `top-app` layout next to the map; a breakdown of everything the panel
|
|
14
|
+
handles automatically; and how an application can use one properties column
|
|
15
|
+
for both BIM and GIS selections.
|
|
16
|
+
|
|
17
|
+
By the end, you'll have GIS attributes on screen with a single line of markup.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { html } from "lit";
|
|
21
|
+
import * as THREE from "three";
|
|
22
|
+
import * as OBC from "@thatopen/components";
|
|
23
|
+
import * as OBF from "@thatopen/components-front";
|
|
24
|
+
import * as FRAGS from "@thatopen/fragments";
|
|
25
|
+
import * as BUI from "@thatopen/ui";
|
|
26
|
+
import { PlatformClient, UIManager, GISManager, ProjectManager } from "@thatopen/services";
|
|
27
|
+
import type { App } from "../app/index";
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
/* MD
|
|
31
|
+
### ✅ Prerequisites
|
|
32
|
+
|
|
33
|
+
Three conditions must be met before the panel mounts:
|
|
34
|
+
|
|
35
|
+
1. **`UIManager` must be in the setup call.** It registers all platform web
|
|
36
|
+
components, including `<top-gis-properties-panel>` itself. Without it the element is
|
|
37
|
+
unknown to the browser and renders as an empty box.
|
|
38
|
+
|
|
39
|
+
2. **`ProjectManager.init(client)` must have resolved.** `GISManager` takes
|
|
40
|
+
the project origin from it: without it the 3D tiles and the vector layers
|
|
41
|
+
are placed around a default origin instead of the project's.
|
|
42
|
+
|
|
43
|
+
3. **`GISManager.init(client)` must have resolved**, after `ProjectManager`.
|
|
44
|
+
The panel itself only listens to selections, but
|
|
45
|
+
without `init` the manifest is never read, so there are no layers to
|
|
46
|
+
select. A panel created after a selection already exists shows that
|
|
47
|
+
selection straight away.
|
|
48
|
+
|
|
49
|
+
Both are satisfied by awaiting the two `init`s, in that order, before
|
|
50
|
+
appending `top-app` to the document, which is the pattern shown below.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
const client = PlatformClient.fromPlatformContext();
|
|
54
|
+
|
|
55
|
+
const { components } = (await client.setup(
|
|
56
|
+
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
57
|
+
{ uuid: UIManager.uuid },
|
|
58
|
+
{ uuid: ProjectManager.uuid },
|
|
59
|
+
{ uuid: GISManager.uuid },
|
|
60
|
+
)) as { components: OBC.Components };
|
|
61
|
+
|
|
62
|
+
components.get(UIManager).init();
|
|
63
|
+
|
|
64
|
+
// Order matters: GISManager reads the origin that ProjectManager has loaded.
|
|
65
|
+
await components.get(ProjectManager).init(client);
|
|
66
|
+
await components.get(GISManager).init(client);
|
|
67
|
+
|
|
68
|
+
/* MD
|
|
69
|
+
### 🖥️ Wiring the panel
|
|
70
|
+
|
|
71
|
+
Add `gis-properties-panel` as a named area. Next to the map is the usual
|
|
72
|
+
place: the user clicks a feature on the map or in the 3D view, and reads
|
|
73
|
+
its attributes in the column beside it. Selecting features on the map requires
|
|
74
|
+
`map-panel` (and, as it is documented there, see-through terrain).
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
const app = document.createElement("top-app") as unknown as App;
|
|
78
|
+
|
|
79
|
+
app.setup = (waitUntil) => {
|
|
80
|
+
waitUntil(
|
|
81
|
+
(async () => {
|
|
82
|
+
const fragments = components.get(OBC.FragmentsManager);
|
|
83
|
+
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
84
|
+
fragments.init(await FRAGS.toClassicWorker(workerUrl), { classicWorker: true });
|
|
85
|
+
})(),
|
|
86
|
+
"Fragments Core",
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return { components, client };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
app.elements = {
|
|
93
|
+
gis: () => html`<top-gis-panel></top-gis-panel>`,
|
|
94
|
+
viewer: () => html`<top-viewer><top-viewer-tools></top-viewer-tools></top-viewer>`,
|
|
95
|
+
map: () => html`<top-map-panel></top-map-panel>`,
|
|
96
|
+
properties: () => html`<top-gis-properties-panel></top-gis-properties-panel>`,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
app.layouts = {
|
|
100
|
+
main: {
|
|
101
|
+
label: "Main",
|
|
102
|
+
icon: "mdi:information-outline",
|
|
103
|
+
template: `"gis viewer map" 1fr "gis viewer properties" 1fr / 20rem 1fr 26rem`,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
app.layout = "main";
|
|
108
|
+
|
|
109
|
+
const container = document.getElementById("that-open-app") ?? document.body;
|
|
110
|
+
container.appendChild(app);
|
|
111
|
+
document.body.style.margin = "0";
|
|
112
|
+
|
|
113
|
+
/* MD
|
|
114
|
+
### 🤖 What the panel manages automatically
|
|
115
|
+
|
|
116
|
+
Once mounted, `gis-properties-panel` takes ownership of the following without
|
|
117
|
+
any further code from you:
|
|
118
|
+
|
|
119
|
+
**Selection tracking** — the panel subscribes to `GISManager.onFeatureSelected`,
|
|
120
|
+
so it shows the selected feature whether the click came from the 3D view, the
|
|
121
|
+
map, or your own call to `selectVectorFeature`.
|
|
122
|
+
|
|
123
|
+
**Attribute table** — the section is titled with the layer's name, shows the
|
|
124
|
+
geometry type, and lists every attribute of the feature as a name and a value.
|
|
125
|
+
|
|
126
|
+
**Filter** — a search box filters the table by attribute name or by visible
|
|
127
|
+
value.
|
|
128
|
+
|
|
129
|
+
**Value formatting** — numbers and text are shown as they are, booleans as
|
|
130
|
+
Yes/No, dates as `YYYY-MM-DD` (with the time when they have one) and complex
|
|
131
|
+
values as JSON. Empty values (null, blank text, invalid dates, non-finite
|
|
132
|
+
numbers) are shown as a dimmed dash, and are not matched by the filter.
|
|
133
|
+
|
|
134
|
+
**Empty state** — with no selection the panel explains that an element of a
|
|
135
|
+
vector layer has to be selected, and it goes back to that state when the
|
|
136
|
+
selection is cleared.
|
|
137
|
+
|
|
138
|
+
**Disconnection cleanup** — when the panel is removed from the DOM it
|
|
139
|
+
unsubscribes from the manager.
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
/* MD
|
|
143
|
+
### 🔀 One properties column for BIM and GIS
|
|
144
|
+
|
|
145
|
+
The panel only knows about GIS features. If your application has a single
|
|
146
|
+
properties column, switch what it shows from the selection events: the GIS
|
|
147
|
+
panel when a vector feature is selected, the BIM `top-properties-panel` when
|
|
148
|
+
a BIM element is. Keep both elements alive and swap them inside one stable
|
|
149
|
+
container.
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
const gis = components.get(GISManager);
|
|
153
|
+
|
|
154
|
+
const bimProperties = document.createElement("top-properties-panel");
|
|
155
|
+
const gisProperties = document.createElement("top-gis-properties-panel");
|
|
156
|
+
const column = document.createElement("div");
|
|
157
|
+
column.style.height = "100%";
|
|
158
|
+
column.append(bimProperties);
|
|
159
|
+
|
|
160
|
+
gis.onFeatureSelected.add((selection) => {
|
|
161
|
+
if (selection) column.replaceChildren(gisProperties);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
components.get(OBF.Highlighter).events.select.onHighlight.add(() => {
|
|
165
|
+
column.replaceChildren(bimProperties);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Render `column` from the `properties` element of the layout above.
|