diff --git a/.gitignore b/.gitignore
index 9340df38..ef23a405 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ ci/emsdk/
.idea
cmake-build-debug-wsl
clion-build-*
+cmake-build-*
static
.angular
node_modules
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5fcea767..2589b14b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -26,7 +26,7 @@ FetchContent_MakeAvailable(glm)
FetchContent_Declare(mapget
GIT_REPOSITORY "https://github.com/Klebert-Engineering/mapget"
- GIT_TAG "main"
+ GIT_TAG "addon-datasources"
GIT_SHALLOW ON)
FetchContent_MakeAvailable(mapget)
diff --git a/README.md b/README.md
index 41bfde44..e66839cd 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@ rule that matches it.
### Custom Style Declarations
It is possible to apply own custom styles easily.
-On build, Erdblick automatically picks up `.yaml` style files from `styles` directory (where you can drop your custom files)
+On build, Erdblick automatically picks up `.yaml` style files from `config/styles` directory (where you can drop your custom files)
and bundles them in `static/bundle/styles` (in case you are using a pre-built Erdblick distribution,
you can directly put your styles in `static/bundle/styles`).
@@ -50,7 +50,7 @@ For Erdblick to apply custom styles, it expects the following declarations for t
]
}
```
-where `url` field must be a path relative to `static/bundle/styles` and `id` is used to identify the particular style in GUI.
+where `url` field must be a path relative to `config/styles` and `id` is used to identify the particular style in GUI.
It is also possible to export and import styles in GUI. Styles imported this way will persist in the local storage of the browser.
diff --git a/VERSION b/VERSION
index a2a82601..80e1c8d5 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2024.2
\ No newline at end of file
+2024.3
\ No newline at end of file
diff --git a/angular.json b/angular.json
index 1d904f60..6d27c1bf 100644
--- a/angular.json
+++ b/angular.json
@@ -32,7 +32,7 @@
},
{
"glob": "**/*",
- "input": "styles",
+ "input": "config/styles",
"output": "/bundle/styles"
},
{
@@ -55,6 +55,7 @@
"node_modules/primeng/resources/themes/lara-light-blue/theme.css",
"node_modules/primeng/resources/primeng.min.css",
"node_modules/cesium/Build/Cesium/Widgets/widgets.css",
+ "node_modules/material-icons/iconfont/material-icons.css",
"erdblick_app/styles.scss"
],
"scripts": [
@@ -62,7 +63,8 @@
],
"customWebpackConfig": {
"path": "./webpack.config.js"
- }
+ },
+ "webWorkerTsConfig": "tsconfig.worker.json"
},
"configurations": {
"production": {
@@ -70,7 +72,7 @@
{
"type": "initial",
"maximumWarning": "500kb",
- "maximumError": "60mb"
+ "maximumError": "100mb"
},
{
"type": "anyComponentStyle",
@@ -126,7 +128,8 @@
"styles": [
"erdblick_app/styles.scss"
],
- "scripts": []
+ "scripts": [],
+ "webWorkerTsConfig": "tsconfig.worker.json"
}
}
}
diff --git a/build-ui.bash b/build-ui.bash
index e9677aef..86a44ae8 100755
--- a/build-ui.bash
+++ b/build-ui.bash
@@ -18,7 +18,7 @@ npm run lint
if [[ -z "$NG_DEVELOP" ]]; then
npm run build -- -c production
else
- npm run build
+ npm run build --watch
fi
exit 0
\ No newline at end of file
diff --git a/ci/00_linux_setup.bash b/ci/00_linux_setup.bash
index ead6317a..0a1912b7 100755
--- a/ci/00_linux_setup.bash
+++ b/ci/00_linux_setup.bash
@@ -4,6 +4,7 @@ ci_dir="$(realpath ${BASH_SOURCE[0]} | xargs -I{} dirname {})"
echo "Setting up Emscripten in: $ci_dir"
cd "$ci_dir"
+export PATH=$PATH:"$ci_dir/../node_modules/.bin/"
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
git pull
diff --git a/ci/10_linux_build.bash b/ci/10_linux_build.bash
index 6f530fb2..cafb34be 100755
--- a/ci/10_linux_build.bash
+++ b/ci/10_linux_build.bash
@@ -7,6 +7,7 @@ source "$ci_dir/emsdk/emsdk_env.sh"
cd "$ci_dir/.."
export EMSCRIPTEN="$ci_dir/emsdk/upstream/emscripten"
+export PATH=$PATH:"$(pwd)/node_modules/.bin/"
rm -rf build && mkdir build
cd build
diff --git a/ci/20_linux_rebuild.bash b/ci/20_linux_rebuild.bash
index ebb72791..3764c07c 100755
--- a/ci/20_linux_rebuild.bash
+++ b/ci/20_linux_rebuild.bash
@@ -6,6 +6,7 @@ ci_dir="$(realpath ${BASH_SOURCE[0]} | xargs -I{} dirname {})"
source "$ci_dir/emsdk/emsdk_env.sh"
export EMSCRIPTEN="$ci_dir/emsdk/upstream/emscripten"
+export PATH=$PATH:"$ci_dir/../node_modules/.bin/"
cd "$ci_dir/../build"
cmake --build . -- -j
diff --git a/config/.gitignore b/config/.gitignore
deleted file mode 100644
index 047c63a6..00000000
--- a/config/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-# Keep this directory to be filled with plugin configurations
\ No newline at end of file
diff --git a/config/styles/default-style.yaml b/config/styles/default-style.yaml
new file mode 100644
index 00000000..1fbe89be
--- /dev/null
+++ b/config/styles/default-style.yaml
@@ -0,0 +1,15 @@
+name: DefaultStyle
+version: 1.0
+rules:
+ - geometry: ["mesh", "polygon"]
+ color: teal
+ opacity: 0.8
+ - geometry: ["point", "line"]
+ color: moccasin
+ opacity: 1.0
+ width: 1.0
+ - geometry: ["point", "line", "mesh", "polygon"]
+ color: red
+ opacity: 1.0
+ width: 4.0
+ mode: highlight
diff --git a/erdblick_app/app/app.component.ts b/erdblick_app/app/app.component.ts
index 5793382f..90b091b7 100644
--- a/erdblick_app/app/app.component.ts
+++ b/erdblick_app/app/app.component.ts
@@ -4,7 +4,6 @@ import {JumpTargetService} from "./jump.service";
import {MapService} from "./map.service";
import {ActivatedRoute, NavigationEnd, Params, Router} from "@angular/router";
import {ParametersService} from "./parameters.service";
-import {OverlayPanel} from "primeng/overlaypanel";
import {StyleService} from "./style.service";
import {filter} from "rxjs";
@@ -13,18 +12,11 @@ import {filter} from "rxjs";
template: `
-
-
-
-
-
-
-
-
+
+
+
{{ title }} {{ version }}
@@ -78,27 +70,15 @@ export class AppComponent {
}
const entries = [...Object.entries(parameters)];
entries.forEach(entry => entry[1] = JSON.stringify(entry[1]));
- this.updateQueryParams(Object.fromEntries(entries));
+ this.updateQueryParams(Object.fromEntries(entries), this.parametersService.replaceUrl);
});
}
- toggleSearchOverlay(value: string, searchOverlay: OverlayPanel, event: any) {
- if (value) {
- searchOverlay.show(event);
- return;
- }
- searchOverlay.toggle(event);
- }
-
- setSearchTargetValue(value: string) {
- this.jumpToTargetService.targetValueSubject.next(value);
- }
-
- updateQueryParams(params: Params): void {
+ updateQueryParams(params: Params, replaceUrl: boolean): void {
this.router.navigate([], {
queryParams: params,
queryParamsHandling: 'merge',
- replaceUrl: true
+ replaceUrl: replaceUrl
});
}
}
diff --git a/erdblick_app/app/app.module.ts b/erdblick_app/app/app.module.ts
index 269b8d3a..4fb31ef9 100644
--- a/erdblick_app/app/app.module.ts
+++ b/erdblick_app/app/app.module.ts
@@ -21,7 +21,7 @@ import {MessageService} from "primeng/api";
import {InputNumberModule} from "primeng/inputnumber";
import {FieldsetModule} from "primeng/fieldset";
import {InfoMessageService} from "./info.service";
-import {SearchMenuComponent} from "./search-menu.component";
+import {SearchPanelComponent} from "./search.panel.component";
import {JumpTargetService} from "./jump.service";
import {MapService} from "./map.service";
import {InputSwitchModule} from "primeng/inputswitch";
@@ -35,7 +35,20 @@ import {PreferencesComponent} from "./preferences.component";
import {FileUploadModule} from "primeng/fileupload";
import {EditorComponent} from "./editor.component";
import {ErdblickViewComponent} from "./view.component";
+import {CoordinatesPanelComponent} from "./coordinates.panel.component";
import {initializeLibrary} from "./wasm";
+import {CheckboxModule} from "primeng/checkbox";
+import {InputTextModule} from "primeng/inputtext";
+import {SidePanelService} from "./sidepanel.service";
+import {MenuModule} from "primeng/menu";
+import {CardModule} from "primeng/card";
+import {CoordinatesService} from "./coordinates.service";
+import {FeatureSearchComponent} from "./feature.search.component";
+import {ColorPickerModule} from "primeng/colorpicker";
+import {ListboxModule} from "primeng/listbox";
+import {FeatureSearchService} from "./feature.search.service";
+import {ClipboardService} from "./clipboard.service";
+import {MultiSelectModule} from "primeng/multiselect";
export function initializeServices(styleService: StyleService, mapService: MapService) {
return async () => {
@@ -48,12 +61,14 @@ export function initializeServices(styleService: StyleService, mapService: MapSe
@NgModule({
declarations: [
AppComponent,
- SearchMenuComponent,
+ SearchPanelComponent,
MapPanelComponent,
InspectionPanelComponent,
PreferencesComponent,
EditorComponent,
- ErdblickViewComponent
+ ErdblickViewComponent,
+ CoordinatesPanelComponent,
+ FeatureSearchComponent
],
imports: [
BrowserModule,
@@ -76,7 +91,14 @@ export function initializeServices(styleService: StyleService, mapService: MapSe
FieldsetModule,
InputSwitchModule,
SliderModule,
- FileUploadModule
+ FileUploadModule,
+ CheckboxModule,
+ InputTextModule,
+ MenuModule,
+ CardModule,
+ ColorPickerModule,
+ ListboxModule,
+ MultiSelectModule
],
providers: [
{
@@ -91,6 +113,10 @@ export function initializeServices(styleService: StyleService, mapService: MapSe
JumpTargetService,
InspectionService,
ParametersService,
+ SidePanelService,
+ CoordinatesService,
+ FeatureSearchService,
+ ClipboardService
],
bootstrap: [AppComponent]
})
diff --git a/erdblick_app/app/cesium.ts b/erdblick_app/app/cesium.ts
index 663881a9..fe130db8 100644
--- a/erdblick_app/app/cesium.ts
+++ b/erdblick_app/app/cesium.ts
@@ -30,10 +30,28 @@ export type ScreenSpaceEventType = Cesium.ScreenSpaceEventType;
export const ScreenSpaceEventType = Cesium.ScreenSpaceEventType;
export type UrlTemplateImageryProvider = Cesium.UrlTemplateImageryProvider;
export const UrlTemplateImageryProvider = Cesium.UrlTemplateImageryProvider;
+export type Rectangle = Cesium.Rectangle;
+export const Rectangle = Cesium.Rectangle;
+export type HeightReference = Cesium.HeightReference;
+export const HeightReference = Cesium.HeightReference;
+export type LabelStyle = Cesium.LabelStyle;
+export const LabelStyle = Cesium.LabelStyle;
+export type VerticalOrigin = Cesium.VerticalOrigin;
+export const VerticalOrigin = Cesium.VerticalOrigin;
+export type HorizontalOrigin = Cesium.HorizontalOrigin;
+export const HorizontalOrigin = Cesium.HorizontalOrigin;
+export type DistanceDisplayCondition = Cesium.DistanceDisplayCondition;
+export const DistanceDisplayCondition = Cesium.DistanceDisplayCondition;
+export type CallbackProperty = Cesium.CallbackProperty;
+export const CallbackProperty = Cesium.CallbackProperty;
export type Viewer = Cesium.Viewer;
export const Viewer = Cesium.Viewer;
export type PrimitiveCollection = Cesium.PrimitiveCollection;
export const PrimitiveCollection = Cesium.PrimitiveCollection;
+export type BillboardCollection = Cesium.BillboardCollection;
+export const BillboardCollection = Cesium.BillboardCollection;
+export type PointPrimitiveCollection = Cesium.PointPrimitiveCollection;
+export const PointPrimitiveCollection = Cesium.PointPrimitiveCollection;
export type Entity = Cesium.Entity;
export const Entity = Cesium.Entity;
export type Camera = Cesium.Camera;
@@ -41,4 +59,4 @@ export const Camera = Cesium.Camera;
// Math is a namespace.
-export const Math = Cesium.Math;
+export const CesiumMath = Cesium.Math;
diff --git a/erdblick_app/app/clipboard.service.ts b/erdblick_app/app/clipboard.service.ts
new file mode 100644
index 00000000..45979256
--- /dev/null
+++ b/erdblick_app/app/clipboard.service.ts
@@ -0,0 +1,24 @@
+import {Injectable} from "@angular/core";
+import {InfoMessageService} from "./info.service";
+
+
+@Injectable()
+export class ClipboardService {
+
+ constructor(private messageService: InfoMessageService) {}
+
+ copyToClipboard(text: string) {
+ try {
+ navigator.clipboard.writeText(text).then(
+ () => {
+ this.messageService.showSuccess("Copied content to clipboard!");
+ },
+ () => {
+ this.messageService.showError("Could not copy content to clipboard.");
+ }
+ );
+ } catch (error) {
+ console.error(error);
+ }
+ }
+}
\ No newline at end of file
diff --git a/erdblick_app/app/coordinates.panel.component.ts b/erdblick_app/app/coordinates.panel.component.ts
new file mode 100644
index 00000000..a6153a2f
--- /dev/null
+++ b/erdblick_app/app/coordinates.panel.component.ts
@@ -0,0 +1,232 @@
+import {Component} from "@angular/core";
+import {CoordinatesService} from "./coordinates.service";
+import {MapService} from "./map.service";
+import {ParametersService} from "./parameters.service";
+import {CesiumMath} from "./cesium";
+import {ClipboardService} from "./clipboard.service";
+import {coreLib} from "./wasm";
+
+interface PanelOption {
+ name: string,
+ level?: number
+}
+
+@Component({
+ selector: "coordinates-panel",
+ template: `
+
+
+ {{ markerButtonIcon }}
+
+
+
+
+
+ WGS84:
+ {{ longitude.toFixed(8) }}
+ {{ latitude.toFixed(8) }}
+
+
+
+ {{ coords.key }}:
+ {{ component }}
+
+
+
+
+ {{ tileId.key }}
+ :
+ {{ tileId.value }}
+
+
+
+
+ {{ tileId.key }}
+ :
+ {{ tileId.value }}
+
+
+
+
+
+ loupe
+
+
+ `,
+ styles: [`
+ .name-span {
+ cursor: pointer;
+ text-decoration: underline dotted;
+ text-wrap: nowrap;
+ }
+
+ .coord-span {
+ text-align: right;
+ font-family: monospace;
+ }
+ `]
+})
+export class CoordinatesPanelComponent {
+
+ longitude: number = 0;
+ latitude: number = 0;
+ isMarkerEnabled: boolean = false;
+ markerPosition: {x: number, y: number} | null = null;
+ auxiliaryCoordinates: Map> = new Map>();
+ mapgetTileIds: Map = new Map();
+ auxiliaryTileIds: Map = new Map();
+ markerButtonIcon: string = "location_off";
+ markerButtonTooltip: string = "Enable marker placement";
+ displayOptions: Array = [{name: "WGS84"}];
+ selectedOptions: Array = [{name: "WGS84"}];
+
+ constructor(public mapService: MapService,
+ public coordinatesService: CoordinatesService,
+ public clipboardService: ClipboardService,
+ public parametersService: ParametersService) {
+ for (let level = 0; level < 15; level++) {
+ this.displayOptions.push({name: `Mapget TileId (level ${level})`});
+ }
+ this.parametersService.parameters.subscribe(parameters => {
+ this.isMarkerEnabled = parameters.marker;
+ if (parameters.markedPosition.length == 2) {
+ this.longitude = parameters.markedPosition[0];
+ this.latitude = parameters.markedPosition[1];
+ if (this.isMarkerEnabled) {
+ this.markerPosition = {x: this.longitude, y: this.latitude};
+ this.markerButtonIcon = "wrong_location";
+ this.markerButtonTooltip = "Reset marker";
+ }
+
+ if (this.coordinatesService.auxiliaryCoordinatesFun) {
+ this.auxiliaryCoordinates =
+ this.coordinatesService.auxiliaryCoordinatesFun(this.longitude, this.latitude).reduce(
+ (map: Map>, [key, value]: [string, Array]) => {
+ map.set(key, value);
+ return map;
+ }, new Map>());
+ }
+ for (let level = 0; level < 15; level++) {
+ this.mapgetTileIds.set(`Mapget TileId (level ${level})`,
+ coreLib.getTileIdFromPosition(this.longitude, this.latitude, level));
+ }
+ if (this.coordinatesService.auxillaryTileIdsFun) {
+ for (let level = 0; level < 15; level++) {
+ const levelData: Map =
+ this.coordinatesService.auxillaryTileIdsFun(this.longitude, this.latitude, level).reduce(
+ (map: Map, [key, value]: [string, bigint]) => {
+ map.set(key, value);
+ return map;
+ }, new Map());
+
+ levelData.forEach((value, key) => {
+ this.auxiliaryTileIds.set(`${key} (level ${level})`, value);
+ });
+ }
+ }
+ } else {
+ if (this.isMarkerEnabled) {
+ this.markerButtonIcon = "location_on";
+ this.markerButtonTooltip = "Disable marker placement";
+ }
+ this.markerPosition = null;
+ }
+ });
+ this.coordinatesService.mouseMoveCoordinates.subscribe(coordinates => {
+ if (!this.markerPosition && coordinates) {
+ this.longitude = CesiumMath.toDegrees(coordinates.longitude);
+ this.latitude = CesiumMath.toDegrees(coordinates.latitude);
+ if (this.coordinatesService.auxiliaryCoordinatesFun) {
+ this.auxiliaryCoordinates =
+ this.coordinatesService.auxiliaryCoordinatesFun(this.longitude, this.latitude).reduce(
+ (map: Map>, [key, value]: [string, Array]) => {
+ map.set(key, value);
+ return map;
+ }, new Map>());
+ for (const key of this.auxiliaryCoordinates.keys()) {
+ if (!this.displayOptions.some(val => val.name == key)) {
+ this.displayOptions.push({name: `${key}`});
+ }
+ }
+ }
+ for (let level = 0; level < 15; level++) {
+ this.mapgetTileIds.set(`Mapget TileId (level ${level})`,
+ coreLib.getTileIdFromPosition(this.longitude, this.latitude, level));
+ }
+ if (this.coordinatesService.auxillaryTileIdsFun) {
+ for (let level = 0; level < 15; level++) {
+ const levelData: Map =
+ this.coordinatesService.auxillaryTileIdsFun(this.longitude, this.latitude, level).reduce(
+ (map: Map, [key, value]: [string, bigint]) => {
+ map.set(key, value);
+ return map;
+ }, new Map());
+
+ levelData.forEach((value, key) => {
+ this.auxiliaryTileIds.set(`${key} (level ${level})`, value);
+ });
+ }
+ for (const key of this.auxiliaryTileIds.keys()) {
+ if (!this.displayOptions.some(val => val.name == key)) {
+ this.displayOptions.push({name: key});
+ }
+ }
+ }
+ }
+ });
+ }
+
+ toggleMarker() {
+ if (!this.isMarkerEnabled) {
+ this.isMarkerEnabled = true;
+ this.parametersService.setMarkerState(true);
+ this.parametersService.setMarkerPosition(null);
+ this.markerButtonIcon = "location_on";
+ this.markerButtonTooltip = "Disable marker placement";
+ } else if (!this.markerPosition) {
+ this.isMarkerEnabled = false;
+ this.parametersService.setMarkerState(false);
+ this.markerButtonIcon = "location_off";
+ this.markerButtonTooltip = "Enable marker placement";
+ } else if (this.markerPosition) {
+ this.isMarkerEnabled = true;
+ this.parametersService.setMarkerState(true);
+ this.parametersService.setMarkerPosition(null);
+ this.markerButtonIcon = "location_on";
+ this.markerButtonTooltip = "Disable marker placement";
+ } else {
+ this.isMarkerEnabled = true;
+ this.markerPosition = null;
+ this.parametersService.setMarkerState(true);
+ this.parametersService.setMarkerPosition(null);
+ this.markerButtonIcon = "wrong_location";
+ this.markerButtonTooltip = "Reset marker";
+ }
+ }
+
+ copyToClipboard(coordArray: Array) {
+ this.clipboardService.copyToClipboard(coordArray.join(" "));
+ }
+
+ // updateDisplayedOptions(key: string, value: boolean) {
+ // this.displayOptions.set(key, value);
+ // }
+
+ isSelectedOption(name: string) {
+ return this.selectedOptions.some(val => val.name == name);
+ }
+
+ selectedOptionsChanged() {
+ // TODO: Save selected label options to parameterService.p().markedPositionLabels
+ }
+}
diff --git a/erdblick_app/app/coordinates.service.ts b/erdblick_app/app/coordinates.service.ts
new file mode 100644
index 00000000..2ae9e954
--- /dev/null
+++ b/erdblick_app/app/coordinates.service.ts
@@ -0,0 +1,56 @@
+import {Injectable} from "@angular/core";
+import {ParametersService} from "./parameters.service";
+import {BehaviorSubject} from "rxjs";
+import {Cartographic} from "./cesium";
+import {HttpClient} from "@angular/common/http";
+
+
+@Injectable()
+export class CoordinatesService {
+ mouseMoveCoordinates: BehaviorSubject = new BehaviorSubject(null);
+ mouseClickCoordinates: BehaviorSubject = new BehaviorSubject(null);
+ auxiliaryCoordinatesFun: Function | null = null;
+ auxillaryTileIdsFun: Function | null = null;
+
+ constructor(private httpClient: HttpClient,
+ public parametersService: ParametersService) {
+ this.httpClient.get("/config.json", {responseType: 'json'}).subscribe({
+ next: (data: any) => {
+ try {
+ if (data && data["extensionModules"] && data["extensionModules"]["jumpTargets"]) {
+ let jumpTargetsConfig = data["extensionModules"]["jumpTargets"];
+ if (jumpTargetsConfig !== undefined) {
+ // Using string interpolation so webpack can trace imports from the location
+ import(`../../config/${jumpTargetsConfig}.js`).then((plugin) => {
+ const { getAuxCoordinates, getAuxTileIds } = plugin;
+ if (getAuxCoordinates) {
+ this.auxiliaryCoordinatesFun = getAuxCoordinates;
+ } else {
+ console.error('Function getAuxCoordinates not found in the plugin.');
+ }
+ if (getAuxTileIds) {
+ this.auxillaryTileIdsFun = getAuxTileIds;
+ } else {
+ console.error('Function getAuxTileIds not found in the plugin.');
+ }
+ }).catch((error) => {
+ console.error(error);
+ });
+ }
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ },
+ error: error => {
+ console.error(error);
+ }
+ });
+
+ this.mouseClickCoordinates.subscribe(position => {
+ this.parametersService.setMarkerPosition(position);
+ });
+ }
+
+
+}
\ No newline at end of file
diff --git a/erdblick_app/app/editor.component.ts b/erdblick_app/app/editor.component.ts
index 277b935a..27dbfadf 100644
--- a/erdblick_app/app/editor.component.ts
+++ b/erdblick_app/app/editor.component.ts
@@ -110,15 +110,14 @@ export class EditorComponent implements AfterViewInit, OnDestroy {
this.renderer.removeChild(this.editorRef.nativeElement, child);
}
this.editorView = new EditorView({
- state: this.createEditorState(),
+ state: this.createEditorState(styleId),
parent: this.editorRef.nativeElement
});
}
});
}
- createEditorState() {
- const styleId = this.styleService.selectedStyleIdForEditing.getValue();
+ createEditorState(styleId: string) {
if (this.styleService.styleData.has(styleId)) {
this.styleData = `${this.styleService.styleData.get(styleId)!.data}\n\n\n\n\n`;
} else {
diff --git a/erdblick_app/app/feature.search.component.ts b/erdblick_app/app/feature.search.component.ts
new file mode 100644
index 00000000..840e7c4b
--- /dev/null
+++ b/erdblick_app/app/feature.search.component.ts
@@ -0,0 +1,82 @@
+import {ChangeDetectorRef, Component, Input} from "@angular/core";
+import {FeatureSearchService} from "./feature.search.service";
+import {JumpTargetService} from "./jump.service";
+import {InspectionService} from "./inspection.service";
+import {MapService} from "./map.service";
+import {SidePanelService, SidePanelState} from "./sidepanel.service";
+
+@Component({
+ selector: "feature-search",
+ template: `
+
+ `,
+ styles: [``]
+})
+export class FeatureSearchComponent {
+ isPanelVisible: boolean = false;
+ results: Array = [];
+ traceResults: Array = [];
+ selectedResult: any;
+ percentDone: number = 0;
+
+ constructor(public searchService: FeatureSearchService,
+ public jumpService: JumpTargetService,
+ public mapService: MapService,
+ public inspectionService: InspectionService,
+ public sidePanelService: SidePanelService) {
+ this.sidePanelService.observable().subscribe(panel=> {
+ this.isPanelVisible = panel == SidePanelState.FEATURESEARCH || this.isPanelVisible;
+ });
+ this.searchService.isFeatureSearchActive.subscribe(value => {
+ this.results = [];
+ });
+ this.searchService.searchUpdates.subscribe(tileResult => {
+ for (const [mapTileKey, featureId, _] of tileResult.matches) {
+ // TODO: Also show info from the mapTileKey
+ const mapId = mapTileKey.split(':')[1]
+ this.results = [...this.results, {label: `${featureId}`, mapId: mapId, featureId: featureId}]
+ }
+ });
+ this.searchService.progress.subscribe(value => {
+ this.percentDone = value;
+ });
+ }
+
+ selectResult(event: any) {
+ if (event.value.mapId && event.value.featureId) {
+ this.jumpService.highlightFeature(event.value.mapId, event.value.featureId).then();
+ this.mapService.focusOnFeature(this.inspectionService.selectedFeature!)
+ }
+ }
+}
\ No newline at end of file
diff --git a/erdblick_app/app/feature.search.service.ts b/erdblick_app/app/feature.search.service.ts
new file mode 100644
index 00000000..6ae25896
--- /dev/null
+++ b/erdblick_app/app/feature.search.service.ts
@@ -0,0 +1,177 @@
+import {Injectable} from "@angular/core";
+import {Subject} from "rxjs";
+import {MapService} from "./map.service";
+import {SearchResultForTile, SearchWorkerTask} from "./featurefilter.worker";
+import {Color, BillboardCollection, Cartesian2, Cartesian3} from "./cesium";
+import {FeatureTile} from "./features.model";
+import {uint8ArrayFromWasm} from "./wasm";
+
+
+@Injectable({providedIn: 'root'})
+export class FeatureSearchService {
+
+ currentQuery: string = ""
+ workers: Array = []
+ visualization: BillboardCollection = new BillboardCollection();
+ visualizationChanged: Subject = new Subject();
+ resultsPerTile: Map = new Map();
+ workQueue: Array = [];
+ totalTiles: number = 0;
+ doneTiles: number = 0;
+ searchUpdates: Subject = new Subject();
+ isFeatureSearchActive: Subject = new Subject();
+ pointColor: string = "#ff69b4";
+ timeElapsed: string = this.formatTime(0); // TODO: Set
+ totalFeatureCount: number = 0;
+ progress: Subject = new Subject();
+
+ private startTime: number = 0;
+ private endTime: number = 0;
+
+ markerGraphics = () => {
+ const svg = ``
+ return `data:image/svg+xml;base64,${btoa(svg)}`;
+ };
+
+ constructor(private mapService: MapService) {
+ // Instantiate workers.
+ const maxWorkers = navigator.hardwareConcurrency || 4;
+ for (let i = 0; i < maxWorkers; i++) {
+ const worker = new Worker(new URL('./featurefilter.worker', import.meta.url));
+ this.workers.push(worker);
+ worker.onmessage = (ev: MessageEvent) => {
+ this.addSearchResult(ev.data);
+ if (this.workQueue.length > 0) {
+ const tileToProcess = this.workQueue.pop()!;
+ this.scheduleTileForWorker(worker, tileToProcess);
+ }
+ };
+ }
+ }
+
+ run(query: string) {
+ // if (query == this.currentQuery) {
+ // return;
+ // }
+
+ // Clear current work queue/visualizations. TODO: Move towards
+ // an update-like function which is invoked when the user
+ // moves the viewport to run differential search on newly visible tiles.
+ this.clear();
+ this.currentQuery = query;
+ this.startTime = Date.now();
+
+ // Fill up work queue and start processing.
+ for (const [_, tile] of this.mapService.loadedTileLayers) {
+ this.workQueue.push(tile);
+ }
+ this.totalTiles = this.workQueue.length;
+ this.isFeatureSearchActive.next(true);
+
+ // Send a task to each worker to start processing.
+ // Further tasks will be picked up in the worker's
+ // onMessage callback.
+ for (const worker of this.workers) {
+ const tile = this.workQueue.pop();
+ if (tile) {
+ this.scheduleTileForWorker(worker, tile);
+ }
+ }
+ }
+
+ stop() {
+ this.workQueue = [];
+ this.endTime = Date.now();
+ this.timeElapsed = this.formatTime(this.endTime - this.startTime);
+ }
+
+ clear() {
+ this.stop();
+ this.currentQuery = "";
+ this.visualization.removeAll();
+ this.resultsPerTile.clear();
+ this.workQueue = [];
+ this.totalTiles = 0;
+ this.doneTiles = 0;
+ this.progress.next(0);
+ this.isFeatureSearchActive.next(false);
+ this.totalFeatureCount = 0;
+ this.startTime = 0;
+ this.endTime = 0;
+ this.timeElapsed = this.formatTime(0);
+ this.visualizationChanged.next();
+ }
+
+ private addSearchResult(tileResult: SearchResultForTile) {
+ // Ignore results that are not related to the ongoing query.
+ if (tileResult.query != this.currentQuery) {
+ return;
+ }
+
+ // Add visualizations and register the search result.
+ if (tileResult.matches.length) {
+ let mapTileKey = tileResult.matches[0][0];
+ this.resultsPerTile.set(mapTileKey, tileResult);
+
+ tileResult.billboardPrimitiveIndices = [];
+ for (const [_, __, position] of tileResult.matches) {
+ tileResult.billboardPrimitiveIndices.push(this.visualization.length);
+ this.visualization.add({
+ position: position,
+ image: this.markerGraphics(),
+ width: 32,
+ height: 32,
+ pixelOffset: new Cartesian2(0, -10),
+ eyeOffset: new Cartesian3(0, 0, -100)
+ });
+ }
+ }
+
+ // Broadcast the search progress.
+ ++this.doneTiles;
+ this.progress.next(this.doneTiles/this.totalTiles * 100 | 0);
+ this.endTime = Date.now();
+ this.timeElapsed = this.formatTime(this.endTime - this.startTime);
+ this.totalFeatureCount += tileResult.numFeatures;
+ this.searchUpdates.next(tileResult);
+ this.updatePointColor();
+ this.visualizationChanged.next();
+ }
+
+ private scheduleTileForWorker(worker: Worker, tileToProcess: FeatureTile) {
+ worker.postMessage({
+ tileBlob: tileToProcess.tileFeatureLayerBlob as Uint8Array,
+ fieldDictBlob: uint8ArrayFromWasm((buf) => {
+ this.mapService.tileParser?.getFieldDict(buf, tileToProcess.nodeId)
+ })!,
+ query: this.currentQuery,
+ dataSourceInfo: uint8ArrayFromWasm((buf) => {
+ this.mapService.tileParser?.getDataSourceInfo(buf, tileToProcess.mapName)
+ })!,
+ nodeId: tileToProcess.nodeId
+ } as SearchWorkerTask);
+ }
+
+ updatePointColor() {
+ const color = Color.fromCssColorString(this.pointColor);
+ for (let i = 0; i < this.visualization.length; ++i) {
+ this.visualization.get(i).color = color;
+ }
+ this.visualizationChanged.next();
+ }
+
+ private formatTime(milliseconds: number): string {
+ const mseconds = Math.floor(milliseconds % 1000);
+ const seconds = Math.floor((milliseconds / 1000) % 60);
+ const minutes = Math.floor((milliseconds / 60000) % 60);
+ const hours = Math.floor((milliseconds / 3600000) % 24);
+
+ return `${hours ? `${hours}h ` : ''}
+ ${minutes ? `${minutes}m ` : ''}
+ ${seconds ? `${seconds}s ` : ''}
+ ${mseconds ? `${mseconds}ms` : ''}`.trim() || "0ms";
+ }
+}
diff --git a/erdblick_app/app/featurefilter.worker.ts b/erdblick_app/app/featurefilter.worker.ts
new file mode 100644
index 00000000..b4ef2a9a
--- /dev/null
+++ b/erdblick_app/app/featurefilter.worker.ts
@@ -0,0 +1,44 @@
+import {coreLib, initializeLibrary, uint8ArrayToWasm} from "./wasm";
+import {TileFeatureLayer} from "../../build/libs/core/erdblick-core";
+
+export interface SearchWorkerTask {
+ tileBlob: Uint8Array;
+ fieldDictBlob: Uint8Array;
+ query: string;
+ dataSourceInfo: Uint8Array;
+ nodeId: string;
+}
+
+export interface SearchResultForTile {
+ query: string;
+ numFeatures: number;
+ matches: Array<[string, string, [number, number, number]]>; // Array of (MapTileKey, FeatureId, (x, y, z))
+ billboardPrimitiveIndices?: Array; // Used by search service for visualization.
+}
+
+addEventListener('message', async ({data}) => {
+ // Initialize WASM if not already done.
+ await initializeLibrary();
+
+ // Parse the tile.
+ let task = data as SearchWorkerTask;
+ let parser = new coreLib.TileLayerParser();
+ uint8ArrayToWasm(data => parser.setDataSourceInfo(data), task.dataSourceInfo);
+ uint8ArrayToWasm(data => parser.addFieldDict(data), task.fieldDictBlob);
+ let tile: TileFeatureLayer = uint8ArrayToWasm(data => parser.readTileFeatureLayer(data), task.tileBlob);
+ let numFeatures = tile.numFeatures();
+
+ // Get the query results from the tile.
+ let search = new coreLib.FeatureLayerSearch(tile);
+ let matchingFeatures = search.filter(task.query);
+ search.delete();
+ tile.delete();
+
+ // Post result back to the main thread.
+ let result: SearchResultForTile = {
+ query: task.query,
+ numFeatures: numFeatures,
+ matches: matchingFeatures
+ };
+ postMessage(result);
+});
diff --git a/erdblick_app/app/features.model.ts b/erdblick_app/app/features.model.ts
index 24070fc0..810927d5 100644
--- a/erdblick_app/app/features.model.ts
+++ b/erdblick_app/app/features.model.ts
@@ -10,15 +10,15 @@ import {TileLayerParser, TileFeatureLayer} from '../../build/libs/core/erdblick-
* WASM TileFeatureLayer, use the peek()-function.
*/
export class FeatureTile {
- // public:
id: string;
+ nodeId: string;
mapName: string;
layerName: string;
tileId: bigint;
numFeatures: number;
private parser: TileLayerParser;
preventCulling: boolean;
- private readonly tileFeatureLayerBlob: any;
+ public readonly tileFeatureLayerBlob: any;
disposed: boolean;
/**
@@ -32,6 +32,7 @@ export class FeatureTile {
return parser.readTileLayerMetadata(wasmBlob);
}, tileFeatureLayerBlob);
this.id = mapTileMetadata.id;
+ this.nodeId = mapTileMetadata.nodeId;
this.mapName = mapTileMetadata.mapName;
this.layerName = mapTileMetadata.layerName;
this.tileId = mapTileMetadata.tileId;
@@ -122,6 +123,10 @@ export class FeatureTile {
return await FeatureTile.peekMany(tiles, cb, parsedTiles);
});
}
+
+ level() {
+ return Number(this.tileId & BigInt(0xffff));
+ }
}
/**
@@ -130,7 +135,7 @@ export class FeatureTile {
* possible to access the WASM feature view in a memory-safe way.
*/
export class FeatureWrapper {
- private readonly index: number;
+ public readonly index: number;
public featureTile: FeatureTile;
/**
@@ -153,7 +158,7 @@ export class FeatureWrapper {
if (this.featureTile.disposed) {
throw new Error(`Unable to access feature of deleted layer ${this.featureTile.id}!`);
}
- return this.featureTile.peek((tileFeatureLayer: any) => {
+ return this.featureTile.peek((tileFeatureLayer: TileFeatureLayer) => {
let feature = tileFeatureLayer.at(this.index);
let result = null;
if (callback) {
@@ -163,4 +168,11 @@ export class FeatureWrapper {
return result;
});
}
+
+ equals(other: FeatureWrapper | null): boolean {
+ if (!other) {
+ return false;
+ }
+ return this.featureTile.id == other.featureTile.id && this.index == other.index;
+ }
}
diff --git a/erdblick_app/app/inspection.panel.component.ts b/erdblick_app/app/inspection.panel.component.ts
index 177e4bbd..306712f8 100644
--- a/erdblick_app/app/inspection.panel.component.ts
+++ b/erdblick_app/app/inspection.panel.component.ts
@@ -1,7 +1,12 @@
-import {Component, OnInit} from "@angular/core";
-import {InfoMessageService} from "./info.service";
-import {TreeNode, TreeTableNode} from "primeng/api";
+import {Component, OnInit, ViewChild} from "@angular/core";
+import {MenuItem, TreeNode, TreeTableNode} from "primeng/api";
import {InspectionService} from "./inspection.service";
+import {JumpTargetService} from "./jump.service";
+import {Menu} from "primeng/menu";
+import {MapService} from "./map.service";
+import {distinctUntilChanged, filter} from "rxjs";
+import {coreLib} from "./wasm";
+import {ClipboardService} from "./clipboard.service";
interface Column {
field: string;
@@ -11,16 +16,42 @@ interface Column {
@Component({
selector: 'inspection-panel',
template: `
-
- {{inspectionService.selectedFeatureIdText}}
+ {{ inspectionService.selectedFeatureIdName }}
+
@@ -28,40 +59,42 @@ interface Column {
-
-
-
-
-
+ |
+
-
+
- {{ rowData[col.field] }}
+ {{ rowData['key'] }}
+
+ |
+
+
+
+ {{ rowData['value'] }}
+
+
+
+
+
|
- No data found. |
+ No entries found. |
@@ -69,8 +102,42 @@ interface Column {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`,
styles: [`
+ .section-style {
+ background-color: gainsboro;
+ margin-top: 1em;
+ }
+
+ .feature-id-style {
+ cursor: pointer;
+ text-decoration: underline dotted;
+ font-style: italic;
+ }
+
@media only screen and (max-width: 56em) {
.resizable-container-expanded {
height: calc(100vh - 3em);;
@@ -85,39 +152,41 @@ export class InspectionPanelComponent implements OnInit {
cols: Column[] = [];
isExpanded: boolean = false;
tooltipOptions = {
- showDelay: 1500,
+ showDelay: 1000,
autoHide: false
};
+ filterByKeys = true;
+ filterByValues = true;
+ filterOnlyFeatureIds = false;
+ filterGeometryEntries = false;
- constructor(private messageService: InfoMessageService,
- public inspectionService: InspectionService) {
- this.inspectionService.featureTree.subscribe((tree: string) => {
+ @ViewChild('inspectionMenu') inspectionMenu!: Menu;
+ inspectionMenuItems: MenuItem[] | undefined;
+ inspectionMenuVisible: boolean = false;
+
+ constructor(private clipboardService: ClipboardService,
+ public inspectionService: InspectionService,
+ public jumpService: JumpTargetService,
+ public mapService: MapService) {
+ this.inspectionService.featureTree.pipe(distinctUntilChanged()).subscribe((tree: string) => {
this.jsonTree = tree;
this.filteredTree = tree ? JSON.parse(tree) : [];
this.expandTreeNodes(this.filteredTree);
+ if (this.inspectionService.featureTreeFilterValue) {
+ this.filterTree();
+ }
});
}
ngOnInit(): void {
this.cols = [
- { field: 'k', header: 'Key' },
- { field: 'v', header: 'Value' }
+ { field: 'key', header: 'Key' },
+ { field: 'value', header: 'Value' }
];
}
- copyGeoJsonToClipboard() {
- navigator.clipboard.writeText(this.inspectionService.selectedFeatureGeoJsonText).then(
- () => {
- this.messageService.showSuccess("Copied GeoJSON content to clipboard!");
- },
- () => {
- this.messageService.showError("Could not copy GeoJSON content to clipboard.");
- },
- );
- }
-
- getFilterValue(event: Event) {
- return (event.target as HTMLInputElement).value;
+ copyToClipboard(text: string) {
+ this.clipboardService.copyToClipboard(text);
}
expandTreeNodes(nodes: TreeTableNode[], parent: any = null): void {
@@ -132,27 +201,40 @@ export class InspectionPanelComponent implements OnInit {
});
}
- typeToBackground(type: string) {
- if (type == "string") {
- return "#4а4";
- } else {
- return "#ad8";
- }
- }
-
- filterTree(event: any) {
- const query = event.target.value.toLowerCase();
+ filterTree() {
+ const query = this.inspectionService.featureTreeFilterValue.toLowerCase();
if (!query) {
this.filteredTree = JSON.parse(this.jsonTree);
this.expandTreeNodes(this.filteredTree);
return;
}
+ if (this.filterOnlyFeatureIds) {
+ this.filterByKeys = false;
+ this.filterByValues = false;
+ this.filterGeometryEntries = false;
+ }
+
const filterNodes = (nodes: TreeTableNode[]): TreeTableNode[] => {
return nodes.reduce
((filtered, node) => {
- const key = node.data.k.toString().toLowerCase();
- const value = node.data.v.toString().toLowerCase();
- let matches = key.includes(query) || value.includes(query);
+ let matches = false;
+ if (!this.filterGeometryEntries && node.data.key == "Geometry") {
+ return filtered;
+ }
+
+ if (this.filterOnlyFeatureIds) {
+ if (node.data.type == this.InspectionValueType.FEATUREID.value) {
+ matches = String(node.data.value).toLowerCase().includes(query) || String(node.data.hoverId).toLowerCase().includes(query);
+ }
+ } else {
+ if (this.filterByKeys && this.filterByValues) {
+ matches = String(node.data.key).toLowerCase().includes(query) || String(node.data.value).toLowerCase().includes(query);
+ } else if (this.filterByKeys) {
+ matches = String(node.data.key).toLowerCase().includes(query);
+ } else if (this.filterByValues) {
+ matches = String(node.data.value).toLowerCase().includes(query);
+ }
+ }
if (node.children) {
let filteredChildren = filterNodes(node.children);
@@ -179,4 +261,90 @@ export class InspectionPanelComponent implements OnInit {
node.expanded = !node.expanded;
this.filteredTree = [...this.filteredTree];
}
+
+ onKeyClick(event: MouseEvent, rowData: any) {
+ this.inspectionMenu.toggle(event);
+ event.stopPropagation();
+ const key = rowData["key"];
+ const value = rowData["value"];
+ this.inspectionMenuItems = [
+ // {
+ // label: 'Find Features with this Value',
+ // command: () => {
+ //
+ // }
+ // },
+ {
+ label: 'Copy Key/Value',
+ command: () => {
+ this.copyToClipboard(`{${key}: ${value}}`);
+ }
+ },
+ // {
+ // label: 'Show in NDS.Live Blob',
+ // command: () => {
+ // }
+ // },
+ {
+ label: 'Open NDS.Live Docs',
+ command: () => {
+ window.open(`https://doc.nds.live/search?q=${key}`, "_blank");
+ }
+ }
+ ];
+ if (rowData.hasOwnProperty("geoJsonPath")) {
+ const path = rowData["geoJsonPath"];
+ this.inspectionMenuItems.push({
+ label: 'Copy GeoJson Path',
+ command: () => {
+ this.copyToClipboard(path);
+ }
+ });
+ }
+ }
+
+ onValueClick(event: any, rowData: any) {
+ event.stopPropagation();
+ const selection = window.getSelection();
+ if (selection && selection.toString().length > 0) {
+ return;
+ }
+
+ if (rowData["type"] == this.InspectionValueType.FEATUREID.value) {
+ this.jumpService.highlightFeature(this.inspectionService.selectedMapIdName, rowData["value"]).then();
+ }
+ this.copyToClipboard(rowData["value"]);
+ }
+
+ highlightFeature(rowData: any) {
+ return;
+ }
+
+ stopHighlight(rowData: any) {
+ return;
+ }
+
+ getStyleClassByType(valueType: number): string {
+ switch (valueType) {
+ case this.InspectionValueType.SECTION.value:
+ return "section-style"
+ case this.InspectionValueType.FEATUREID.value:
+ return "feature-id-style"
+ default:
+ return "standard-style"
+ }
+ }
+
+ protected readonly InspectionValueType = coreLib.ValueType;
+
+ clearFilter() {
+ this.inspectionService.featureTreeFilterValue = "";
+ this.filterTree();
+ }
+
+ onKeydown(event: KeyboardEvent) {
+ if (event.key === 'Escape') {
+ this.clearFilter();
+ }
+ }
}
\ No newline at end of file
diff --git a/erdblick_app/app/inspection.service.ts b/erdblick_app/app/inspection.service.ts
index e060fee6..cd5a959f 100644
--- a/erdblick_app/app/inspection.service.ts
+++ b/erdblick_app/app/inspection.service.ts
@@ -1,79 +1,144 @@
import {Injectable} from "@angular/core";
import {TreeTableNode} from "primeng/api";
-import {BehaviorSubject} from "rxjs";
+import {BehaviorSubject, distinctUntilChanged, filter} from "rxjs";
+import {MapService} from "./map.service";
+import {Feature} from "../../build/libs/core/erdblick-core";
+import {FeatureWrapper} from "./features.model";
+import {ParametersService} from "./parameters.service";
+import {coreLib} from "./wasm";
+import {JumpTargetService} from "./jump.service";
+
+
+interface InspectionModelData {
+ key: string;
+ type: number;
+ value: any;
+ info?: string;
+ hoverId?: string
+ geoJsonPath?: string;
+ children: Array;
+}
@Injectable({providedIn: 'root'})
export class InspectionService {
featureTree: BehaviorSubject = new BehaviorSubject("");
+ featureTreeFilterValue: string = "";
isInspectionPanelVisible: boolean = false;
selectedFeatureGeoJsonText: string = "";
- selectedFeatureIdText: string = "";
-
- constructor() { }
+ selectedFeatureInspectionModel: Array | null = null;
+ selectedFeatureIdName: string = "";
+ selectedMapIdName: string = "";
+ selectedFeature: FeatureWrapper | null = null;
- getFeatureTreeData() {
- let jsonData = JSON.parse(this.selectedFeatureGeoJsonText);
- if (jsonData.hasOwnProperty("id")) {
- delete jsonData["id"];
- }
- if (jsonData.hasOwnProperty("properties")) {
- jsonData["attributes"] = jsonData["properties"];
- delete jsonData["properties"];
- }
- // Push leaf values up
- const sortedJson: Record = {};
- for (const key in jsonData) {
- if (typeof jsonData[key] === "string" || typeof jsonData[key] === "number") {
- sortedJson[key] = jsonData[key];
- }
- }
- for (const key in jsonData) {
- if (typeof jsonData[key] !== "string" && typeof jsonData[key] !== "number") {
- sortedJson[key] = jsonData[key];
+ constructor(private mapService: MapService,
+ private jumpService: JumpTargetService,
+ public parametersService: ParametersService) {
+ this.mapService.selectionTopic.pipe(distinctUntilChanged()).subscribe(selectedFeature => {
+ if (!selectedFeature) {
+ this.isInspectionPanelVisible = false;
+ this.featureTreeFilterValue = "";
+ this.parametersService.unsetSelectedFeature();
+ return;
}
- }
-
- let convertToTreeTableNodes = (json: any): TreeTableNode[] => {
- const treeTableNodes: TreeTableNode[] = [];
+ this.selectedMapIdName = selectedFeature.featureTile.mapName;
+ selectedFeature.peek((feature: Feature) => {
+ this.selectedFeatureInspectionModel = feature.inspectionModel();
+ this.selectedFeatureGeoJsonText = feature.geojson() as string;
+ this.selectedFeatureIdName = feature.id() as string;
+ this.isInspectionPanelVisible = true;
+ this.loadFeatureData();
+ });
+ this.selectedFeature = selectedFeature;
+ this.parametersService.setSelectedFeature(this.selectedMapIdName, this.selectedFeatureIdName);
+ });
- for (const key in json) {
- if (json.hasOwnProperty(key)) {
- const value = json[key];
- const node: TreeTableNode = {};
+ this.parametersService.parameters.pipe(filter(
+ parameters => parameters.selected.length == 2)).subscribe(parameters => {
+ const [mapId, featureId] = parameters.selected;
+ if (mapId != this.selectedMapIdName || featureId != this.selectedFeatureIdName) {
+ this.jumpService.highlightFeature(mapId, featureId);
+ if (this.selectedFeature != null) {
+ this.mapService.focusOnFeature(this.selectedFeature);
+ }
+ }
+ });
+ }
- if (typeof value === 'object' && value !== null) {
- if (Array.isArray(value)) {
- // If it's an array, iterate through its elements and convert them to TreeTableNodes
- node.data = {k: key, v: "", t: ""};
- node.children = value.map((item: any, index: number) => {
- if (typeof item === 'object') {
- return {data: {k: index, v: "", t: typeof item}, children: convertToTreeTableNodes(item)};
- } else {
- return {data: {k: index, v: item.toString(), t: typeof item}};
- }
- });
- } else {
- // If it's an object, recursively call the function to convert it to TreeTableNodes
- node.data = {k: key, v: "", t: ""}
- node.children = convertToTreeTableNodes(value);
+ getFeatureTreeDataFromModel() {
+ let convertToTreeTableNodes = (dataNodes: Array): TreeTableNode[] => {
+ let treeNodes: Array = [];
+ for (const data of dataNodes) {
+ const node: TreeTableNode = {};
+ let value = data.value;
+ if (data.type == this.InspectionValueType.NULL.value && data.children === undefined) {
+ value = "NULL";
+ } else if ((data.type & 128) == 128 && (data.type - 128) == 1) {
+ for (let i = 0; i < value.length; i++) {
+ if (!Number.isInteger(value[i])) {
+ const strValue = String(value[i])
+ const index = strValue.indexOf('.');
+ if (index !== -1 && strValue.length - index - 1 > 8) {
+ value[i] = value[i].toFixed(8);
+ }
}
- } else {
- // If it's a primitive value, set it as the node's data
- node.data = {k: key, v: value ? value : "null" , t: typeof value};
}
+ }
- treeTableNodes.push(node);
+ if ((data.type & 128) == 128) {
+ value = value.join(", ");
}
- }
- return treeTableNodes;
+ node.data = {
+ key: data.key,
+ value: value,
+ type: data.type
+ };
+ if (data.hasOwnProperty("info")) {
+ node.data["info"] = data.info;
+ }
+ if (data.hasOwnProperty("hoverId")) {
+ node.data["hoverId"] = data.hoverId;
+ }
+ if (data.hasOwnProperty("geoJsonPath")) {
+ node.data["geoJsonPath"] = data.geoJsonPath;
+ }
+ node.children = data.hasOwnProperty("children") ? convertToTreeTableNodes(data.children) : [];
+ treeNodes.push(node);
+ }
+ return treeNodes;
}
- return convertToTreeTableNodes(sortedJson);
+ let treeNodes: Array = [];
+ if (this.selectedFeatureInspectionModel) {
+ for (const section of this.selectedFeatureInspectionModel) {
+ const node: TreeTableNode = {};
+ node.data = {key: section.key, value: section.value, type: section.type};
+ if (section.hasOwnProperty("info")) {
+ node.data["info"] = section.info;
+ }
+ node.children = convertToTreeTableNodes(section.children);
+ treeNodes.push(node);
+ }
+ }
+ return treeNodes;
}
loadFeatureData() {
- this.featureTree.next(JSON.stringify(this.getFeatureTreeData()));
+ if (this.selectedFeatureInspectionModel) {
+ this.featureTree.next(JSON.stringify(this.getFeatureTreeDataFromModel(), (_, value) => {
+ if (typeof value === 'bigint') {
+ return value.toString();
+ } else if (value == null) {
+ return "";
+ } else {
+ return value;
+ }
+ }));
+ } else {
+ this.featureTree.next('[]');
+ }
}
+
+ protected readonly InspectionValueType = coreLib.ValueType;
}
\ No newline at end of file
diff --git a/erdblick_app/app/jump.service.ts b/erdblick_app/app/jump.service.ts
index 0cd9d1e5..39b6fa7c 100644
--- a/erdblick_app/app/jump.service.ts
+++ b/erdblick_app/app/jump.service.ts
@@ -1,51 +1,187 @@
import {Injectable} from "@angular/core";
-import {Subject} from "rxjs";
+import {BehaviorSubject, Subject} from "rxjs";
import {HttpClient} from "@angular/common/http";
+import {MapService} from "./map.service";
+import {LocateResponse} from "./visualization.model";
+import {InfoMessageService} from "./info.service";
+import {coreLib} from "./wasm";
+import {FeatureSearchService} from "./feature.search.service";
+import {SidePanelService, SidePanelState} from "./sidepanel.service";
-export interface JumpTarget {
+export interface SearchTarget {
name: string;
label: string;
enabled: boolean;
- jump: (value: string) => number[] | undefined;
+ jump?: (value: string) => number[] | undefined;
+ execute?: (value: string) => void;
validate: (value: string) => boolean;
}
+interface FeatureJumpAction {
+ name: string;
+ error: string|null;
+ idParts: Array<{key: string, value: string|number}>;
+ maps: Array;
+}
+
@Injectable({providedIn: 'root'})
export class JumpTargetService {
- targetValueSubject = new Subject();
- availableOptions = new Subject>();
-
- constructor(private httpClient: HttpClient) {
- httpClient.get("/config.json", {responseType: 'json'}).subscribe(
- {
- next: (data: any) => {
- try {
- if (data && data["extensionModules"] && data["extensionModules"]["jumpTargets"]) {
- let jumpTargetsConfig = data["extensionModules"]["jumpTargets"];
- if (jumpTargetsConfig !== undefined) {
- // Using string interpolation so webpack can trace imports from the location
- import(`../../config/${jumpTargetsConfig}.js`).then(function (plugin) {
- return plugin.default() as Array;
- }).then((jumpTargets: Array) => {
- this.availableOptions.next(jumpTargets);
- }).catch((error) => {
- this.availableOptions.next([]);
- console.log(error);
- });
- return;
- }
+ markedPosition: Subject> = new Subject>();
+ targetValueSubject = new BehaviorSubject("");
+ jumpTargets = new BehaviorSubject>([]);
+ extJumpTargets: Array = [];
+
+ // Communication channels with the map selection dialog (in SearchPanelComponent).
+ // The mapSelectionSubject triggers the display of the dialog, and
+ // the setSelectedMap promise resolver is used by the dialog to communicate the
+ // user's choice.
+ mapSelectionSubject = new Subject>();
+ setSelectedMap: ((choice: string|null)=>void)|null = null;
+
+ constructor(private httpClient: HttpClient,
+ private mapService: MapService,
+ private messageService: InfoMessageService,
+ private sidePanelService: SidePanelService,
+ private searchService: FeatureSearchService) {
+ this.httpClient.get("/config.json", {responseType: 'json'}).subscribe({
+ next: (data: any) => {
+ try {
+ if (data && data["extensionModules"] && data["extensionModules"]["jumpTargets"]) {
+ let jumpTargetsConfig = data["extensionModules"]["jumpTargets"];
+ if (jumpTargetsConfig !== undefined) {
+ // Using string interpolation so webpack can trace imports from the location
+ import(`../../config/${jumpTargetsConfig}.js`).then(function (plugin) {
+ return plugin.default() as Array;
+ }).then((jumpTargets: Array) => {
+ this.extJumpTargets = jumpTargets;
+ this.update();
+ }).catch((error) => {
+ console.error(error);
+ });
+ return;
}
- this.availableOptions.next([]);
- } catch (error) {
- this.availableOptions.next([]);
- console.log(error);
}
- },
- error: error => {
- this.availableOptions.next([]);
- console.log(error);
+ } catch (error) {
+ console.error(error);
+ }
+ },
+ error: error => {
+ console.error(error);
+ }
+ });
+
+ // Filter out feature jump targets based on search value.
+ this.targetValueSubject.subscribe(_ => {
+ this.update();
+ })
+ }
+
+ getFeatureMatchTarget(): SearchTarget {
+ let simfilError = '';
+ try {
+ coreLib.validateSimfilQuery(this.targetValueSubject.getValue());
+ } catch (e: any) {
+ const parsingError = e.message.split(':', 2);
+ simfilError = parsingError.length > 1 ? parsingError[1] : parsingError[0];
+ }
+ let label = "Match features with a filter expression";
+ if (simfilError) {
+ label += `
${simfilError}`;
+ }
+ return {
+ name: "Search Loaded Features",
+ label: label,
+ enabled: false,
+ execute: (value: string) => {
+ this.sidePanelService.panel = SidePanelState.FEATURESEARCH;
+ this.searchService.run(value);
+ },
+ validate: (_: string) => {
+ return !simfilError;
+ }
+ }
+ }
+
+ update() {
+ let featureJumpTargets = this.mapService.tileParser?.filterFeatureJumpTargets(this.targetValueSubject.getValue());
+ let featureJumpTargetsConverted = [];
+ if (featureJumpTargets) {
+ featureJumpTargetsConverted = featureJumpTargets.map((fjt: FeatureJumpAction) => {
+ let label = fjt.idParts.map(idPart => `${idPart.key}=${idPart.value}`).join(" | ")
+ if (fjt.error) {
+ label += `
${fjt.error}`;
+ }
+ return {
+ name: `Jump to ${fjt.name}`,
+ label: label,
+ enabled: !fjt.error,
+ execute: (_: string) => { this.jumpToFeature(fjt).then(); },
+ validate: (_: string) => { return !fjt.error; },
}
});
+ }
+
+ this.jumpTargets.next([
+ this.getFeatureMatchTarget(),
+ ...featureJumpTargetsConverted,
+ ...this.extJumpTargets
+ ]);
+ }
+
+ async highlightFeature(mapId: string, featureId: string) {
+ let featureJumpTargets = this.mapService.tileParser?.filterFeatureJumpTargets(featureId) as Array;
+ const validIndex = featureJumpTargets.findIndex(action => !action.error);
+ if (validIndex == -1) {
+ console.error(`Error highlighting ${featureId}!`);
+ return;
+ }
+ await this.jumpToFeature(featureJumpTargets[validIndex], false, mapId);
+ }
+
+ async jumpToFeature(action: FeatureJumpAction, moveCamera: boolean=true, mapId?:string|null) {
+ // Select the map.
+ if (!mapId) {
+ if (action.maps.length > 1) {
+ let selectedMapPromise = new Promise((resolve, _) => {
+ this.setSelectedMap = resolve;
+ })
+ this.mapSelectionSubject.next(action.maps);
+ mapId = await selectedMapPromise;
+ }
+ else {
+ mapId = action.maps[0];
+ }
+ }
+ if (!mapId) {
+ return;
+ }
+
+ // Locate the feature.
+ let resolveMe = {requests: [{
+ typeId: action.name,
+ mapId: mapId,
+ featureId: action.idParts.map((kv) => [kv.key, kv.value]).flat()
+ }]};
+ let response = await fetch("/locate", {
+ body: JSON.stringify(resolveMe),
+ method: "POST"
+ }).catch((err)=>console.error(`Error during /locate call: ${err}`));
+ if (!response) {
+ return;
+ }
+ let extRefsResolved = await response.json() as LocateResponse;
+ if (extRefsResolved.responses[0].length < 1) {
+ this.messageService.showError("Could not locate feature!")
+ return;
+ }
+ let selectThisFeature = extRefsResolved.responses[0][0];
+
+ // Set feature-to-select on MapService.
+ await this.mapService.selectFeature(
+ selectThisFeature.tileId,
+ selectThisFeature.typeId,
+ selectThisFeature.featureId,
+ moveCamera);
}
}
\ No newline at end of file
diff --git a/erdblick_app/app/map.panel.component.ts b/erdblick_app/app/map.panel.component.ts
index ad4b1732..20d1c532 100644
--- a/erdblick_app/app/map.panel.component.ts
+++ b/erdblick_app/app/map.panel.component.ts
@@ -1,13 +1,16 @@
import {Component, ViewChild} from "@angular/core";
import {InfoMessageService} from "./info.service";
import {MapInfoItem, MapService} from "./map.service";
-import {StyleService} from "./style.service";
+import {ErdblickStyle, StyleService} from "./style.service";
import {ParametersService} from "./parameters.service";
import {FileUpload} from "primeng/fileupload";
import {Subscription} from "rxjs";
import {Dialog} from "primeng/dialog";
import {KeyValue} from "@angular/common";
import {coreLib} from "./wasm";
+import {SidePanelService, SidePanelState} from "./sidepanel.service";
+import {MenuItem} from "primeng/api";
+import {Menu} from "primeng/menu";
@Component({
@@ -23,10 +26,10 @@ import {coreLib} from "./wasm";
label="" pTooltip="Toggle OSM overlay" tooltipPosition="bottom">
@@ -34,21 +37,34 @@ import {coreLib} from "./wasm";
-
- {{ mapLayer.key }}
-
+
-
+
+ {{ mapLayer.value.tileBorders ? 'select_all' : 'deselect' }}
+ label="" pTooltip="Focus on layer" tooltipPosition="bottom"
+ [style]="{'padding-left': '0', 'padding-right': '0'}">
+ loupe
+
@@ -72,28 +89,26 @@ import {coreLib} from "./wasm";
-
- {{ style.key }}
-
+
@@ -103,28 +118,26 @@ import {coreLib} from "./wasm";
-
- {{ style.key }}
-
+
@@ -133,33 +146,34 @@ import {coreLib} from "./wasm";
-
+
{{ message.key }}: {{ message.value }} (see console)
-
+
-
+
@@ -172,6 +186,10 @@ import {coreLib} from "./wasm";
Press Esc to quit without saving
+
+
@@ -195,8 +213,11 @@ export class MapPanelComponent {
savedStyleDataSubscription: Subscription = new Subscription();
dataWasModified: boolean = false;
- osmEnabled: boolean;
- osmOpacityValue: number;
+ osmEnabled: boolean = true;
+ osmOpacityValue: number = 30;
+
+ @ViewChild('menu') toggleMenu!: Menu;
+ toggleMenuItems: MenuItem[] | undefined;
@ViewChild('styleUploader') styleUploader: FileUpload | undefined;
@ViewChild('editorDialog') editorDialog: Dialog | undefined;
@@ -204,21 +225,137 @@ export class MapPanelComponent {
constructor(public mapService: MapService,
private messageService: InfoMessageService,
public styleService: StyleService,
- public parameterService: ParametersService) {
- this.osmEnabled = this.parameterService.osmEnabled.getValue();
- this.osmOpacityValue = this.parameterService.osmOpacityValue.getValue();
+ public parameterService: ParametersService,
+ private sidePanelService: SidePanelService)
+ {
+ this.parameterService.parameters.subscribe(parameters => {
+ this.osmEnabled = parameters.osm;
+ this.osmOpacityValue = parameters.osmOpacity;
+ });
this.mapService.maps.subscribe(
mapItems => this.mapItems = mapItems
);
+ this.sidePanelService.observable().subscribe(activePanel => {
+ if (activePanel != SidePanelState.MAPS) {
+ this.layerDialogVisible = false;
+ }
+ })
+ }
+
+ get osmOpacityString(): string {
+ return 'Opacity: ' + this.osmOpacityValue;
+ }
+
+ showStylesToggleMenu(event: MouseEvent, styleId: string) {
+ this.toggleMenu.toggle(event);
+ this.toggleMenuItems = [
+ {
+ label: 'Toggle All off but This',
+ command: () => {
+ for (const id of this.styleService.styleData.keys()) {
+ this.styleService.styleData.get(id)!.enabled = styleId == id;
+ this.parameterService.setStyleConfig(id, styleId == id);
+ }
+ this.styleService.reapplyAllStyles();
+ this.mapService.update();
+ }
+ },
+ {
+ label: 'Toggle All on but This',
+ command: () => {
+ for (const id of this.styleService.styleData.keys()) {
+ this.styleService.styleData.get(id)!.enabled = styleId != id;
+ this.parameterService.setStyleConfig(id, styleId != id);
+ }
+ this.styleService.reapplyAllStyles();
+ this.mapService.update();
+ }
+ },
+ {
+ label: 'Toggle All Off',
+ command: () => {
+ for (const id of this.styleService.styleData.keys()) {
+ this.styleService.styleData.get(id)!.enabled = false;
+ this.parameterService.setStyleConfig(id, false);
+ }
+ this.styleService.reapplyAllStyles();
+ this.mapService.update();
+ }
+ },
+ {
+ label: 'Toggle All On',
+ command: () => {
+ for (const id of this.styleService.styleData.keys()) {
+ this.styleService.styleData.get(id)!.enabled = true;
+ this.parameterService.setStyleConfig(id, true);
+ }
+ this.styleService.reapplyAllStyles();
+ this.mapService.update();
+ }
+ }
+ ];
+ }
+
+ showLayersToggleMenu(event: MouseEvent, mapName: string, layerName: string) {
+ this.toggleMenu.toggle(event);
+ this.toggleMenuItems = [
+ {
+ label: 'Toggle All off but This',
+ command: () => {
+ if (this.mapItems.has(mapName)) {
+ for (const id of this.mapItems.get(mapName)!.layers.keys()!) {
+ this.mapItems.get(mapName)!.layers.get(id)!.visible = id == layerName;
+ this.toggleLayer(mapName, layerName);
+ }
+ }
+ }
+ },
+ {
+ label: 'Toggle All on but This',
+ command: () => {
+ if (this.mapItems.has(mapName)) {
+ for (const id of this.mapItems.get(mapName)!.layers.keys()!) {
+ this.mapItems.get(mapName)!.layers.get(id)!.visible = id != layerName;
+ this.toggleLayer(mapName, layerName);
+ }
+ }
+ }
+ },
+ {
+ label: 'Toggle All Off',
+ command: () => {
+ if (this.mapItems.has(mapName)) {
+ for (const id of this.mapItems.get(mapName)!.layers.keys()!) {
+ this.mapItems.get(mapName)!.layers.get(id)!.visible = false;
+ this.toggleLayer(mapName, layerName);
+ }
+ }
+ }
+ },
+ {
+ label: 'Toggle All On',
+ command: () => {
+ if (this.mapItems.has(mapName)) {
+ for (const id of this.mapItems.get(mapName)!.layers.keys()!) {
+ this.mapItems.get(mapName)!.layers.get(id)!.visible = true;
+ this.toggleLayer(mapName, layerName);
+ }
+ }
+ }
+ }
+ ];
}
showLayerDialog() {
this.layerDialogVisible = !this.layerDialogVisible;
+ if (this.layerDialogVisible) {
+ this.sidePanelService.panel = SidePanelState.MAPS;
+ }
}
focus(tileId: bigint, event: any) {
event.stopPropagation();
- this.mapService.zoomToWgs84PositionTopic.next(
+ this.mapService.moveToWgs84PositionTopic.next(
coreLib.getTilePosition(BigInt(tileId))
);
}
@@ -229,25 +366,23 @@ export class MapPanelComponent {
toggleOSMOverlay() {
this.osmEnabled = !this.osmEnabled;
- this.parameterService.osmEnabled.next(this.osmEnabled);
this.updateOSMOverlay();
}
updateOSMOverlay() {
- if (this.parameterService.osmEnabled.getValue()) {
- this.parameterService.osmOpacityValue.next(this.osmOpacityValue);
- } else {
- this.parameterService.osmOpacityValue.next(0);
- }
const parameters = this.parameterService.parameters.getValue();
if (parameters) {
- parameters.osmEnabled = this.osmEnabled;
+ parameters.osm = this.osmEnabled;
parameters.osmOpacity = this.osmOpacityValue;
this.parameterService.parameters.next(parameters);
}
}
- toggleLayer(mapName: string, layerName: string) {
+ toggleTileBorders(mapName: string, layerName: string) {
+ this.mapService.toggleLayerTileBorderVisibility(mapName, layerName);
+ }
+
+ toggleLayer(mapName: string, layerName: string = "") {
this.mapService.toggleMapLayerVisibility(mapName, layerName);
}
@@ -284,7 +419,7 @@ export class MapPanelComponent {
})
.catch((error) => {
this.messageService.showError(`Error occurred while trying to import style: ${styleId}`);
- console.log(error);
+ console.error(error);
});
}
}
diff --git a/erdblick_app/app/map.service.ts b/erdblick_app/app/map.service.ts
index ba17dd44..0de11cdd 100644
--- a/erdblick_app/app/map.service.ts
+++ b/erdblick_app/app/map.service.ts
@@ -1,12 +1,14 @@
import {Injectable} from "@angular/core";
import {Fetch} from "./fetch.model";
-import {FeatureTile} from "./features.model";
+import {FeatureTile, FeatureWrapper} from "./features.model";
import {coreLib, uint8ArrayToWasm} from "./wasm";
import {TileVisualization} from "./visualization.model";
import {BehaviorSubject, Subject} from "rxjs";
import {ErdblickStyle, StyleService} from "./style.service";
-import {FeatureLayerStyle, TileLayerParser} from '../../build/libs/core/erdblick-core';
+import {FeatureLayerStyle, TileLayerParser, Feature} from '../../build/libs/core/erdblick-core';
import {ParametersService} from "./parameters.service";
+import {SidePanelService, SidePanelState} from "./sidepanel.service";
+import {InfoMessageService} from "./info.service";
export interface LayerInfoItem extends Object {
canRead: boolean;
@@ -19,6 +21,7 @@ export interface LayerInfoItem extends Object {
zoomLevels: Array
;
level: number;
visible: boolean;
+ tileBorders: boolean;
}
export interface MapInfoItem extends Object {
@@ -28,6 +31,8 @@ export interface MapInfoItem extends Object {
maxParallelJobs: number;
nodeId: string;
protocolVersion: {major: number, minor: number, patch: number};
+ addOn: boolean;
+ visible: boolean;
}
const infoUrl = "/sources";
@@ -58,8 +63,7 @@ type ViewportProperties = {
export class MapService {
public maps: BehaviorSubject