diff --git a/erdblick_app/app/jump.service.ts b/erdblick_app/app/jump.service.ts index b8c48e85..64c01204 100644 --- a/erdblick_app/app/jump.service.ts +++ b/erdblick_app/app/jump.service.ts @@ -8,6 +8,8 @@ import {coreLib} from "./wasm"; import {FeatureSearchService} from "./feature.search.service"; import {SidePanelService, SidePanelState} from "./sidepanel.service"; import {HighlightMode} from "build/libs/core/erdblick-core"; +import {InspectionService} from "./inspection.service"; +import {RightClickMenuService} from "./rightclickmenu.service"; export interface SearchTarget { icon: string; @@ -46,6 +48,8 @@ export class JumpTargetService { private mapService: MapService, private messageService: InfoMessageService, private sidePanelService: SidePanelService, + private inspectionService: InspectionService, + private menuService: RightClickMenuService, private searchService: FeatureSearchService) { this.httpClient.get("/config.json", {responseType: 'json'}).subscribe({ next: (data: any) => { @@ -108,6 +112,124 @@ export class JumpTargetService { } } + validateMapgetTileId(value: string) { + return value.length > 0 && !/\s/g.test(value.trim()) && !isNaN(+value.trim()); + } + + parseMapgetTileId(value: string): number[] | undefined { + if (!value) { + this.messageService.showError("No value provided!"); + return; + } + try { + let wgs84TileId = BigInt(value); + let position = coreLib.getTilePosition(wgs84TileId); + return [position.x, position.y, position.z] + } catch (e) { + this.messageService.showError("Possibly malformed TileId: " + (e as Error).message.toString()); + } + return undefined; + } + + getInspectTileSourceDataTarget() { + const searchString = this.targetValueSubject.getValue(); + let label = "tileId = ? | (mapId = ?) | (sourceLayerId = ?)"; + + const matchSourceDataElements = (value: string) => { + const regex = /^\s*(\d+)\s*(?:[,\s;]+)?\s*([^\s,;]*)\s*(?:[,\s;]+)?\s*([^\s,;]*)?\s*$/; + const match = value.match(regex); + let tileId: bigint | null = null; + let mapId = null; + let sourceLayerId = null; + let valid = true; + + if (match) { + const [_, bigintStr, str1, str2] = match; + try { + tileId = BigInt(bigintStr); + valid = this.validateMapgetTileId(tileId.toString()); + } catch { + valid = false; + } + + // TODO: check whether the mapId and layerId are valid + if (str1) { + mapId = str1; + } + if (str2) { + sourceLayerId = str2; + } + } else { + valid = false; + } + + if (!tileId || !valid) { + return null; + } + + + + return [tileId, mapId, sourceLayerId] + } + + const matches = matchSourceDataElements(searchString); + if (matches) { + const [tileId, mapId, sourceLayerId] = matches; + if (tileId) { + label = `tileId = ${tileId}`; + if (mapId) { + label = `${label} | mapId = ${mapId}`; + if (sourceLayerId) { + label = `${label} | sourceLayerId = ${sourceLayerId}`; + } else { + label = `${label} | (sourceLayerId = ?)`; + } + } else { + label = `${label} | (mapId = ?) | (sourceLayerId = ?)` + } + } else { + label += `
Insufficient parameters`; + } + } + + return { + icon: "pi-database", + color: "red", + name: "Inspect Mapget Tile", + label: label, + enabled: false, + execute: (value: string) => { + const matches = matchSourceDataElements(value); + if (matches) { + const [tileId, mapId, sourceLayerId] = matches; + try { + if (tileId) { + if (mapId) { + if (sourceLayerId) { + this.inspectionService.loadSourceDataInspection( + Number(tileId), + String(mapId), + String(sourceLayerId) + ) + } else { + this.menuService.customTileAndMapId.next([String(tileId), String(mapId)]); + } + } else { + this.menuService.customTileAndMapId.next([String(tileId), ""]); + } + } + } catch (e) { + this.messageService.showError(String(e)); + } + } + }, + validate: (value: string) => { + const matches = matchSourceDataElements(value); + return matches && matches.length && matches[0]; + } + } + } + update() { let featureJumpTargets = this.mapService.tileParser?.filterFeatureJumpTargets(this.targetValueSubject.getValue()); let featureJumpTargetsConverted = []; @@ -123,7 +245,12 @@ export class JumpTargetService { name: `Jump to ${fjt.name}`, label: label, enabled: !fjt.error, - execute: (_: string) => { this.highlightByJumpTarget(fjt).then(); }, + execute: (_: string) => { + if (fjt.name.toLowerCase().includes("tileid")) { + + } + this.highlightByJumpTarget(fjt).then(); + }, validate: (_: string) => { return !fjt.error; }, } }); @@ -131,6 +258,7 @@ export class JumpTargetService { this.jumpTargets.next([ this.getFeatureMatchTarget(), + this.getInspectTileSourceDataTarget(), ...featureJumpTargetsConverted, ...this.extJumpTargets ]); diff --git a/erdblick_app/app/map.service.ts b/erdblick_app/app/map.service.ts index df9e8062..57c9d831 100644 --- a/erdblick_app/app/map.service.ts +++ b/erdblick_app/app/map.service.ts @@ -92,7 +92,6 @@ export class MapService { private tileVisualizationQueue: [string, TileVisualization][]; private selectionVisualizations: TileVisualization[]; private hoverVisualizations: TileVisualization[]; - private specialTileBorderColourForTiles: [bigint, Color] = [-1n, Color.TRANSPARENT]; tileParser: TileLayerParser|null = null; tileVisualizationTopic: Subject; @@ -428,10 +427,6 @@ export class MapService { return false; } tileVisu.showTileBorder = this.getMapLayerBorderState(mapName, layerName); - if (this.specialTileBorderColourForTiles[0] == tileVisu.tile.tileId) { - tileVisu.showTileBorder = true; - tileVisu.specialBorderColour = this.specialTileBorderColourForTiles[1]; - } tileVisu.isHighDetail = this.currentHighDetailTileIds.has(tileVisu.tile.tileId) || tileVisu.tile.preventCulling; return true; }); @@ -810,9 +805,4 @@ export class MapService { } } } - - setSpecialTileBorder(tileId: bigint, color: Color) { - this.specialTileBorderColourForTiles = [tileId, color]; - this.update(); - } } diff --git a/erdblick_app/app/parameters.service.ts b/erdblick_app/app/parameters.service.ts index 84851dee..b4dfa3a0 100644 --- a/erdblick_app/app/parameters.service.ts +++ b/erdblick_app/app/parameters.service.ts @@ -499,10 +499,11 @@ export class ParametersService { this.saveHistoryStateValue(value); } } + console.log(value); this.p().search = value ? value : []; this._replaceUrl = false; - this.parameters.next(this.p()) this.lastSearchHistoryEntry.next(value); + this.parameters.next(this.p()) } private saveHistoryStateValue(value: [number, string]) { diff --git a/erdblick_app/app/rightclickmenu.service.ts b/erdblick_app/app/rightclickmenu.service.ts index 63a93bfd..e2f9857b 100644 --- a/erdblick_app/app/rightclickmenu.service.ts +++ b/erdblick_app/app/rightclickmenu.service.ts @@ -2,6 +2,7 @@ import {Injectable} from "@angular/core"; import {MenuItem} from "primeng/api"; import {BehaviorSubject, Subject} from "rxjs"; import {InspectionService} from "./inspection.service"; +import {Entity} from "./cesium"; export interface SourceDataDropdownOption { id: bigint | string, @@ -12,26 +13,30 @@ export interface SourceDataDropdownOption { @Injectable() export class RightClickMenuService { - menuItems: MenuItem[]; + menuItems: BehaviorSubject = new BehaviorSubject([]); tileSourceDataDialogVisible: boolean = false; lastInspectedTileSourceDataOption: BehaviorSubject<{tileId: number, mapId: string, layerId: string} | null> = new BehaviorSubject<{tileId: number, mapId: string, layerId: string} | null>(null); tileIdsForSourceData: Subject = new Subject(); + tileOutiline: Subject = new Subject(); + customTileAndMapId: Subject<[string, string]> = new Subject<[string, string]>(); constructor(private inspectionService: InspectionService) { - this.menuItems = [{ + this.menuItems.next([{ label: 'Inspect Source Data for Tile', icon: 'pi pi-database', command: () => { this.tileSourceDataDialogVisible = true; } - }]; + }]); this.lastInspectedTileSourceDataOption.subscribe(lastInspectedTileSourceData => { + const items = this.menuItems.getValue(); if (lastInspectedTileSourceData) { this.updateMenuForLastInspectedSourceData(lastInspectedTileSourceData); - } else if (this.menuItems.length > 1) { - this.menuItems.shift(); + } else if (items.length > 1) { + items.shift(); + this.menuItems.next(items); } }); } @@ -48,11 +53,12 @@ export class RightClickMenuService { ); } }; - - if (this.menuItems.length > 1) { - this.menuItems[0] = menuItem; + const items = this.menuItems.getValue(); + if (items.length > 1) { + items[0] = menuItem; } else { - this.menuItems.unshift(menuItem); + items.unshift(menuItem); } + this.menuItems.next(items); } } \ No newline at end of file diff --git a/erdblick_app/app/search.panel.component.ts b/erdblick_app/app/search.panel.component.ts index aa47c29e..886421df 100644 --- a/erdblick_app/app/search.panel.component.ts +++ b/erdblick_app/app/search.panel.component.ts @@ -9,6 +9,7 @@ import {SidePanelService, SidePanelState} from "./sidepanel.service"; import {Dialog} from "primeng/dialog"; import {KeyboardService} from "./keyboard.service"; import {distinctUntilChanged} from "rxjs"; +import {RightClickMenuService} from "./rightclickmenu.service"; interface ExtendedSearchTarget extends SearchTarget { index: number; @@ -116,7 +117,7 @@ export class SearchPanelComponent implements AfterViewInit { const targetsArray: Array = []; const value = this.searchInputValue.trim(); let label = "tileId = ?"; - if (this.validateMapgetTileId(value)) { + if (this.jumpToTargetService.validateMapgetTileId(value)) { label = `tileId = ${value}`; } else { label += `
Insufficient parameters`; @@ -127,8 +128,8 @@ export class SearchPanelComponent implements AfterViewInit { name: "Mapget Tile ID", label: label, enabled: false, - jump: (value: string) => { return this.parseMapgetTileId(value) }, - validate: (value: string) => { return this.validateMapgetTileId(value) } + jump: (value: string) => { return this.jumpToTargetService.parseMapgetTileId(value) }, + validate: (value: string) => { return this.jumpToTargetService.validateMapgetTileId(value) } }); label = "lon = ? | lat = ? | (level = ?)" if (this.validateWGS84(value, true)) { @@ -203,6 +204,7 @@ export class SearchPanelComponent implements AfterViewInit { private keyboardService: KeyboardService, private messageService: InfoMessageService, private jumpToTargetService: JumpTargetService, + private menuService: RightClickMenuService, private sidePanelService: SidePanelService) { this.keyboardService.registerShortcuts(["Ctrl+k", "Ctrl+K"], this.clickOnSearchToStart.bind(this)); this.clickListener = this.renderer.listen('document', 'click', this.handleClickOut.bind(this)); @@ -233,7 +235,11 @@ export class SearchPanelComponent implements AfterViewInit { this.parametersService.parameters.pipe(distinctUntilChanged()).subscribe(parameters => { if (parameters.search.length) { const lastEntry = this.parametersService.lastSearchHistoryEntry.getValue(); - if (lastEntry && parameters.search[0] != lastEntry[0] && parameters.search[1] != lastEntry[1]) { + if (lastEntry) { + if (parameters.search[0] != lastEntry[0] && parameters.search[1] != lastEntry[1]) { + this.parametersService.lastSearchHistoryEntry.next(parameters.search); + } + } else { this.parametersService.lastSearchHistoryEntry.next(parameters.search); } } @@ -256,6 +262,21 @@ export class SearchPanelComponent implements AfterViewInit { this.reloadSearchHistory(); }); + this.menuService.lastInspectedTileSourceDataOption.subscribe(lastInspectedData => { + if (lastInspectedData && lastInspectedData.tileId && lastInspectedData.mapId && lastInspectedData.layerId) { + const value = `${lastInspectedData?.tileId} ${lastInspectedData?.mapId} ${lastInspectedData?.layerId}`; + for (let i = 0; i < this.searchItems.length; i++) { + if (!this.searchItems[i].name.toLowerCase().includes("features") && + this.searchItems[i].validate(value)) { + console.log("VALIDATED") + console.log("SET HISTORY") + this.parametersService.setSearchHistoryState([i, value]); + break; + } + } + } + }); + this.reloadSearchHistory(); } @@ -298,21 +319,6 @@ export class SearchPanelComponent implements AfterViewInit { } } - parseMapgetTileId(value: string): number[] | undefined { - if (!value) { - this.messageService.showError("No value provided!"); - return; - } - try { - let wgs84TileId = BigInt(value); - let position = coreLib.getTilePosition(wgs84TileId); - return [position.x, position.y, position.z] - } catch (e) { - this.messageService.showError("Possibly malformed TileId: " + (e as Error).message.toString()); - } - return undefined; - } - parseWgs84Coordinates(coordinateString: string, isLonLat: boolean): number[] | undefined { let lon = 0; let lat = 0; @@ -442,10 +448,6 @@ export class SearchPanelComponent implements AfterViewInit { ); } - validateMapgetTileId(value: string) { - return value.length > 0 && !/\s/g.test(value.trim()) && !isNaN(+value.trim()); - } - validateWGS84(value: string, isLonLat: boolean = false) { const coords = this.parseWgs84Coordinates(value, isLonLat); return coords !== undefined && coords[0] >= -90 && coords[0] <= 90 && coords[1] >= -180 && coords[1] <= 180; diff --git a/erdblick_app/app/sourcedata.panel.component.ts b/erdblick_app/app/sourcedata.panel.component.ts index 3ac0f351..18826ed5 100644 --- a/erdblick_app/app/sourcedata.panel.component.ts +++ b/erdblick_app/app/sourcedata.panel.component.ts @@ -282,8 +282,13 @@ export class SourceDataPanelComponent implements OnInit, AfterViewInit, OnDestro }); } - if (address === undefined && node.children?.length == 1) { + if (address === undefined && node.children && node.children.length < 5) { node.expanded = true; + for (const child of node.children) { + if (child.children && child.children.length < 5) { + child.expanded = true; + } + } } if (node.children) { @@ -297,7 +302,14 @@ export class SourceDataPanelComponent implements OnInit, AfterViewInit, OnDestro if (address === undefined) { for (const item of this.treeData) { - item.expanded = true; + if (item.children) { + item.expanded = true; + for (const child of item.children) { + if (child.children && child.children.length < 5) { + child.expanded = true; + } + } + } } } diff --git a/erdblick_app/app/sourcedataselection.dialog.component.ts b/erdblick_app/app/sourcedataselection.dialog.component.ts index dd1406df..c6ed8b75 100644 --- a/erdblick_app/app/sourcedataselection.dialog.component.ts +++ b/erdblick_app/app/sourcedataselection.dialog.component.ts @@ -4,7 +4,8 @@ import {RightClickMenuService, SourceDataDropdownOption} from "./rightclickmenu. import {MapService} from "./map.service"; import {SourceDataPanelComponent} from "./sourcedata.panel.component"; import {InspectionService} from "./inspection.service"; -import {Color} from "./cesium"; +import {CallbackProperty, Color, HeightReference, Rectangle} from "./cesium"; +import {coreLib} from "./wasm"; @Component({ selector: 'sourcedatadialog', @@ -29,7 +30,7 @@ import {Color} from "./cesium"; pInputText [(ngModel)]="customTileId" (ngModelChange)="onCustomTileIdChange($event)"/> + label="" [pTooltip]="showCustomTileIdInput ? 'Reset custom Tile ID' : 'Enter custom Tile ID'" tooltipPosition="bottom" tabindex="0"> @@ -73,10 +74,10 @@ export class SourceDataLayerSelectionDialogComponent { errorString: string = ""; loading: boolean = true; customTileId: string = ""; + customMapId: string = ""; showCustomTileIdInput: boolean = false; - constructor(private parameterService: ParametersService, - private mapService: MapService, + constructor(private mapService: MapService, private inspectionService: InspectionService, public menuService: RightClickMenuService) { this.menuService.tileIdsForSourceData.subscribe(data => { @@ -84,14 +85,26 @@ export class SourceDataLayerSelectionDialogComponent { this.loading = !data.length; this.load(); }); + this.menuService.customTileAndMapId.subscribe(([tileId, mapId]: [string, string]) => { + this.load(tileId.length > 0, tileId, mapId); + this.menuService.tileSourceDataDialogVisible = true; + }); } - load() { - this.showCustomTileIdInput = false; - this.customTileId = ""; + load(withCustomTileId: boolean = false, customTileId: string = "", customMapId: string = "") { + this.showCustomTileIdInput = withCustomTileId; + this.customTileId = customTileId; + this.customMapId = customMapId; this.mapIds = []; this.sourceDataLayers = []; this.loading = false; + this.menuService.tileOutiline.next(null); + if (withCustomTileId && customTileId) { + const tileId = BigInt(customTileId); + this.triggerModelChange({id: tileId, name: customTileId}); + return; + } + if (!this.tileIds.length) { this.selectedTileId = undefined; this.errorString = "No tile IDs available for the clicked position!"; @@ -100,7 +113,7 @@ export class SourceDataLayerSelectionDialogComponent { for (let i = 0; i < this.tileIds.length; i++) { const id = this.tileIds[i].id as bigint; - const maps = this.findMapsForTileId(id); + const maps = [...this.findMapsForTileId(id)]; this.tileIds[i]["disabled"] = !maps.length; this.mapIdsMap.set(id, maps); } @@ -111,15 +124,25 @@ export class SourceDataLayerSelectionDialogComponent { this.triggerModelChange(this.selectedTileId); } - findMapsForTileId(tileId: bigint) { - // TODO: Load the tile if not loaded. - const maps = new Set(); - for (const featureTile of this.mapService.loadedTileLayers.values()) { - if (featureTile.tileId == tileId && featureTile.numFeatures > 0) { - maps.add(featureTile.mapName); + *findMapsForTileId(tileId: bigint): Generator { + const level = coreLib.getTileLevel(tileId); + for (const [mapId, mapInfo] of this.mapService.maps.getValue().entries()) { + for (const [_, layerInfo] of mapInfo.layers.entries()) { + if (layerInfo.type == "SourceData") { + if (layerInfo.zoomLevels.includes(level)) { + yield { id: mapId, name: mapId }; + break; + } else { + for (const featureTile of this.mapService.loadedTileLayers.values()) { + if (featureTile.tileId == tileId) { + yield { id: mapId, name: mapId }; + break; + } + } + } + } } } - return [...maps].map(mapId => ({ id: mapId, name: mapId })); } onCustomTileIdChange(tileIdString: string) { @@ -130,13 +153,26 @@ export class SourceDataLayerSelectionDialogComponent { } const tileId = BigInt(tileIdString); - const maps = this.findMapsForTileId(tileId); + const maps = [...this.findMapsForTileId(tileId)]; this.mapIdsMap.set(tileId, maps); this.triggerModelChange({id: tileId, name: tileIdString}); } private triggerModelChange(tileId: SourceDataDropdownOption) { this.onTileIdChange(tileId); + if (this.customMapId.length) { + const mapId = { id: this.customMapId, name: this.customMapId }; + if (!this.mapIds.includes(mapId)) { + this.mapIds.push(mapId); + } + this.selectedMapId = mapId; + this.onMapIdChange(this.selectedMapId); + if (this.sourceDataLayers.length) { + this.selectedSourceDataLayer = this.sourceDataLayers[0]; + this.onLayerIdChange(this.selectedSourceDataLayer); + } + return; + } if (this.mapIds.length) { this.selectedMapId = this.mapIds[0]; this.onMapIdChange(this.selectedMapId); @@ -151,13 +187,7 @@ export class SourceDataLayerSelectionDialogComponent { this.selectedMapId = undefined; this.selectedSourceDataLayer = undefined; this.sourceDataLayers = []; - // TODO: Fix this. - // Consider just drawing a tile box rectangle without visualising the tile. - // for (const featureTile of this.mapService.loadedTileLayers.values()) { - // if (featureTile.tileId == tileId.id as bigint) { - // this.mapService.setSpecialTileBorder(tileId.id as bigint, Color.HOTPINK); - // } - // } + this.outlineTheTileBox(BigInt(tileId.id), Color.HOTPINK); const mapIds = this.mapIdsMap.get(tileId.id as bigint); if (mapIds !== undefined) { this.mapIds = mapIds.sort((a, b) => a.name.localeCompare(b.name)); @@ -170,6 +200,22 @@ export class SourceDataLayerSelectionDialogComponent { } } + outlineTheTileBox(tileId: bigint, color: Color) { + this.menuService.tileOutiline.next(null); + const tileBox = coreLib.getTileBox(tileId); + const entity = { + rectangle: { + coordinates: Rectangle.fromDegrees(...tileBox), + height: HeightReference.CLAMP_TO_GROUND, + material: Color.TRANSPARENT, + outlineWidth: 2, + outline: true, + outlineColor: color.withAlpha(0.5) + } + } + this.menuService.tileOutiline.next(entity); + } + findLayersForMapId(mapId: string) { const map = this.mapService.maps.getValue().get(mapId); if (map) { @@ -195,7 +241,9 @@ export class SourceDataLayerSelectionDialogComponent { } } - onLayerIdChange(layerId: SourceDataDropdownOption) { + onLayerIdChange(_: SourceDataDropdownOption) {} + + requestSourceData() { if (this.selectedTileId === undefined || this.selectedMapId === undefined || this.selectedSourceDataLayer === undefined) { @@ -206,15 +254,12 @@ export class SourceDataLayerSelectionDialogComponent { mapId: String(this.selectedMapId.id), layerId: String(this.selectedSourceDataLayer.id) }); - } - - requestSourceData() { - const tileId = this.customTileId ? this.customTileId : this.selectedTileId?.id; - this.inspectionService.loadSourceDataInspection( - Number(tileId), - String(this.selectedMapId?.id), - String(this.selectedSourceDataLayer?.id) - ); + // const tileId = this.customTileId ? this.customTileId : this.selectedTileId?.id; + // this.inspectionService.loadSourceDataInspection( + // Number(tileId), + // String(this.selectedMapId?.id), + // String(this.selectedSourceDataLayer?.id) + // ); this.close(); } diff --git a/erdblick_app/app/view.component.ts b/erdblick_app/app/view.component.ts index cba5d14b..ba88f832 100644 --- a/erdblick_app/app/view.component.ts +++ b/erdblick_app/app/view.component.ts @@ -38,7 +38,7 @@ declare let window: DebugWindow; selector: 'erdblick-view', template: `
- + `, styles: [` @@ -55,6 +55,8 @@ export class ErdblickViewComponent implements AfterViewInit { private mouseHandler: ScreenSpaceEventHandler | null = null; private openStreetMapLayer: ImageryLayer | null = null; private marker: Entity | null = null; + private tileOutlineEntity: Entity | null = null; + menuItems: MenuItem[] = []; /** * Construct a Cesium View with a Model. @@ -102,6 +104,10 @@ export class ErdblickViewComponent implements AfterViewInit { } ); }); + + this.menuService.menuItems.subscribe(items => { + this.menuItems = [...items]; + }); } ngAfterViewInit() { @@ -171,6 +177,7 @@ export class ErdblickViewComponent implements AfterViewInit { } if (!defined(feature)) { this.inspectionService.isInspectionPanelVisible = false; + this.menuService.tileOutiline.next(null); } this.mapService.highlightFeatures( Array.isArray(feature?.id) ? feature.id : [feature?.id], @@ -293,6 +300,14 @@ export class ErdblickViewComponent implements AfterViewInit { if (spinner) { spinner.style.display = 'none'; } + + this.menuService.tileOutiline.subscribe(entity => { + if (entity) { + this.tileOutlineEntity = this.viewer.entities.add(entity); + } else if (this.tileOutlineEntity) { + this.viewer.entities.remove(this.tileOutlineEntity); + } + }); } /** diff --git a/erdblick_app/styles.scss b/erdblick_app/styles.scss index 9bfe58a9..7c57110f 100644 --- a/erdblick_app/styles.scss +++ b/erdblick_app/styles.scss @@ -42,7 +42,7 @@ body { } .p-contextmenu { - width: 20em; + width: 22em; } } @@ -459,6 +459,10 @@ body { background-color: var(--primary-color); } + .red { + background-color: indianred; + } + .orange { background-color: darkorange; }