diff --git a/erdblick_app/app/app.module.ts b/erdblick_app/app/app.module.ts index d290b01c..5bad1861 100644 --- a/erdblick_app/app/app.module.ts +++ b/erdblick_app/app/app.module.ts @@ -72,6 +72,9 @@ import {ReactiveFormsModule} from '@angular/forms'; import {FormlyPrimeNGModule} from "@ngx-formly/primeng"; import {DataSourcesService} from "./datasources.service"; import {ProgressSpinnerModule} from "primeng/progressspinner"; +import {TileSourceDataComponent} from "./tilesources.component"; +import {ContextMenuModule} from "primeng/contextmenu"; +import {RightClickMenuService} from "./rightclickmenu.service"; export function initializeServices(styleService: StyleService, mapService: MapService, coordService: CoordinatesService) { return async () => { @@ -147,6 +150,7 @@ export function typeValidationMessage({ schemaType }: any) { MultiSchemaTypeComponent, HighlightSearch, TreeTableFilterPatchDirective, + TileSourceDataComponent ], bootstrap: [ AppComponent @@ -213,7 +217,8 @@ export function typeValidationMessage({ schemaType }: any) { {name: 'multischema', component: MultiSchemaTypeComponent} ], }), - ProgressSpinnerModule + ProgressSpinnerModule, + ContextMenuModule ], providers: [ { @@ -233,6 +238,7 @@ export function typeValidationMessage({ schemaType }: any) { ClipboardService, EditorService, DataSourcesService, + RightClickMenuService, provideHttpClient() ] }) diff --git a/erdblick_app/app/datasources.component.ts b/erdblick_app/app/datasources.component.ts index 17ee9b11..60cee168 100644 --- a/erdblick_app/app/datasources.component.ts +++ b/erdblick_app/app/datasources.component.ts @@ -208,7 +208,6 @@ export class DatasourcesComponent { } closeEditorDialog(event: any) { - console.log(event); if (this.editorDialog !== undefined) { if (this.wasModified) { event.stopPropagation(); diff --git a/erdblick_app/app/feature.panel.component.ts b/erdblick_app/app/feature.panel.component.ts index 7205cca0..f983b1a3 100644 --- a/erdblick_app/app/feature.panel.component.ts +++ b/erdblick_app/app/feature.panel.component.ts @@ -15,6 +15,7 @@ import {coreLib} from "./wasm"; import {ClipboardService} from "./clipboard.service"; import {TreeTable} from "primeng/treetable"; import {ParametersService} from "./parameters.service"; +import {InfoMessageService} from "./info.service"; interface Column { field: string; @@ -193,6 +194,7 @@ export class FeaturePanelComponent implements OnInit, AfterViewInit, OnDestroy public jumpService: JumpTargetService, public parameterService: ParametersService, private renderer: Renderer2, + private messageService: InfoMessageService, public mapService: MapService) { this.inspectionService.featureTree.pipe(distinctUntilChanged()).subscribe((tree: string) => { this.jsonTree = tree; @@ -360,19 +362,23 @@ export class FeaturePanelComponent implements OnInit, AfterViewInit, OnDestroy showSourceData(event: any, sourceDataRef: any) { event.stopPropagation(); - const layerId = sourceDataRef.layerId; - const tileId = sourceDataRef.tileId; - const address = sourceDataRef.address; - const mapId = this.inspectionService.selectedFeatures[0].featureTile.mapName; - const featureIds = this.inspectionService.selectedFeatures.map(f=>f.featureId).join(", "); - - this.inspectionService.selectedSourceData.next({ - tileId: Number(tileId), - layerId: String(layerId), - mapId: String(mapId), - address: BigInt(address), - featureIds: featureIds, - }) + try { + const layerId = sourceDataRef.layerId; + const tileId = sourceDataRef.tileId; + const address = sourceDataRef.address; + const mapId = this.inspectionService.selectedFeatures[0].featureTile.mapName; + const featureIds = this.inspectionService.selectedFeatures.map(f => f.featureId).join(", "); + + this.inspectionService.selectedSourceData.next({ + tileId: Number(tileId), + layerId: String(layerId), + mapId: String(mapId), + address: BigInt(address), + featureIds: featureIds, + }) + } catch (e) { + this.messageService.showError(`Encountered error: ${e}`); + } } onValueClick(event: any, rowData: any) { diff --git a/erdblick_app/app/inspection.panel.component.ts b/erdblick_app/app/inspection.panel.component.ts index e57066db..03562550 100644 --- a/erdblick_app/app/inspection.panel.component.ts +++ b/erdblick_app/app/inspection.panel.component.ts @@ -29,13 +29,14 @@ export interface InspectionContainerSize { @Component({ selector: 'inspection-panel', template: ` - - + {{ tabs[activeIndex].title || '' }} @@ -87,7 +88,7 @@ export class InspectionPanelComponent private parameterService: ParametersService) { this.pushFeatureInspector(); - this.inspectionService.featureTree.pipe(distinctUntilChanged()).subscribe((tree: string) => { + this.inspectionService.featureTree.pipe(distinctUntilChanged()).subscribe(_ => { this.reset(); // TODO: Create a new FeaturePanelComponent instance for each unique feature selection. @@ -96,12 +97,11 @@ export class InspectionPanelComponent const featureIds = this.inspectionService.selectedFeatures.map(f=>f.featureId).join(", "); if (this.inspectionService.selectedFeatures.length == 1) { this.tabs[0].title = featureIds; - } - else { + } else { this.tabs[0].title = `Selected ${this.inspectionService.selectedFeatures.length} Features`; } - const selectedSourceData = parameterService.getSelectedSourceData() + const selectedSourceData = this.parameterService.getSelectedSourceData() if (selectedSourceData?.featureIds === featureIds) this.inspectionService.selectedSourceData.next(selectedSourceData); else @@ -131,7 +131,7 @@ export class InspectionPanelComponent } this.pushSourceDataInspector(selection); } - }) + }); } reset() { diff --git a/erdblick_app/app/inspection.service.ts b/erdblick_app/app/inspection.service.ts index 2e2c7600..c8eab300 100644 --- a/erdblick_app/app/inspection.service.ts +++ b/erdblick_app/app/inspection.service.ts @@ -29,8 +29,8 @@ export interface SelectedSourceData { mapId: string, tileId: number, layerId: string, - address: bigint, - featureIds: string, + address?: bigint, + featureIds?: string, } export function selectedSourceDataEqualTo(a: SelectedSourceData | null, b: SelectedSourceData | null) { @@ -73,7 +73,6 @@ export class InspectionService { inspectionPanelChanged = new EventEmitter(); constructor(private mapService: MapService, - private jumpService: JumpTargetService, private infoMessageService: InfoMessageService, private keyboardService: KeyboardService, public parametersService: ParametersService) { @@ -98,14 +97,14 @@ export class InspectionService { selectedFeatures[0].peek((feature: Feature) => { this.selectedFeatureInspectionModel.push(...feature.inspectionModel()); this.selectedFeatureGeoJsonTexts.push(feature.geojson() as string); - this.isInspectionPanelVisible = true; const center = feature.center() as Cartesian3; this.selectedFeatureCenter = center; this.selectedFeatureOrigin = Cartesian3.fromDegrees(center.x, center.y, center.z); let radiusPoint = feature.boundingRadiusEndPoint() as Cartesian3; radiusPoint = Cartesian3.fromDegrees(radiusPoint.x, radiusPoint.y, radiusPoint.z); this.selectedFeatureBoundingRadius = Cartesian3.distance(this.selectedFeatureOrigin, radiusPoint); - this.selectedFeatureGeometryType = feature.getGeometryType() as any;this.isInspectionPanelVisible = true; + this.selectedFeatureGeometryType = feature.getGeometryType() as any; + this.isInspectionPanelVisible = true; }); } if (selectedFeatures.length > 1) { @@ -123,10 +122,11 @@ export class InspectionService { }); this.selectedSourceData.pipe(distinctUntilChanged(selectedSourceDataEqualTo)).subscribe(selection => { - if (selection) + if (selection) { this.parametersService.setSelectedSourceData(selection); - else + } else { this.parametersService.unsetSelectedSourceData(); + } }); } diff --git a/erdblick_app/app/map.service.ts b/erdblick_app/app/map.service.ts index 70a2ad01..f4670bf1 100644 --- a/erdblick_app/app/map.service.ts +++ b/erdblick_app/app/map.service.ts @@ -388,7 +388,7 @@ export class MapService { ...this.currentVisibleTileIds, ...new Set( allViewportTileIds.slice(0, this.parameterService.parameters.getValue().tilesVisualizeLimit)) - ]) + ]); } } diff --git a/erdblick_app/app/parameters.service.ts b/erdblick_app/app/parameters.service.ts index f21d0451..59cd9fa4 100644 --- a/erdblick_app/app/parameters.service.ts +++ b/erdblick_app/app/parameters.service.ts @@ -210,6 +210,8 @@ export class ParametersService { lastSearchHistoryEntry: BehaviorSubject<[number, string] | null> = new BehaviorSubject<[number, string] | null>(null); + tileIdsForSourceData: Array = []; + baseFontSize: number = 16; inspectionContainerWidth: number = 40; inspectionContainerHeight: number = (window.innerHeight - 10.5 * this.baseFontSize); @@ -255,8 +257,8 @@ export class ParametersService { selection.tileId, selection.layerId, selection.mapId, - selection.address.toString(), - selection.featureIds, + selection.address ? selection.address.toString() : "", + selection.featureIds ? selection.featureIds : "", ]; this.parameters.next(this.p()); } diff --git a/erdblick_app/app/rightclickmenu.service.ts b/erdblick_app/app/rightclickmenu.service.ts new file mode 100644 index 00000000..20138135 --- /dev/null +++ b/erdblick_app/app/rightclickmenu.service.ts @@ -0,0 +1,21 @@ +import {Injectable} from "@angular/core"; +import {MenuItem} from "primeng/api"; +import {BehaviorSubject} from "rxjs"; + +@Injectable() +export class RightClickMenuService { + + menuItems: MenuItem[]; + tileIdsReady: BehaviorSubject = new BehaviorSubject(false); + tileSourceDataDialogVisible: boolean = false; + + constructor() { + this.menuItems = [{ + label: 'Tile Source Data', + icon: 'pi pi-database', + command: () => { + this.tileSourceDataDialogVisible = true; + } + }]; + } +} \ No newline at end of file diff --git a/erdblick_app/app/sourcedata.panel.component.ts b/erdblick_app/app/sourcedata.panel.component.ts index 9bac6a13..94551c12 100644 --- a/erdblick_app/app/sourcedata.panel.component.ts +++ b/erdblick_app/app/sourcedata.panel.component.ts @@ -233,26 +233,28 @@ export class SourceDataPanelComponent implements OnInit, AfterViewInit, OnDestro } } - selectItemWithAddress(address: bigint) { + selectItemWithAddress(address?: bigint) { let addressInRange: any; - if (this.addressFormat == coreLib.SourceDataAddressFormat.BIT_RANGE) { - const searchAddress = { - offset: address >> BigInt(32) & BigInt(0xFFFFFFFF), - size: address & BigInt(0xFFFFFFFF), - } + if (address) { + if (this.addressFormat == coreLib.SourceDataAddressFormat.BIT_RANGE) { + const searchAddress = { + offset: address >> BigInt(32) & BigInt(0xFFFFFFFF), + size: address & BigInt(0xFFFFFFFF), + } - const addressLow = typeof searchAddress === 'object' ? searchAddress['offset'] : searchAddress; - const addressHigh = addressLow + (typeof searchAddress === 'object' ? searchAddress['size'] : searchAddress); + const addressLow = typeof searchAddress === 'object' ? searchAddress['offset'] : searchAddress; + const addressHigh = addressLow + (typeof searchAddress === 'object' ? searchAddress['size'] : searchAddress); - addressInRange = (address: any) => { - return address.offset >= addressLow && - address.offset + address.size <= addressHigh && - (address.size != 0 || addressLow == addressHigh); - } - } else { - const searchAddress = address; - addressInRange = (address: any) => { - return address == searchAddress; + addressInRange = (address: any) => { + return address.offset >= addressLow && + address.offset + address.size <= addressHigh && + (address.size != 0 || addressLow == addressHigh); + } + } else { + const searchAddress = address; + addressInRange = (address: any) => { + return address == searchAddress; + } } } @@ -268,7 +270,7 @@ export class SourceDataPanelComponent implements OnInit, AfterViewInit, OnDestro node.data.styleClass = "highlight"; } - if (node.data.address && addressInRange(node.data.address)) { + if (node.data.address && addressInRange && addressInRange(node.data.address)) { highlight = true; if (!firstHighlightedItemIndex) @@ -277,7 +279,7 @@ export class SourceDataPanelComponent implements OnInit, AfterViewInit, OnDestro node.data.styleClass = "highlight"; parents.forEach((parent: TreeTableNode) =>{ parent.expanded = true; - }) + }); } if (node.children) { diff --git a/erdblick_app/app/tilesources.component.ts b/erdblick_app/app/tilesources.component.ts new file mode 100644 index 00000000..089f0aa1 --- /dev/null +++ b/erdblick_app/app/tilesources.component.ts @@ -0,0 +1,140 @@ +import {Component} from "@angular/core"; +import {ParametersService} from "./parameters.service"; +import {RightClickMenuService} from "./rightclickmenu.service"; +import {MapService} from "./map.service"; +import {SourceDataPanelComponent} from "./sourcedata.panel.component"; +import {InspectionService} from "./inspection.service"; + + +@Component({ + selector: 'tilesources', + template: ` + +
+ +
+
+

{{ errorString }}

+ + + +
+ + +
+
+
+ `, + styles: [``] +}) +export class TileSourceDataComponent { + selectedTileId: any | undefined; + tileIds: any[] = []; + selectedSourceDataLayer: any | undefined; + sourceDataLayers: any[] = []; + selectedMapId: any | undefined; + mapIds: any[] = []; + errorString: string = ""; + loading: boolean = true; + + constructor(private parameterService: ParametersService, + private mapService: MapService, + private inspectionService: InspectionService, + public menuService: RightClickMenuService) { + this.menuService.tileIdsReady.subscribe(ready => { + if (ready) { + this.load(); + } + this.loading = !ready; + }); + } + + load() { + const tileIds = this.parameterService.tileIdsForSourceData; + if (tileIds.length) { + this.tileIds = tileIds; + } else { + this.tileIds = []; + this.selectedTileId = undefined; + this.errorString = "No tile IDs available for the clicked position!"; + } + this.mapIds = []; + this.sourceDataLayers = []; + } + + onTileIdChange(tileId: any) { + this.selectedMapId = undefined; + this.selectedSourceDataLayer = undefined; + const maps = new Set(); + for (const featureTile of this.mapService.loadedTileLayers.values()) { + if (featureTile.tileId == tileId.id) { + maps.add(featureTile.mapName); + } + } + this.mapIds = [...maps].map(mapId => ({ id: mapId, name: mapId })); + this.sourceDataLayers = []; + } + + onMapIdChange(mapId: any) { + // Reset sourceDataLayer selection + this.selectedSourceDataLayer = undefined; + + // Update sourceDataLayers based on the selected mapId + const map = this.mapService.maps.getValue().get(mapId.id); + if (map) { + const dataLayers = new Set(); + for (const layer of map.layers.values()) { + if (layer.type == "SourceData") { + dataLayers.add(layer.layerId); + } + } + this.sourceDataLayers = [...dataLayers].map(layerId => ({ + id: layerId, + name: SourceDataPanelComponent.layerNameForLayerId(layerId) + })); + } + } + + requestSourceData() { + this.inspectionService.isInspectionPanelVisible = true; + this.inspectionService.selectedSourceData.next({ + tileId: Number(this.selectedTileId.id), + layerId: String(this.selectedSourceDataLayer.id), + mapId: String(this.selectedMapId.id) + }); + this.close(); + } + + reset() { + this.loading = true; + this.errorString = ""; + this.selectedTileId = undefined; + this.selectedMapId = undefined; + this.selectedSourceDataLayer = undefined; + } + + close() { + this.menuService.tileSourceDataDialogVisible = false; + } +} \ No newline at end of file diff --git a/erdblick_app/app/view.component.ts b/erdblick_app/app/view.component.ts index ebace4c4..8be759a8 100644 --- a/erdblick_app/app/view.component.ts +++ b/erdblick_app/app/view.component.ts @@ -16,7 +16,7 @@ import { defined } from "./cesium"; import {ParametersService} from "./parameters.service"; -import {AfterViewInit, Component} from "@angular/core"; +import {AfterViewInit, Component, OnInit} from "@angular/core"; import {MapService} from "./map.service"; import {DebugWindow, ErdblickDebugApi} from "./debugapi.component"; import {StyleService} from "./style.service"; @@ -28,6 +28,8 @@ import {SearchResultPosition} from "./featurefilter.worker"; import {InspectionService} from "./inspection.service"; import {KeyboardService} from "./keyboard.service"; import {coreLib} from "./wasm"; +import {MenuItem} from "primeng/api"; +import {RightClickMenuService} from "./rightclickmenu.service"; // Redeclare window with extended interface declare let window: DebugWindow; @@ -35,7 +37,9 @@ declare let window: DebugWindow; @Component({ selector: 'erdblick-view', template: ` -
+
+ + `, styles: [` @media only screen and (max-width: 56em) { @@ -46,12 +50,14 @@ declare let window: DebugWindow; } `] }) -export class ErdblickViewComponent implements AfterViewInit { +export class ErdblickViewComponent implements OnInit, AfterViewInit { viewer!: Viewer; private mouseHandler: ScreenSpaceEventHandler | null = null; private openStreetMapLayer: ImageryLayer | null = null; private marker: Entity | null = null; + items: MenuItem[] | undefined; + /** * Construct a Cesium View with a Model. * @param mapService The map model service providing access to data @@ -68,6 +74,7 @@ export class ErdblickViewComponent implements AfterViewInit { public jumpService: JumpTargetService, public inspectionService: InspectionService, public keyboardService: KeyboardService, + public menuService: RightClickMenuService, public coordinatesService: CoordinatesService) { this.mapService.tileVisualizationTopic.subscribe((tileVis: TileVisualization) => { @@ -99,6 +106,10 @@ export class ErdblickViewComponent implements AfterViewInit { }); } + ngOnInit() { + this.items = this.menuService.menuItems; + } + ngAfterViewInit() { this.viewer = new Viewer("mapViewContainer", { @@ -123,6 +134,27 @@ export class ErdblickViewComponent implements AfterViewInit { this.mouseHandler = new ScreenSpaceEventHandler(this.viewer.scene.canvas); + this.mouseHandler.setInputAction((movement: any) => { + const position = movement.position; + const cartesian = this.viewer.camera.pickEllipsoid( + new Cartesian2(position.x, position.y), + this.viewer.scene.globe.ellipsoid + ); + if (defined(cartesian)) { + const cartographic = Cartographic.fromCartesian(cartesian); + const longitude = CesiumMath.toDegrees(cartographic.longitude); + const latitude = CesiumMath.toDegrees(cartographic.latitude); + this.parameterService.tileIdsForSourceData = [...Array(16).keys()].map(level => { + const tileId = coreLib.getTileIdFromPosition(longitude, latitude, level); + return {id: tileId, name: `${tileId} (level ${level})`}; + }); + this.menuService.tileIdsReady.next(true); + } else { + this.parameterService.tileIdsForSourceData = []; + this.menuService.tileIdsReady.next(false); + } + }, ScreenSpaceEventType.RIGHT_DOWN); + // Add a handler for selection. this.mouseHandler.setInputAction((movement: any) => { const position = movement.position; @@ -145,6 +177,9 @@ export class ErdblickViewComponent implements AfterViewInit { }); } } + if (!defined(feature)) { + this.inspectionService.isInspectionPanelVisible = false; + } this.mapService.highlightFeatures( Array.isArray(feature?.id) ? feature.id : [feature?.id], false, diff --git a/erdblick_app/styles.scss b/erdblick_app/styles.scss index 58b36d44..9c71408f 100644 --- a/erdblick_app/styles.scss +++ b/erdblick_app/styles.scss @@ -1042,6 +1042,16 @@ body { } } +.tilesource-options { + display:flex; + flex-direction: column; + gap: 0.5em; + + .p-dropdown { + width: 100%; + } +} + @media only screen and (max-width: 56em) { .help-button { width: 3em;