diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 1820413b..99a0b923 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -2,8 +2,6 @@ name: build-release on: push: - branches: - - 'feature/renderer-wasm-demo' pull_request: workflow_dispatch: diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 00000000..46955151 --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,27 @@ +name: build-test + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + build-test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install build dependencies + run: sudo apt-get install ninja-build + + - name: Compile + run: | + mkdir build-test && cd build-test + cmake -GNinja -DCMAKE_BUILD_TYPE=Debug .. + cmake --build . + + - name: Run Tests + run: | + cd build-test/test + ctest --verbose --no-tests=error diff --git a/CMakeLists.txt b/CMakeLists.txt index 773c2de3..225ca521 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ message("Building for ${CMAKE_SYSTEM_NAME}.") FetchContent_Declare(mapget GIT_REPOSITORY "https://github.com/Klebert-Engineering/mapget" - GIT_TAG "main" + GIT_TAG "v2024.4.0" GIT_SHALLOW ON) FetchContent_MakeAvailable(mapget) @@ -33,17 +33,15 @@ FetchContent_MakeAvailable(yaml-cpp) include(cmake/cesium.cmake) # Erdblick Core Library - add_subdirectory(libs/core) if(NOT ${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten") add_subdirectory(test) +else() + # Angular Build + add_custom_target(erdblick-ui ALL + COMMAND bash "${CMAKE_SOURCE_DIR}/build-ui.bash" "${CMAKE_SOURCE_DIR}" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + DEPENDS erdblick-core) endif() -# Angular Build - -add_custom_target(erdblick-ui ALL - COMMAND bash "${CMAKE_SOURCE_DIR}/build-ui.bash" "${CMAKE_SOURCE_DIR}" - WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" - DEPENDS erdblick-core -) diff --git a/README.md b/README.md index ade0dee5..91294be2 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Each rule within the YAML `rules` array can have the following fields: |-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|--------------------------------------| | `geometry` | List of geometry type(s) or single type the rule applies to. | At least one of `"point"`,`"mesh"`, `"line"`, `"polygon"`. | `["point", "mesh"]`, `line` | | `aspect` | Specifies the aspect to which the rule applies: `"feature"`, `"relation"`, or `"attribute"`. | String | `"feature"`, `"relation"` | -| `mode` | Specifies the mode: `"normal"` or `"highlight"`. | String | `"normal"`, `"highlight"` | +| `mode` | Specifies the highlight mode: `"none"` or `"hover"` or `"selection"`. | String | `"none"`, `"hover"` | | `type` | A regular expression to match against a feature type. | String | `"Lane\|Boundary"` | | `filter` | A [simfil](https://github.com/klebert-engineering/simfil) filter expression over the feature's JSON representation. | String | `*roadClass == 4` | | `selectable` | Indicates if the feature is selectable. | Boolean | `true`, `false` | @@ -80,6 +80,7 @@ Each rule within the YAML `rules` array can have the following fields: | `flat` | Clamps the feature to the ground (Does not work for meshes). | Boolean | `true`, `false` | | `outline-color` | Point outline color. | String | `green`, `#fff` | | `outline-width` | Point outline width in px. | Float | `3.6` | +| `point-merge-grid-cell` | WGS84/altutide meter tolerance for merging point visualizations. | Array of three Floats. | `[0.000000084, 0.000000084, 0.01]` | | `near-far-scale` | For points, indicate (`near-alt-meters`, `near-scale`, `far-alt-meters`, `far-scale`). | Array of four Floats. | `[1.5e2,10,8.0e6,0]` | | `offset` | Apply a fixed offset to each shape-point in meters. Can be used for z-ordering. | Array of three Floats. | `[0, 0, 5]` | | `arrow` | For arrow-heads: One of `none`, `forward`, `backward`, `double`. Not compatible with `dashed`. | String | `single` | @@ -230,6 +231,17 @@ For attributes, style expressions (e.g. `color-expression`) are evaluated in a c set the `offset` field. The spatial `offset` will be multiplied, so it is possible to "stack" attributes over a feature. +### About Merged Point Visualizations + +By setting `point-merge-grid-cell`, a tolerance may be defined which allows merging the visual representations +of point features which share the same 3D spatial cell, map, layer, and style rule. This has two advantages: + +* **Multi-Selection**: When selecting the merged representation, a multi-selection of all merged features happens. +* **Logical Evaluation using `$mergeCount`**: In some map formats, it may be desirable to apply a style based on the number of merged points. + This may be done to display a warning, or to check a matching requirement. + To this end, the `$mergeCount` variable is injected into each simfil evaluation context of a merged-point style rule. + Check out the default style for an example. + ### About `first-of` Normally, all style rules from a style sheet are naively applied to all matching features. diff --git a/VERSION b/VERSION index 4951ec3f..d6d6d28d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2024.3.2 \ No newline at end of file +2024.4.0 \ No newline at end of file diff --git a/cmake/cesium.cmake b/cmake/cesium.cmake index 651d4fd2..59a948ed 100644 --- a/cmake/cesium.cmake +++ b/cmake/cesium.cmake @@ -15,7 +15,7 @@ set(CESIUM_LIBS CesiumGltf CesiumGltfWriter) -# Use fetch content for cloning the repository durring +# Use fetch content for cloning the repository during # configure phase. We do not call `FetchContent_MakeAvailable`, # but instead use `ExternalProject_Add` to compile Cesium in # isolation. @@ -23,7 +23,10 @@ FetchContent_Declare(cesiumnative_src GIT_REPOSITORY "https://github.com/Klebert-Engineering/cesium-native.git" GIT_TAG "main" GIT_SUBMODULES_RECURSE YES - GIT_PROGRESS YES) + GIT_PROGRESS YES + PATCH_COMMAND git reset --hard HEAD && git -C extern/draco reset --hard HEAD && git apply "${CMAKE_CURRENT_SOURCE_DIR}/cmake/cesium.patch" + UPDATE_DISCONNECTED YES + UPDATE_COMMAND "") FetchContent_GetProperties(cesiumnative_src) if (NOT cesiumnative_src_POPULATED) @@ -49,6 +52,11 @@ foreach (lib ${CESIUM_LIBS}) endforeach() message(STATUS "cesium byproducts: ${CESIUM_BYPRODUCTS}") +set(CESIUM_EXTRA_ARGS) +if (CMAKE_TOOLCHAIN_FILE) + list(APPEND CESIUM_EXTRA_ARGS "-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}") +endif() + ExternalProject_Add(cesiumnative SOURCE_DIR ${cesiumnative_src_SOURCE_DIR} CMAKE_ARGS @@ -58,8 +66,8 @@ ExternalProject_Add(cesiumnative -DCESIUM_TRACING_ENABLED=OFF -DDRACO_JS_GLUE=OFF -DBUILD_SHARED_LIBS=OFF - -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + ${CESIUM_EXTRA_ARGS} BUILD_BYPRODUCTS ${CESIUM_BYPRODUCTS} INSTALL_COMMAND "" diff --git a/cmake/cesium.patch b/cmake/cesium.patch new file mode 100644 index 00000000..06f61e00 --- /dev/null +++ b/cmake/cesium.patch @@ -0,0 +1,47 @@ +index c928fdf..cf6a63b 100644 +--- a/extern/draco/src/draco/io/file_utils.h ++++ b/extern/draco/src/draco/io/file_utils.h +@@ -17,6 +17,7 @@ + + #include + #include ++#include + + namespace draco { + +diff --git a/CesiumAsync/include/CesiumAsync/CacheItem.h b/CesiumAsync/include/CesiumAsync/CacheItem.h +index 20d1ca80..bd47492b 100644 +--- a/CesiumAsync/include/CesiumAsync/CacheItem.h ++++ b/CesiumAsync/include/CesiumAsync/CacheItem.h +@@ -9,6 +9,7 @@ + #include + #include + #include ++#include + + namespace CesiumAsync { + +diff --git a/CesiumAsync/include/CesiumAsync/IAssetResponse.h b/CesiumAsync/include/CesiumAsync/IAssetResponse.h +index 10519057..0944b26b 100644 +--- a/CesiumAsync/include/CesiumAsync/IAssetResponse.h ++++ b/CesiumAsync/include/CesiumAsync/IAssetResponse.h +@@ -8,6 +8,7 @@ + #include + #include + #include ++#include + + namespace CesiumAsync { + +diff --git a/CesiumIonClient/src/fillWithRandomBytes.h b/CesiumIonClient/src/fillWithRandomBytes.h +index 55765c72..654d09df 100644 +--- a/CesiumIonClient/src/fillWithRandomBytes.h ++++ b/CesiumIonClient/src/fillWithRandomBytes.h +@@ -1,6 +1,7 @@ + #pragma once + + #include ++#include + + namespace CesiumIonClient { + diff --git a/config/styles/default-style.yaml b/config/styles/default-style.yaml index 1fbe89be..dad05c41 100644 --- a/config/styles/default-style.yaml +++ b/config/styles/default-style.yaml @@ -1,15 +1,68 @@ name: DefaultStyle version: 1.0 +options: + - label: Show Meshes/Polygons + id: showMesh + - label: Show Points + id: showPoint + - label: Show Lines + id: showLine + rules: + # Normal styles - geometry: ["mesh", "polygon"] + filter: showMesh color: teal opacity: 0.8 - - geometry: ["point", "line"] + offset: [0, 0, -0.4] + - geometry: ["line"] + filter: showLine color: moccasin opacity: 1.0 - width: 1.0 - - geometry: ["point", "line", "mesh", "polygon"] + width: 5.0 + offset: [0, 0, -0.2] + - geometry: ["point"] + point-merge-grid-cell: [0.000000084, 0.000000084, 0.01] + filter: showPoint + color-expression: "$mergeCount > 1 and 'red' or 'moccasin'" + label-text-expression: "$mergeCount > 1 and ($mergeCount as string) or ''" + label-color: black + label-font: "12px Helvetica" + opacity: 1.0 + width: 15.0 + + # Hover/Selection styles + - geometry: ["mesh", "polygon"] + color: orange + opacity: 1.0 + mode: hover + offset: [0, 0, -0.3] + - geometry: ["line"] + color: orange + opacity: 1.0 + width: 10.0 + mode: hover + offset: [0, 0, -0.1] + - geometry: ["point"] + color: orange + opacity: 1.0 + width: 20.0 + mode: hover + offset: [0, 0, 0.1] + - geometry: ["mesh", "polygon"] + color: red + opacity: 1.0 + mode: selection + offset: [0, 0, -0.3] + - geometry: ["line"] + color: red + opacity: 1.0 + width: 10.0 + mode: selection + offset: [0, 0, -0.1] + - geometry: ["point"] color: red opacity: 1.0 - width: 4.0 - mode: highlight + width: 20.0 + mode: selection + offset: [0, 0, 0.1] \ No newline at end of file diff --git a/erdblick_app/app/app.component.ts b/erdblick_app/app/app.component.ts index 0fb71521..19fb0c8e 100644 --- a/erdblick_app/app/app.component.ts +++ b/erdblick_app/app/app.component.ts @@ -13,7 +13,7 @@ import {filter} from "rxjs"; - + @@ -33,9 +33,8 @@ import {filter} from "rxjs"; }) export class AppComponent { - title: string = 'erdblick'; - version: string = "v0.3.0"; - searchValue: string = "" + title: string = "erdblick"; + version: string = ""; constructor(private httpClient: HttpClient, private router: Router, diff --git a/erdblick_app/app/app.module.ts b/erdblick_app/app/app.module.ts index 407b57c7..d290b01c 100644 --- a/erdblick_app/app/app.module.ts +++ b/erdblick_app/app/app.module.ts @@ -1,9 +1,8 @@ import {APP_INITIALIZER, NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; - import {AppRoutingModule} from './app-routing.module'; import {AppComponent} from './app.component'; -import {provideHttpClient, withInterceptorsFromDi} from "@angular/common/http"; +import {provideHttpClient} from "@angular/common/http"; import {SpeedDialModule} from "primeng/speeddial"; import {DialogModule} from "primeng/dialog"; import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; @@ -52,12 +51,27 @@ import {FeatureSearchService} from "./feature.search.service"; import {ClipboardService} from "./clipboard.service"; import {MultiSelectModule} from "primeng/multiselect"; import {ButtonGroupModule} from "primeng/buttongroup"; -import {TabViewModule} from "primeng/tabview"; import {BreadcrumbModule} from "primeng/breadcrumb"; import {TableModule} from "primeng/table"; import {HighlightSearch} from "./highlight.pipe"; import {TreeTableFilterPatchDirective} from "./treetablefilter-patch.directive"; import {InputTextareaModule} from "primeng/inputtextarea"; +import {FloatLabelModule} from "primeng/floatlabel"; +import {TabViewModule} from "primeng/tabview"; +import {OnEnterClickDirective} from "./keyboard.service"; +import {DropdownModule} from "primeng/dropdown"; +import { + ArrayTypeComponent, + DatasourcesComponent, + MultiSchemaTypeComponent, + ObjectTypeComponent +} from "./datasources.component"; +import {EditorService} from "./editor.service"; +import {FormlyFieldConfig, FormlyModule} from "@ngx-formly/core"; +import {ReactiveFormsModule} from '@angular/forms'; +import {FormlyPrimeNGModule} from "@ngx-formly/primeng"; +import {DataSourcesService} from "./datasources.service"; +import {ProgressSpinnerModule} from "primeng/progressspinner"; export function initializeServices(styleService: StyleService, mapService: MapService, coordService: CoordinatesService) { return async () => { @@ -68,6 +82,50 @@ export function initializeServices(styleService: StyleService, mapService: MapSe } } +export function minItemsValidationMessage(error: any, field: FormlyFieldConfig) { + return `should NOT have fewer than ${field.props?.['minItems']} items`; +} + +export function maxItemsValidationMessage(error: any, field: FormlyFieldConfig) { + return `should NOT have more than ${field.props?.['maxItems']} items`; +} + +export function minLengthValidationMessage(error: any, field: FormlyFieldConfig) { + return `should NOT be shorter than ${field.props?.minLength} characters`; +} + +export function maxLengthValidationMessage(error: any, field: FormlyFieldConfig) { + return `should NOT be longer than ${field.props?.maxLength} characters`; +} + +export function minValidationMessage(error: any, field: FormlyFieldConfig) { + return `should be >= ${field.props?.min}`; +} + +export function maxValidationMessage(error: any, field: FormlyFieldConfig) { + return `should be <= ${field.props?.max}`; +} + +export function multipleOfValidationMessage(error: any, field: FormlyFieldConfig) { + return `should be multiple of ${field.props?.step}`; +} + +export function exclusiveMinimumValidationMessage(error: any, field: FormlyFieldConfig) { + return `should be > ${field.props?.step}`; +} + +export function exclusiveMaximumValidationMessage(error: any, field: FormlyFieldConfig) { + return `should be < ${field.props?.step}`; +} + +export function constValidationMessage(error: any, field: FormlyFieldConfig) { + return `should be equal to constant "${field.props?.['const']}"`; +} + +export function typeValidationMessage({ schemaType }: any) { + return `should be "${schemaType[0]}".`; +} + @NgModule({ declarations: [ AppComponent, @@ -82,6 +140,11 @@ export function initializeServices(styleService: StyleService, mapService: MapSe CoordinatesPanelComponent, FeatureSearchComponent, AlertDialogComponent, + DatasourcesComponent, + OnEnterClickDirective, + ArrayTypeComponent, + ObjectTypeComponent, + MultiSchemaTypeComponent, HighlightSearch, TreeTableFilterPatchDirective, ], @@ -117,17 +180,47 @@ export function initializeServices(styleService: StyleService, mapService: MapSe ListboxModule, MultiSelectModule, InputTextareaModule, - ButtonGroupModule, + FloatLabelModule, TabViewModule, + InputTextareaModule, + ButtonGroupModule, BreadcrumbModule, - TableModule + TableModule, + DropdownModule, + TableModule, + ReactiveFormsModule, + FormlyPrimeNGModule, + FormlyModule.forRoot({ + validationMessages: [ + {name: 'required', message: 'This field is required'}, + {name: 'type', message: typeValidationMessage}, + {name: 'minLength', message: minLengthValidationMessage}, + {name: 'maxLength', message: maxLengthValidationMessage}, + {name: 'min', message: minValidationMessage}, + {name: 'max', message: maxValidationMessage}, + {name: 'multipleOf', message: multipleOfValidationMessage}, + {name: 'exclusiveMinimum', message: exclusiveMinimumValidationMessage}, + {name: 'exclusiveMaximum', message: exclusiveMaximumValidationMessage}, + {name: 'minItems', message: minItemsValidationMessage}, + {name: 'maxItems', message: maxItemsValidationMessage}, + {name: 'uniqueItems', message: 'should NOT have duplicate items'}, + {name: 'const', message: constValidationMessage}, + {name: 'enum', message: `must be equal to one of the allowed values`}, + ], + types: [ + {name: 'array', component: ArrayTypeComponent}, + {name: 'object', component: ObjectTypeComponent}, + {name: 'multischema', component: MultiSchemaTypeComponent} + ], + }), + ProgressSpinnerModule ], providers: [ { provide: APP_INITIALIZER, useFactory: initializeServices, deps: [StyleService, MapService, CoordinatesService], - multi: true, + multi: true }, MapService, MessageService, @@ -138,7 +231,10 @@ export function initializeServices(styleService: StyleService, mapService: MapSe SidePanelService, FeatureSearchService, ClipboardService, - provideHttpClient(withInterceptorsFromDi()), - ] }) + EditorService, + DataSourcesService, + provideHttpClient() + ] +}) export class AppModule { } diff --git a/erdblick_app/app/cesium.ts b/erdblick_app/app/cesium.ts index 7dec33cb..a6792f04 100644 --- a/erdblick_app/app/cesium.ts +++ b/erdblick_app/app/cesium.ts @@ -18,6 +18,8 @@ export type Cartesian3 = Cesium.Cartesian3; export const Cartesian3 = Cesium.Cartesian3; export type Cartographic = Cesium.Cartographic; export const Cartographic = Cesium.Cartographic; +export type Matrix3 = Cesium.Matrix3; +export const Matrix3 = Cesium.Matrix3; export type Color = Cesium.Color; export const Color = Cesium.Color; export type ColorGeometryInstanceAttribute = Cesium.ColorGeometryInstanceAttribute; @@ -48,15 +50,24 @@ export type Viewer = Cesium.Viewer; export const Viewer = Cesium.Viewer; export type PrimitiveCollection = Cesium.PrimitiveCollection; export const PrimitiveCollection = Cesium.PrimitiveCollection; +export type PointPrimitiveCollection = Cesium.PointPrimitiveCollection; +export const PointPrimitiveCollection = Cesium.PointPrimitiveCollection; +export type LabelCollection = Cesium.LabelCollection; +export const LabelCollection = Cesium.LabelCollection; export type BillboardCollection = Cesium.BillboardCollection; export const BillboardCollection = Cesium.BillboardCollection; +export type Billboard = Cesium.Billboard; +export const Billboard = Cesium.Billboard; +export const defined = Cesium.defined; export type PinBuilder = Cesium.PinBuilder; -export const PinBuilder = Cesium.PinBuilder; -export const SceneTransforms = Cesium.SceneTransforms; export type Entity = Cesium.Entity; export const Entity = Cesium.Entity; export type Camera = Cesium.Camera; export const Camera = Cesium.Camera; +export type HeadingPitchRange = Cesium.HeadingPitchRange; +export const HeadingPitchRange = Cesium.HeadingPitchRange; +export type BoundingSphere = Cesium.BoundingSphere; +export const BoundingSphere = Cesium.BoundingSphere; // Math is a namespace. diff --git a/erdblick_app/app/coordinates.panel.component.ts b/erdblick_app/app/coordinates.panel.component.ts index 1e1149ad..d287566d 100644 --- a/erdblick_app/app/coordinates.panel.component.ts +++ b/erdblick_app/app/coordinates.panel.component.ts @@ -5,6 +5,7 @@ import {ParametersService} from "./parameters.service"; import {CesiumMath} from "./cesium"; import {ClipboardService} from "./clipboard.service"; import {coreLib} from "./wasm"; +import {InspectionService} from "./inspection.service"; interface PanelOption { name: string, @@ -14,7 +15,7 @@ interface PanelOption { @Component({ selector: "coordinates-panel", template: ` -
+
{{ markerButtonIcon }} @@ -73,6 +74,13 @@ interface PanelOption { text-align: right; font-family: monospace; } + + @media only screen and (max-width: 56em) { + .elevated { + bottom: 4em; + padding-bottom: 0; + } + } `] }) export class CoordinatesPanelComponent { @@ -92,6 +100,7 @@ export class CoordinatesPanelComponent { constructor(public mapService: MapService, public coordinatesService: CoordinatesService, public clipboardService: ClipboardService, + public inspectionService: InspectionService, public parametersService: ParametersService) { for (let level = 0; level < 15; level++) { this.displayOptions.push({name: `Mapget TileId (level ${level})`}); diff --git a/erdblick_app/app/coordinates.service.ts b/erdblick_app/app/coordinates.service.ts index d7fde1ef..93e9873b 100644 --- a/erdblick_app/app/coordinates.service.ts +++ b/erdblick_app/app/coordinates.service.ts @@ -9,8 +9,8 @@ import {HttpClient} from "@angular/common/http"; export class CoordinatesService { mouseMoveCoordinates: BehaviorSubject = new BehaviorSubject(null); mouseClickCoordinates: BehaviorSubject = new BehaviorSubject(null); - auxiliaryCoordinatesFun: Function | null = null; - auxiliaryTileIdsFun: Function | null = null; + auxiliaryCoordinatesFun: ((x: number, y: number)=>any) | null = null; + auxiliaryTileIdsFun: ((x: number, y: number, level: number)=>any) | null = null; constructor(private httpClient: HttpClient, public parametersService: ParametersService) { diff --git a/erdblick_app/app/datasources.component.ts b/erdblick_app/app/datasources.component.ts new file mode 100644 index 00000000..17ee9b11 --- /dev/null +++ b/erdblick_app/app/datasources.component.ts @@ -0,0 +1,242 @@ +import {Component, ViewChild} from "@angular/core"; +import {InfoMessageService} from "./info.service"; +import {ParametersService} from "./parameters.service"; +import {Subscription} from "rxjs"; +import {Dialog} from "primeng/dialog"; +import {EditorService} from "./editor.service"; +import {JSONSchema7} from "json-schema"; +import {DataSourcesService} from "./datasources.service"; +import {FormGroup} from '@angular/forms'; +import {FormlyFormOptions, FormlyFieldConfig, FieldType, FieldArrayType} from '@ngx-formly/core'; +import {FormlyJsonschema} from '@ngx-formly/core/json-schema'; + +@Component({ + selector: 'formly-multi-schema-type', + template: ` +
+
+ {{ props.label }} +

{{ props.description }}

+ + +
+
+ `, +}) +export class MultiSchemaTypeComponent extends FieldType {} + +@Component({ + selector: 'formly-object-type', + template: ` +
+ {{ props.label }} +

{{ props.description }}

+ + +
+ `, +}) +export class ObjectTypeComponent extends FieldType {} + +@Component({ + selector: 'formly-array-type', + template: ` +
+ +

{{ props.description }}

+ + + +
+
+
+ - +
+ +
+ +
+
+ + +
+
+
+ `, +}) +export class ArrayTypeComponent extends FieldArrayType {} + +@Component({ + selector: 'datasources', + template: ` + + + + + + + + + + + + + + + + + + + + + + + +

{{ dsService.errorMessage }}

+
+ +
+
+ + +
+
Press Ctrl-S/Cmd-S to save changes
+
Press Esc to quit without saving
+
+
+
+
+
+ +
+
+
+ +
+
+ {{ dsService.errorMessage ? dsService.errorMessage : "Data Source configuration is set to read-only!" }} +
+
+
+
+
+ `, + styles: [` + .loading { + visibility: collapse; + } + `] +}) +export class DatasourcesComponent { + datasourceWasModified: boolean = false; + wasModified: boolean = false; + dataSourcesConfig: string = ""; + form: FormGroup | undefined; + model: any = {}; + options!: FormlyFormOptions; + fields!: FormlyFieldConfig[]; + schema: JSONSchema7 = {}; + + @ViewChild('formElement') formElement!: HTMLFormElement; + @ViewChild('editorDialog') editorDialog: Dialog | undefined; + + private editedConfigSourceSubscription: Subscription = new Subscription(); + private savedConfigSourceSubscription: Subscription = new Subscription(); + + constructor(private messageService: InfoMessageService, + public parameterService: ParametersService, + private formlyJsonSchema: FormlyJsonschema, + public editorService: EditorService, + public dsService: DataSourcesService) { + this.parameterService.parameters.subscribe(parameters => { + return; + }); + + this.dsService.dataSourcesConfigJson.subscribe((config: any) => { + if (config && config["schema"] && config["model"]) { + this.schema = config["schema"]; + this.model = config["model"]; + this.dataSourcesConfig = JSON.stringify(this.model, null, 2); + this.editorService.styleEditorVisible = false; + this.editorService.readOnly = config.hasOwnProperty("readOnly") ? config["readOnly"] : true; + this.editorService.editableData = `${this.dataSourcesConfig}\n\n\n\n\n`; + this.editorService.datasourcesEditorVisible = true; + this.form = new FormGroup({}); + this.options = {}; + this.fields = [this.formlyJsonSchema.toFieldConfig(this.schema)]; + this.dsService.loading = false; + this.editorService.updateEditorState.next(true); + } + }); + + } + + loadConfigEditor() { + // this.datasourcesEditorDialogVisible = true; + // this.editorService.updateEditorState.next(true); + this.dsService.getConfig(); + this.editedConfigSourceSubscription = this.editorService.editedStateData.subscribe(editedStyleSource => { + this.wasModified = editedStyleSource.replace(/\n+$/, '') !== this.dataSourcesConfig.replace(/\n+$/, ''); + }); + this.savedConfigSourceSubscription = this.editorService.editedSaveTriggered.subscribe(_ => { + this.applyEditedDatasourceConfig(); + }); + } + + applyEditedDatasourceConfig() { + this.editorService.editableData = this.editorService.editedStateData.getValue(); + const configData = this.editorService.editedStateData.getValue().replace(/\n+$/, ''); + if (!configData) { + this.messageService.showError(`Cannot apply an empty configuration definition!`); + return; + } + this.dsService.postConfig(configData); + this.dataSourcesConfig = configData; + this.wasModified = false; + } + + closeEditorDialog(event: any) { + console.log(event); + if (this.editorDialog !== undefined) { + if (this.wasModified) { + event.stopPropagation(); + } else { + this.editorDialog.close(event); + } + } + this.editedConfigSourceSubscription.unsubscribe(); + this.savedConfigSourceSubscription.unsubscribe(); + } + + discardConfigEdits() { + this.editorService.updateEditorState.next(false); + } + + closeDatasources() { + this.editorService.datasourcesEditorVisible = false; + } + + submitForm() { + // if (this.form && this.form.valid) { + // this.formElement.submit(); + // } else { + // alert("Form is invalid"); + // } + } + + postConfig() { + // this.dsService.postConfig(this.model); + } +} \ No newline at end of file diff --git a/erdblick_app/app/datasources.service.ts b/erdblick_app/app/datasources.service.ts new file mode 100644 index 00000000..61419241 --- /dev/null +++ b/erdblick_app/app/datasources.service.ts @@ -0,0 +1,67 @@ +import {Injectable} from "@angular/core"; +import {HttpClient} from "@angular/common/http"; +import {MapService} from "./map.service"; +import {BehaviorSubject} from "rxjs"; +import {InfoMessageService} from "./info.service"; + + +@Injectable() +export class DataSourcesService { + + loading = false; + errorMessage: string = ""; + readOnly: boolean = true; + dataSourcesConfigJson: BehaviorSubject = new BehaviorSubject({}); + + constructor(private messageService: InfoMessageService, + public mapService: MapService, + private http: HttpClient) {} + + postConfig(config: string) { + this.loading = true; + this.http.post("/config", config, { observe: 'response', responseType: 'text' }).subscribe({ + next: (data: any) => { + this.messageService.showSuccess(data.body); + setTimeout(() => { + this.loading = false; + this.mapService.reloadDataSources().then(_ => this.mapService.update()); + }, 2000); + }, + error: error => { + this.loading = false; + alert(`Error: ${error.error}`); + } + }); + } + + getConfig() { + this.readOnly = true; + this.errorMessage = ""; + this.loading = true; + this.http.get("/config").subscribe({ + next: (data: any) => { + if (!data) { + this.errorMessage = "Unknown error: DataSources configuration data is missing!"; + this.dataSourcesConfigJson.next({}); + return; + } + if (!data["model"]) { + this.errorMessage = "Unknown error: DataSources config file data is missing!"; + this.dataSourcesConfigJson.next({}); + return; + } + if (!data["schema"]) { + this.errorMessage = "Unknown error: DataSources schema file data is missing!"; + this.dataSourcesConfigJson.next({}); + return; + } + this.readOnly = data["readOnly"]; + this.dataSourcesConfigJson.next(data); + }, + error: error => { + this.loading = false; + this.errorMessage = `Error: ${error.error}`; + } + }); + } +} \ No newline at end of file diff --git a/erdblick_app/app/debugapi.component.ts b/erdblick_app/app/debugapi.component.ts index 5caceff7..b9c2c7dc 100644 --- a/erdblick_app/app/debugapi.component.ts +++ b/erdblick_app/app/debugapi.component.ts @@ -1,4 +1,4 @@ -import {coreLib, uint8ArrayFromWasm} from "./wasm"; +import {coreLib, uint8ArrayFromWasm, ErdblickCore_} from "./wasm"; import {MapService} from "./map.service"; import {ErdblickViewComponent} from "./view.component"; import {ParametersService} from "./parameters.service"; @@ -36,16 +36,16 @@ export class ErdblickDebugApi { * * @param cameraInfoStr A JSON-formatted string containing camera information. */ - private setCamera(cameraInfoStr: string) { + setCamera(cameraInfoStr: string) { const cameraInfo = JSON.parse(cameraInfoStr); - this.parametersService.cameraViewData.next({ - destination: Cartesian3.fromArray(cameraInfo.position), - orientation: { + this.parametersService.setView( + Cartesian3.fromArray(cameraInfo.position), + { heading: cameraInfo.orientation.heading, pitch: cameraInfo.orientation.pitch, roll: cameraInfo.orientation.roll } - }); + ); } /** @@ -53,20 +53,21 @@ export class ErdblickDebugApi { * * @return A JSON-formatted string containing the current camera's position and orientation. */ - private getCamera() { + getCamera() { + const destination = this.parametersService.getCameraPosition(); const position = [ - this.parametersService.cameraViewData.getValue().destination.x, - this.parametersService.cameraViewData.getValue().destination.y, - this.parametersService.cameraViewData.getValue().destination.z, + destination.x, + destination.y, + destination.z, ]; - const orientation = this.parametersService.cameraViewData.getValue().orientation; + const orientation = this.parametersService.getCameraOrientation(); return JSON.stringify({position, orientation}); } /** * Generate a test TileFeatureLayer, and show it. */ - private showTestTile() { + showTestTile() { let tile = uint8ArrayFromWasm((sharedArr: any) => { coreLib.generateTestTile(sharedArr, this.mapService.tileParser!); }); @@ -81,4 +82,11 @@ export class ErdblickDebugApi { options: [] }, "_builtin", true); } + + /** + * Check for memory leaks. + */ + coreLib(): ErdblickCore_ { + return coreLib; + } } diff --git a/erdblick_app/app/editor.component.ts b/erdblick_app/app/editor.component.ts index b842f136..f4b40d06 100644 --- a/erdblick_app/app/editor.component.ts +++ b/erdblick_app/app/editor.component.ts @@ -1,4 +1,4 @@ -import {Component, ViewChild, ElementRef, AfterViewInit, OnDestroy, Renderer2} from '@angular/core'; +import {Component, ViewChild, ElementRef, AfterViewInit, OnDestroy, Renderer2, Input} from '@angular/core'; import {basicSetup} from 'codemirror'; import {EditorState, Extension} from '@codemirror/state'; import {yaml} from '@codemirror/lang-yaml'; @@ -8,6 +8,7 @@ import {linter, Diagnostic, lintGutter} from '@codemirror/lint'; import {syntaxHighlighting, defaultHighlightStyle} from "@codemirror/language" import {StyleService} from "./style.service"; import * as jsyaml from 'js-yaml'; +import {EditorService} from "./editor.service"; const completionsList = [ {label: "version", type: "property"}, @@ -60,6 +61,7 @@ const completionsList = [ {label: "first-of", type: "property"}, {label: "attribute-type", type: "property"}, {label: "attribute-layer-type", type: "property"}, + {label: "point-merge-grid-cell", type: "property"}, {label: "FILL", type: "keyword"}, {label: "OUTLINE", type: "keyword"}, {label: "FILL_AND_OUTLINE", type: "keyword"}, @@ -79,8 +81,9 @@ const completionsList = [ {label: "feature", type: "keyword"}, {label: "relation", type: "keyword"}, {label: "attribute", type: "keyword"}, - {label: "normal", type: "keyword"}, - {label: "highlight", type: "keyword"}, + {label: "none", type: "keyword"}, + {label: "selection", type: "keyword"}, + {label: "hover", type: "keyword"}, {label: "Lane", type: "keyword"}, {label: "Boundary", type: "keyword"} ] @@ -97,34 +100,30 @@ export class EditorComponent implements AfterViewInit, OnDestroy { @ViewChild('editor') private editorRef!: ElementRef; private editorView?: EditorView; - private styleSource: string = ""; + private editedSource: string = ""; - constructor(public styleService: StyleService, + constructor(public editorService: EditorService, public renderer: Renderer2) {} ngAfterViewInit(): void { - this.styleService.selectedStyleIdForEditing.subscribe(styleId => { - if (styleId) { + this.editorService.updateEditorState.subscribe(state => { + if (state) { const childElements = this.editorRef.nativeElement.childNodes; for (let child of childElements) { this.renderer.removeChild(this.editorRef.nativeElement, child); } this.editorView = new EditorView({ - state: this.createEditorState(styleId), + state: this.createEditorState(), parent: this.editorRef.nativeElement }); } }); } - createEditorState(styleId: string) { - if (this.styleService.styles.has(styleId)) { - this.styleSource = `${this.styleService.styles.get(styleId)!.source}\n\n\n\n\n`; - } else { - this.styleSource = ""; - } + createEditorState() { + this.editedSource = this.editorService.editableData; return EditorState.create({ - doc: this.styleSource, + doc: this.editedSource, extensions: [ basicSetup, yaml(), @@ -135,8 +134,9 @@ export class EditorComponent implements AfterViewInit, OnDestroy { this.yamlLinter, this.stopMouseWheelClipboard, EditorState.tabSize.of(2), + EditorState.readOnly.of(this.editorService.readOnly), EditorView.updateListener.of((e: ViewUpdate) => { - this.styleService.styleEditedStateData.next(e.state.doc.toString()); + this.editorService.editedStateData.next(e.state.doc.toString()); }) ] }); @@ -193,7 +193,7 @@ export class EditorComponent implements AfterViewInit, OnDestroy { return { key: 'Mod-s', run: () => { - this.styleService.styleEditedSaveTriggered.next(true); + this.editorService.editedSaveTriggered.next(true); return true; } }; diff --git a/erdblick_app/app/editor.service.ts b/erdblick_app/app/editor.service.ts new file mode 100644 index 00000000..c4af52a8 --- /dev/null +++ b/erdblick_app/app/editor.service.ts @@ -0,0 +1,17 @@ +import {Injectable} from "@angular/core"; +import {BehaviorSubject, Subject} from "rxjs"; + +@Injectable() +export class EditorService { + + // TODO: Change to a stack of references to support many editors. + styleEditorVisible: boolean = false; + datasourcesEditorVisible: boolean = false; + updateEditorState: Subject = new Subject(); + editedSaveTriggered: Subject = new Subject(); + editedStateData: BehaviorSubject = new BehaviorSubject(""); + editableData: string = ""; + readOnly: boolean = false; + + constructor() {} +} \ No newline at end of file diff --git a/erdblick_app/app/feature.panel.component.ts b/erdblick_app/app/feature.panel.component.ts index eb20623b..7205cca0 100644 --- a/erdblick_app/app/feature.panel.component.ts +++ b/erdblick_app/app/feature.panel.component.ts @@ -1,13 +1,20 @@ -import {Component, Input, OnInit, ViewChild} from "@angular/core"; +import { + AfterViewInit, + Component, ElementRef, + OnDestroy, + OnInit, Renderer2, + 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} from "rxjs"; +import {distinctUntilChanged, Subscription} from "rxjs"; import {coreLib} from "./wasm"; import {ClipboardService} from "./clipboard.service"; import {TreeTable} from "primeng/treetable"; +import {ParametersService} from "./parameters.service"; interface Column { field: string; @@ -18,45 +25,47 @@ interface Column { selector: 'feature-panel', template: `
+ style="display: flex; align-content: center; justify-content: center; width: 100%; padding: 0.5em;">
+ class="pi pi-times clear-icon" style="cursor: pointer">
- + loupe
- +
-
-
- - -
- + + + + +
+ [pTooltip]="rowData['key'].toString()" tooltipPosition="left" + [tooltipOptions]="tooltipOptions"> {{ rowData['key'] }} + style="cursor: pointer">{{ rowData['key'] }} - - + - +
+ [pTooltip]="rowData['value'].toString()" tooltipPosition="left" + [tooltipOptions]="tooltipOptions">
+ (mouseover)="onValueHover($event, rowData)" + (mouseout)="onValueHoverExit($event, rowData)"> {{ rowData['value'] }} + [pTooltip]="rowData['info'].toString()" + tooltipPosition="left">
@@ -153,25 +155,15 @@ interface Column { font-style: italic; } - .source-data-ref-container { - button { - width: 20px; - height: 20px; - padding: 3px; - margin-bottom: 0.5px; - } - } - @media only screen and (max-width: 56em) { .resizable-container-expanded { - height: calc(100vh - 3em);; + height: calc(100vh - 3em); } } `] }) -export class FeaturePanelComponent implements OnInit { +export class FeaturePanelComponent implements OnInit, AfterViewInit, OnDestroy { - //jsonTree: string = ""; filteredTree: TreeTableNode[] = []; cols: Column[] = []; isExpanded: boolean = false; @@ -187,13 +179,20 @@ export class FeaturePanelComponent implements OnInit { @ViewChild('tt') table!: TreeTable; + @ViewChild('resizeableContainer') resizeableContainer!: ElementRef; @ViewChild('inspectionMenu') inspectionMenu!: Menu; inspectionMenuItems: MenuItem[] | undefined; inspectionMenuVisible: boolean = false; + inspectionContainerWidth: number; + inspectionContainerHeight: number; + containerSizeSubscription: Subscription; + constructor(private clipboardService: ClipboardService, public inspectionService: InspectionService, public jumpService: JumpTargetService, + public parameterService: ParametersService, + private renderer: Renderer2, public mapService: MapService) { this.inspectionService.featureTree.pipe(distinctUntilChanged()).subscribe((tree: string) => { this.jsonTree = tree; @@ -213,7 +212,19 @@ export class FeaturePanelComponent implements OnInit { scroller.calculateAutoSize(); } }, 0); - }) + }); + + this.inspectionContainerWidth = this.parameterService.inspectionContainerWidth * this.parameterService.baseFontSize; + this.inspectionContainerHeight = this.parameterService.inspectionContainerHeight * this.parameterService.baseFontSize; + this.containerSizeSubscription = this.parameterService.parameters.subscribe(parameter => { + if (parameter.panel.length == 2) { + this.inspectionContainerWidth = parameter.panel[0] * this.parameterService.baseFontSize; + this.inspectionContainerHeight = parameter.panel[1] * this.parameterService.baseFontSize; + } else { + this.inspectionContainerWidth = this.parameterService.inspectionContainerWidth * this.parameterService.baseFontSize; + this.inspectionContainerHeight = (window.innerHeight - this.parameterService.inspectionContainerHeight * this.parameterService.baseFontSize) * this.parameterService.baseFontSize; + } + }); } ngOnInit(): void { @@ -223,6 +234,10 @@ export class FeaturePanelComponent implements OnInit { ]; } + ngAfterViewInit() { + this.detectSafari(); + } + copyToClipboard(text: string) { this.clipboardService.copyToClipboard(text); } @@ -230,8 +245,9 @@ export class FeaturePanelComponent implements OnInit { expandTreeNodes(nodes: TreeTableNode[], parent: any = null): void { nodes.forEach(node => { const isTopLevelNode = parent === null; + const isSection = node.data && node.data["type"] === this.InspectionValueType.SECTION.value; const hasSingleChild = node.children && node.children.length === 1; - node.expanded = isTopLevelNode || hasSingleChild; + node.expanded = isTopLevelNode || isSection || hasSingleChild; if (node.children) { this.expandTreeNodes(node.children, node); @@ -341,19 +357,21 @@ export class FeaturePanelComponent implements OnInit { } } - showSourceData(sourceDataRef: any) { + showSourceData(event: any, sourceDataRef: any) { + event.stopPropagation(); + const layerId = sourceDataRef.layerId; const tileId = sourceDataRef.tileId; const address = sourceDataRef.address; - const mapId = this.inspectionService.selectedMapIdName; - const featureId = this.inspectionService.selectedFeatureIdName; + 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), - featureId: featureId, + featureIds: featureIds, }) } @@ -365,27 +383,37 @@ export class FeaturePanelComponent implements OnInit { } if (rowData["type"] == this.InspectionValueType.FEATUREID.value) { - this.jumpService.highlightFeature(this.inspectionService.selectedMapIdName, rowData["value"]).then(); + this.jumpService.highlightByJumpTargetFilter( + rowData["mapId"], + rowData["value"]).then(); } - this.copyToClipboard(rowData["value"]); } - highlightFeature(rowData: any) { - return; + onValueHover(event: any, rowData: any) { + event.stopPropagation(); + if (rowData["type"] == this.InspectionValueType.FEATUREID.value) { + this.jumpService.highlightByJumpTargetFilter( + rowData["mapId"], + rowData["value"], + coreLib.HighlightMode.HOVER_HIGHLIGHT).then(); + } } - stopHighlight(rowData: any) { - return; + onValueHoverExit(event: any, rowData: any) { + event.stopPropagation(); + if (rowData["type"] == this.InspectionValueType.FEATUREID.value) { + this.mapService.highlightFeatures([], false, coreLib.HighlightMode.HOVER_HIGHLIGHT).then(); + } } getStyleClassByType(valueType: number): string { switch (valueType) { case this.InspectionValueType.SECTION.value: - return "section-style" + return "section-style"; case this.InspectionValueType.FEATUREID.value: - return "feature-id-style" + return "feature-id-style"; default: - return "standard-style" + return "standard-style"; } } @@ -401,4 +429,15 @@ export class FeaturePanelComponent implements OnInit { this.clearFilter(); } } + + ngOnDestroy() { + this.containerSizeSubscription.unsubscribe(); + } + + detectSafari() { + const isSafari = /Safari/i.test(navigator.userAgent); + if (isSafari) { + this.renderer.addClass(this.resizeableContainer.nativeElement, 'safari'); + } + } } diff --git a/erdblick_app/app/feature.search.component.ts b/erdblick_app/app/feature.search.component.ts index b2d82db9..3f35bf76 100644 --- a/erdblick_app/app/feature.search.component.ts +++ b/erdblick_app/app/feature.search.component.ts @@ -6,59 +6,65 @@ import {MapService} from "./map.service"; import {SidePanelService, SidePanelState} from "./sidepanel.service"; import {Listbox} from "primeng/listbox"; import {InfoMessageService} from "./info.service"; +import {KeyboardService} from "./keyboard.service"; @Component({ selector: "feature-search", template: ` - -
-
- - - {{ searchService.doneTiles }} / {{ searchService.totalTiles }} tiles - - +
+ +
+
+ + + {{ searchService.doneTiles }} / {{ searchService.totalTiles }} tiles + + +
+ +
- - -
-
- Elapsed time:{{ searchService.timeElapsed }} -
-
- Features:{{ searchService.totalFeatureCount }} -
-
- Matched:{{ results.length }} -
-
- Highlight colour: - -
- - - {{ trace.content }} - - - - +
+ Elapsed time:{{ searchService.timeElapsed }} +
+
+ Features:{{ searchService.totalFeatureCount }} +
+
+ Matched:{{ searchService.searchResults.length }} +
+
+ Highlight colour: + +
+ + + {{ trace.content }} + + + + +
`, styles: [``] }) export class FeatureSearchComponent { isPanelVisible: boolean = false; - results: Array = []; placeholder: Array = []; traceResults: Array = []; selectedResult: any; @@ -74,30 +80,23 @@ export class FeatureSearchComponent { public mapService: MapService, public inspectionService: InspectionService, public sidePanelService: SidePanelService, + public keyboardService: KeyboardService, private infoMessageService: InfoMessageService) { this.sidePanelService.observable().subscribe(panel=> { this.isPanelVisible = panel == SidePanelState.FEATURESEARCH || this.isPanelVisible; }); this.searchService.isFeatureSearchActive.subscribe(isActive => { if (isActive) { - this.results = []; this.placeholder = [{label: "Loading..."}]; this.canPauseStopSearch = isActive; } else { - this.listbox.options = 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.push({label: `${featureId}`, mapId: mapId, featureId: featureId}); + this.listbox.options = this.searchService.searchResults; } }); this.searchService.progress.subscribe(value => { this.percentDone = value; if (value >= 100) { - this.listbox.options = this.results; + this.listbox.options = this.searchService.searchResults; this.canPauseStopSearch = false; if (this.searchService.errors.size) { this.infoMessageService.showAlertDialog( @@ -110,10 +109,10 @@ export class FeatureSearchComponent { } selectResult(event: any) { - if (event.value.mapId && event.value.featureId) { - this.jumpService.highlightFeature(event.value.mapId, event.value.featureId).then(() => { - if (this.inspectionService.selectedFeature) { - this.mapService.focusOnFeature(this.inspectionService.selectedFeature); + if (event.value && event.value.mapId && event.value.featureId) { + this.jumpService.highlightByJumpTargetFilter(event.value.mapId, event.value.featureId).then(() => { + if (this.inspectionService.selectedFeatures.length) { + this.mapService.focusOnFeature(this.inspectionService.selectedFeatures[0]); } }); } @@ -127,14 +126,14 @@ export class FeatureSearchComponent { return; } this.searchService.pause(); - this.listbox.options = this.results; + this.listbox.options = this.searchService.searchResults; this.isSearchPaused = true; } } stopSearch() { if (this.canPauseStopSearch) { - this.listbox.options = this.results; + this.listbox.options = this.searchService.searchResults; this.searchService.stop(); this.canPauseStopSearch = false; @@ -146,4 +145,15 @@ export class FeatureSearchComponent { } } } + + onHide(event: any) { + this.searchService.clear(); + this.sidePanelService.featureSearchOpen = false; + this.keyboardService.dialogOnHide(event); + } + + onShow(event: any) { + this.sidePanelService.featureSearchOpen = true; + this.keyboardService.dialogOnShow(event); + } } \ No newline at end of file diff --git a/erdblick_app/app/feature.search.service.ts b/erdblick_app/app/feature.search.service.ts index 2676886a..7f000c7d 100644 --- a/erdblick_app/app/feature.search.service.ts +++ b/erdblick_app/app/feature.search.service.ts @@ -8,6 +8,11 @@ import {coreLib, uint8ArrayFromWasm} from "./wasm"; export const MAX_ZOOM_LEVEL = 15; +export interface SearchResultPrimitiveId { + type: string, + index: number +} + function generateChildrenIds(parentTileId: bigint) { if (parentTileId == -1n) { return [0n, 4294967296n]; @@ -33,7 +38,7 @@ class FeatureSearchQuadTreeNode { level: number; children: Array; count: number; - markers: Array = []; + markers: Array<[SearchResultPrimitiveId, SearchResultPosition]> = []; rectangle: Rectangle; center: Cartesian3 | null; @@ -42,7 +47,7 @@ class FeatureSearchQuadTreeNode { level: number, count: number, children: Array = [], - markers: Array = []) { + markers: Array<[SearchResultPrimitiveId, SearchResultPosition]> = []) { this.tileId = tileId; this.parentId = parentTileId; this.level = level; @@ -59,19 +64,19 @@ class FeatureSearchQuadTreeNode { return Rectangle.contains(this.rectangle, point); } - contains(points: Array) { - return points.some(point => - this.containsPoint(point.cartographicRad as Cartographic) + contains(markers: Array<[SearchResultPrimitiveId, SearchResultPosition]>) { + return markers.some(marker => + this.containsPoint(marker[1].cartographicRad as Cartographic) ); } - filterPointsForNode(points: Array) { - return points.filter(point => - this.containsPoint(point.cartographicRad as Cartographic) + filterPointsForNode(markers: Array<[SearchResultPrimitiveId, SearchResultPosition]>) { + return markers.filter(marker => + this.containsPoint(marker[1].cartographicRad as Cartographic) ); } - addChildren(markers: Array | Cartographic) { + addChildren(markers: Array<[SearchResultPrimitiveId, SearchResultPosition]> | Cartographic) { const existingIds = this.children.map(child => child.tileId); const missingIds = generateChildrenIds(this.tileId).filter(id => !existingIds.includes(id)); for (const id of missingIds) { @@ -97,12 +102,12 @@ class FeatureSearchQuadTree { this.root = new FeatureSearchQuadTreeNode(-1n, null, -1, 0); } - private calculateAveragePosition(markers: Array): Cartesian3 { + private calculateAveragePosition(markers: Array<[SearchResultPrimitiveId, SearchResultPosition]>): Cartesian3 { const sum = markers.reduce( - (acc, pos) => { - acc.x += pos.cartesian.x; - acc.y += pos.cartesian.y; - acc.z += pos.cartesian.z; + (acc, marker) => { + acc.x += marker[1].cartesian.x; + acc.y += marker[1].cartesian.y; + acc.z += marker[1].cartesian.z; return acc; }, { x: 0, y: 0, z: 0 } @@ -111,7 +116,7 @@ class FeatureSearchQuadTree { return new Cartesian3(sum.x / markers.length, sum.y / markers.length, sum.z / markers.length); } - insert(tileId: bigint, markers: Array) { + insert(tileId: bigint, markers: Array<[SearchResultPrimitiveId, SearchResultPosition]>) { const markersCenter = this.calculateAveragePosition(markers); const markersCenterCartographic = Cartographic.fromCartesian(markersCenter); let currentLevel = 0; @@ -215,13 +220,13 @@ export class FeatureSearchService { cachedWorkQueue: Array = []; totalTiles: number = 0; doneTiles: number = 0; - searchUpdates: Subject = new Subject(); isFeatureSearchActive: Subject = new Subject(); pointColor: string = "#ea4336"; timeElapsed: string = this.formatTime(0); totalFeatureCount: number = 0; progress: Subject = new Subject(); pinGraphicsByTier: Map = new Map; + searchResults: Array = []; pinTiers = [ 10000, 9000, 8000, 7000, 6000, 5000, 4000, 3000, 2000, 1000, 900, 800, 700, 600, 500, 400, 300, 200, 100, @@ -364,6 +369,7 @@ export class FeatureSearchService { this.totalTiles = 0; this.doneTiles = 0; this.progress.next(0); + this.searchResults = []; this.isFeatureSearchActive.next(false); this.totalFeatureCount = 0; this.startTime = 0; @@ -385,7 +391,8 @@ export class FeatureSearchService { // Add visualizations and register the search result. if (tileResult.matches.length && tileResult.tileId) { - let mapTileKey = tileResult.matches[0][0]; + const mapTileKey = tileResult.matches[0][0]; + const mapId = mapTileKey.split(':')[1] this.resultsPerTile.set(mapTileKey, tileResult); this.resultTree.insert(tileResult.tileId, tileResult.matches.map(result => { if (result[2].cartographic) { @@ -396,7 +403,10 @@ export class FeatureSearchService { ); } result[2].cartographic = null; - return result[2]; + const featureId = result[1]; + const id: SearchResultPrimitiveId = {type: "SearchResult", index: this.searchResults.length}; + this.searchResults.push({label: `${featureId}`, mapId: mapId, featureId: featureId}); + return [id, result[2]]; })); } @@ -406,7 +416,6 @@ export class FeatureSearchService { this.endTime = Date.now(); this.timeElapsed = this.formatTime(this.endTime - this.startTime); this.totalFeatureCount += tileResult.numFeatures; - this.searchUpdates.next(tileResult); this.visualizationChanged.next(); } diff --git a/erdblick_app/app/features.model.ts b/erdblick_app/app/features.model.ts index f3cb9c18..fcc5f048 100644 --- a/erdblick_app/app/features.model.ts +++ b/erdblick_app/app/features.model.ts @@ -1,7 +1,6 @@ -"use strict"; - import {uint8ArrayToWasm, uint8ArrayToWasmAsync} from "./wasm"; import {TileLayerParser, TileFeatureLayer} from '../../build/libs/core/erdblick-core'; +import {TileFeatureId} from "./parameters.service"; /** * JS interface of a WASM TileFeatureLayer. @@ -10,7 +9,7 @@ import {TileLayerParser, TileFeatureLayer} from '../../build/libs/core/erdblick- * WASM TileFeatureLayer, use the peek()-function. */ export class FeatureTile { - id: string; + mapTileKey: string; nodeId: string; mapName: string; layerName: string; @@ -31,7 +30,7 @@ export class FeatureTile { let mapTileMetadata = uint8ArrayToWasm((wasmBlob: any) => { return parser.readTileLayerMetadata(wasmBlob); }, tileFeatureLayerBlob); - this.id = mapTileMetadata.id; + this.mapTileKey = mapTileMetadata.id; this.nodeId = mapTileMetadata.nodeId; this.mapName = mapTileMetadata.mapName; this.layerName = mapTileMetadata.layerName; @@ -50,7 +49,7 @@ export class FeatureTile { */ peek(callback: (layer: TileFeatureLayer) => any) { // Deserialize the WASM tileFeatureLayer from the blob. - return uint8ArrayToWasm((bufferToRead: any) => { + let result = uint8ArrayToWasm((bufferToRead: any) => { let deserializedLayer = this.parser.readTileFeatureLayer(bufferToRead); if (!deserializedLayer) return null; @@ -64,6 +63,7 @@ export class FeatureTile { deserializedLayer.delete(); return result; }, this.tileFeatureLayerBlob); + return result; } /** @@ -71,7 +71,7 @@ export class FeatureTile { */ async peekAsync(callback: (layer: TileFeatureLayer) => Promise) { // Deserialize the WASM tileFeatureLayer from the blob. - return await uint8ArrayToWasmAsync(async (bufferToRead: any) => { + let result = await uint8ArrayToWasmAsync(async (bufferToRead: any) => { let deserializedLayer = this.parser.readTileFeatureLayer(bufferToRead); if (!deserializedLayer) return null; @@ -85,6 +85,7 @@ export class FeatureTile { deserializedLayer.delete(); return result; }, this.tileFeatureLayerBlob); + return result; } /** @@ -133,25 +134,34 @@ export class FeatureTile { level() { return Number(this.tileId & BigInt(0xffff)); } + + has(featureId: string) { + return this.peek((tileFeatureLayer: TileFeatureLayer) => { + let feature = tileFeatureLayer.find(featureId); + let result = !feature.isNull(); + feature.delete(); + return result; + }); + } } /** - * Wrapper which combines a FeatureTile and the index of - * a feature within the tileset. Using the peek-function, it is - * possible to access the WASM feature view in a memory-safe way. + * Wrapper which combines a FeatureTile and feature id. + * Using the peek-function, it is possible to access the + * WASM feature view in a memory-safe way. */ export class FeatureWrapper { - public readonly index: number; + public readonly featureId: string; public featureTile: FeatureTile; /** * Construct a feature wrapper from a featureTile and a feature index * within that tile. - * @param index The index of the feature within the tile. + * @param featureId The feature-id of the feature. * @param featureTile {FeatureTile} The feature tile container. */ - constructor(index: number, featureTile: FeatureTile) { - this.index = index; + constructor(featureId: string, featureTile: FeatureTile) { + this.featureId = featureId; this.featureTile = featureTile; } @@ -161,11 +171,12 @@ export class FeatureWrapper { * @returns The value returned by the callback. */ peek(callback: any) { - if (this.featureTile.disposed) { - throw new Error(`Unable to access feature of deleted layer ${this.featureTile.id}!`); - } return this.featureTile.peek((tileFeatureLayer: TileFeatureLayer) => { - let feature = tileFeatureLayer.at(this.index); + let feature = tileFeatureLayer.find(this.featureId); + if (feature.isNull()) { + feature.delete(); + return null; + } let result = null; if (callback) { result = callback(feature); @@ -175,10 +186,19 @@ export class FeatureWrapper { }); } + /** Check if this wrapper wraps the same feature as another wrapper. */ equals(other: FeatureWrapper | null): boolean { if (!other) { return false; } - return this.featureTile.id == other.featureTile.id && this.index == other.index; + return this.featureTile.mapTileKey == other.featureTile.mapTileKey && this.featureId == other.featureId; + } + + /** Returns the cross-map-layer global ID for this feature. */ + key(): TileFeatureId { + return { + mapTileKey: this.featureTile.mapTileKey, + featureId: this.featureId + }; } } diff --git a/erdblick_app/app/fetch.model.ts b/erdblick_app/app/fetch.model.ts index a4227960..f3ca2417 100644 --- a/erdblick_app/app/fetch.model.ts +++ b/erdblick_app/app/fetch.model.ts @@ -101,7 +101,7 @@ export class Fetch requestOptions["body"] = this.bodyJson; headers["Content-Type"] = "application/json"; } - requestOptions["headers"] = headers + requestOptions["headers"] = headers; return fetch(this.url, requestOptions) .then(response => { diff --git a/erdblick_app/app/inspection.panel.component.ts b/erdblick_app/app/inspection.panel.component.ts index 0ceb8c77..e57066db 100644 --- a/erdblick_app/app/inspection.panel.component.ts +++ b/erdblick_app/app/inspection.panel.component.ts @@ -1,9 +1,10 @@ -import {Component, OnInit} from "@angular/core"; +import {Component} from "@angular/core"; import {InspectionService, SelectedSourceData, selectedSourceDataEqualTo} from "./inspection.service"; import {distinctUntilChanged} from "rxjs"; import {FeaturePanelComponent} from "./feature.panel.component"; import {SourceDataPanelComponent} from "./sourcedata.panel.component"; import {ParametersService} from "./parameters.service"; +import {MapService} from "./map.service"; interface InspectorTab { title: string, @@ -13,18 +14,34 @@ interface InspectorTab { onClose?: any, } +interface SourceLayerMenuItem { + label: string, + disabled: boolean, + command: () => void +} + +export interface InspectionContainerSize { + height: number, + width: number, + type: string +} + @Component({ selector: 'inspection-panel', template: ` + class="w-full inspect-panel" [ngClass]="{'inspect-panel-small-header': activeIndex > 0}" + [activeIndex]="0" > {{ tabs[activeIndex].title || '' }} + + @@ -47,12 +64,13 @@ interface InspectorTab { align-items: center; .p-button { - width: 30px; - height: 30px; + width: 1.75em; + height: 1.75em; margin: 0; } } - }`, + } + `, ] }) export class InspectionPanelComponent @@ -61,28 +79,58 @@ export class InspectionPanelComponent tabs: InspectorTab[] = []; activeIndex = 0; - constructor(public inspectionService: InspectionService, private parameterService: ParametersService) { + layerMenuItems: SourceLayerMenuItem[] = []; + selectedLayerItem?: SourceLayerMenuItem; + + constructor(public inspectionService: InspectionService, + public mapService: MapService, + private parameterService: ParametersService) { this.pushFeatureInspector(); this.inspectionService.featureTree.pipe(distinctUntilChanged()).subscribe((tree: string) => { this.reset(); - // TODO: Create a new FeaturePanelComponent instance for each unique selected feature - // then we can get rid of all the service's View Component logic/functions. + // TODO: Create a new FeaturePanelComponent instance for each unique feature selection. + // Then we can get rid of all the service's View Component logic/functions. // reset() Would then completely clear the tabs. - const featureId = this.inspectionService.selectedFeatureIdName; - this.tabs[0].title = featureId; + const featureIds = this.inspectionService.selectedFeatures.map(f=>f.featureId).join(", "); + if (this.inspectionService.selectedFeatures.length == 1) { + this.tabs[0].title = featureIds; + } + else { + this.tabs[0].title = `Selected ${this.inspectionService.selectedFeatures.length} Features`; + } const selectedSourceData = parameterService.getSelectedSourceData() - if (selectedSourceData?.featureId === featureId) + if (selectedSourceData?.featureIds === featureIds) this.inspectionService.selectedSourceData.next(selectedSourceData); else this.inspectionService.selectedSourceData.next(null); }); this.inspectionService.selectedSourceData.pipe(distinctUntilChanged(selectedSourceDataEqualTo)).subscribe(selection => { - if (selection) + if (selection) { + this.reset(); + const map = this.mapService.maps.getValue().get(selection.mapId); + if (map) { + this.layerMenuItems = Array.from(map.layers.values()).filter(item => item.type == "SourceData").map(item => { + return { + label: SourceDataPanelComponent.layerNameForLayerId(item.layerId), + disabled: item.layerId === selection.layerId, + command: () => { + let sourceData = {...selection}; + sourceData.layerId = item.layerId; + sourceData.address = BigInt(0); + this.inspectionService.selectedSourceData.next(sourceData); + }, + }; + }); + this.selectedLayerItem = this.layerMenuItems.filter(item => item.disabled).pop(); + } else { + this.layerMenuItems = []; + } this.pushSourceDataInspector(selection); + } }) } @@ -115,8 +163,8 @@ export class InspectionPanelComponent pushSourceDataInspector(data: SelectedSourceData) { let tab = { - title: SourceDataPanelComponent.layerNameForLayerId(data.layerId), - icon: "pi-database", + title: `${data.tileId}.`, + icon: "", component: SourceDataPanelComponent, inputs: { sourceData: data @@ -148,4 +196,14 @@ export class InspectionPanelComponent this.tabs.pop(); } } + + onSelectedLayerItem() { + if (this.selectedLayerItem && !this.selectedLayerItem.disabled) { + this.selectedLayerItem.command(); + } + } + + onDropdownClick(event: MouseEvent) { + event.stopPropagation(); + } } diff --git a/erdblick_app/app/inspection.service.ts b/erdblick_app/app/inspection.service.ts index 883e932f..2e2c7600 100644 --- a/erdblick_app/app/inspection.service.ts +++ b/erdblick_app/app/inspection.service.ts @@ -1,6 +1,6 @@ import {EventEmitter, Injectable} from "@angular/core"; import {TreeTableNode} from "primeng/api"; -import {BehaviorSubject, distinctUntilChanged, distinctUntilKeyChanged, filter, ReplaySubject} from "rxjs"; +import {BehaviorSubject, distinctUntilChanged, Subject} from "rxjs"; import {MapService} from "./map.service"; import {Feature, TileSourceDataLayer} from "../../build/libs/core/erdblick-core"; import {FeatureWrapper} from "./features.model"; @@ -8,6 +8,9 @@ import {ParametersService} from "./parameters.service"; import {coreLib, uint8ArrayToWasm} from "./wasm"; import {JumpTargetService} from "./jump.service"; import {Fetch} from "./fetch.model"; +import {Cartesian3} from "./cesium"; +import {InfoMessageService} from "./info.service"; +import {KeyboardService} from "./keyboard.service"; interface InspectionModelData { @@ -17,6 +20,7 @@ interface InspectionModelData { info?: string; hoverId?: string geoJsonPath?: string; + mapId?: string; sourceDataReferences?: Array; children: Array; } @@ -26,13 +30,27 @@ export interface SelectedSourceData { tileId: number, layerId: string, address: bigint, - featureId: string, + featureIds: string, } export function selectedSourceDataEqualTo(a: SelectedSourceData | null, b: SelectedSourceData | null) { if (!a || !b) return false; - return (a === b || (a.mapId === b.mapId && a.tileId === b.tileId && a.layerId === b.layerId && a.address === b.address && a.featureId === b.featureId)); + return (a === b || (a.mapId === b.mapId && a.tileId === b.tileId && a.layerId === b.layerId && a.address === b.address && a.featureIds === b.featureIds)); +} + +export function selectedFeaturesEqualTo(a: FeatureWrapper[] | null, b: FeatureWrapper[] | null) { + if (!a || !b) + return false; + if (a.length !== b.length) { + return false + } + for (let i = 0; i < a.length; ++i) { + if (!a[i].equals(b[i])) { + return false; + } + } + return true; } @Injectable({providedIn: 'root'}) @@ -41,11 +59,14 @@ export class InspectionService { featureTree: BehaviorSubject = new BehaviorSubject(""); featureTreeFilterValue: string = ""; isInspectionPanelVisible: boolean = false; - selectedFeatureGeoJsonText: string = ""; - selectedFeatureInspectionModel: Array | null = null; - selectedFeatureIdName: string = ""; - selectedMapIdName: string = ""; - selectedFeature: FeatureWrapper | null = null; + selectedFeatureGeoJsonTexts: string[] = []; + selectedFeatureInspectionModel: InspectionModelData[] = []; + selectedFeatures: FeatureWrapper[] = []; + selectedFeatureGeometryType: any; + selectedFeatureCenter: Cartesian3 | null = null; + selectedFeatureOrigin: Cartesian3 | null = null; + selectedFeatureBoundingRadius: number = 0; + originAndNormalForFeatureZoom: Subject<[Cartesian3, Cartesian3]> = new Subject(); selectedSourceData = new BehaviorSubject(null); // Event called when the active inspector of the inspection panel changed @@ -53,36 +74,52 @@ export class InspectionService { constructor(private mapService: MapService, private jumpService: JumpTargetService, + private infoMessageService: InfoMessageService, + private keyboardService: KeyboardService, public parametersService: ParametersService) { - this.mapService.selectionTopic.pipe(distinctUntilChanged()).subscribe(selectedFeature => { - if (!selectedFeature) { + + this.keyboardService.registerShortcuts(["Ctrl+j", "Ctrl+J"], this.zoomToFeature.bind(this)); + + this.mapService.selectionTopic.pipe(distinctUntilChanged(selectedFeaturesEqualTo)).subscribe(selectedFeatures => { + if (!selectedFeatures?.length) { this.isInspectionPanelVisible = false; this.featureTreeFilterValue = ""; - this.parametersService.unsetSelectedFeature(); + this.parametersService.setSelectedFeatures([]); + this.selectedFeatures = []; return; } - 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); - }); + this.selectedFeatureInspectionModel = []; + this.selectedFeatureGeoJsonTexts = []; + this.selectedFeatures = selectedFeatures; - this.parametersService.parameters.pipe(distinctUntilChanged()).subscribe(parameters => { - if (parameters.selected.length == 2) { - 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); - } - } + // Currently only takes the first element for Jump to Feature functionality. + // TODO: Allow to use the whole set for Jump to Feature. + if (selectedFeatures.length) { + 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; + }); } + if (selectedFeatures.length > 1) { + selectedFeatures.slice(1).forEach(selectedFeature => { + selectedFeature.peek((feature: Feature) => { + this.selectedFeatureInspectionModel.push(...feature.inspectionModel()); + this.selectedFeatureGeoJsonTexts.push(feature.geojson() as string); + this.isInspectionPanelVisible = true; + }); + }); + } + this.loadFeatureData(); + + this.parametersService.setSelectedFeatures(this.selectedFeatures.map(f => f.key())); }); this.selectedSourceData.pipe(distinctUntilChanged(selectedSourceDataEqualTo)).subscribe(selection => { @@ -99,9 +136,9 @@ export class InspectionService { for (const data of dataNodes) { const node: TreeTableNode = {}; let value = data.value; - if (data.type == this.InspectionValueType.NULL.value && data.children === undefined) { + if (data.type == coreLib.ValueType.NULL.value && data.children === undefined) { value = "NULL"; - } else if ((data.type & 128) == 128 && (data.type - 128) == 1) { + } else if ((data.type & coreLib.ValueType.ARRAY.value) && (data.type & coreLib.ValueType.NUMBER.value)) { for (let i = 0; i < value.length; i++) { if (!Number.isInteger(value[i])) { const strValue = String(value[i]) @@ -113,7 +150,7 @@ export class InspectionService { } } - if ((data.type & 128) == 128) { + if (data.type & coreLib.ValueType.ARRAY.value) { value = value.join(", "); } @@ -128,6 +165,9 @@ export class InspectionService { if (data.hasOwnProperty("hoverId")) { node.data["hoverId"] = data.hoverId; } + if (data.hasOwnProperty("mapId")) { + node.data["mapId"] = data.mapId; + } if (data.hasOwnProperty("geoJsonPath")) { node.data["geoJsonPath"] = data.geoJsonPath; } @@ -148,6 +188,9 @@ export class InspectionService { if (section.hasOwnProperty("info")) { node.data["info"] = section.info; } + if (section.hasOwnProperty("sourceDataReferences")) { + node.data["sourceDataReferences"] = section.sourceDataReferences; + } node.children = convertToTreeTableNodes(section.children); treeNodes.push(node); } @@ -171,6 +214,51 @@ export class InspectionService { } } + zoomToFeature() { + if (!this.selectedFeatures) { + this.infoMessageService.showError("Could not zoom to feature: no feature is selected!"); + return; + } + if (!this.selectedFeatureGeometryType) { + this.infoMessageService.showError("Could not zoom to feature: geometry type is missing for the feature!"); + return; + } + + if (this.selectedFeatureGeometryType === this.GeometryType.Mesh) { + let triangle: Array = []; + if (this.selectedFeatureInspectionModel) { + for (const section of this.selectedFeatureInspectionModel) { + if (section.key == "Geometry") { + for (let i = 0; i < 3; i++) { + const cartographic = section.children[0].children[i].value.map((coordinate: string) => Number(coordinate)); + if (cartographic.length == 3) { + triangle.push(Cartesian3.fromDegrees(cartographic[0], cartographic[1], cartographic[2])); + } + } + break; + } + } + } + if (this.selectedFeatureOrigin) { + const normal = Cartesian3.cross( + Cartesian3.subtract(triangle[1], triangle[0], new Cartesian3()), + Cartesian3.subtract(triangle[2], triangle[0], new Cartesian3()), + new Cartesian3() + ); + Cartesian3.negate(normal, normal); + Cartesian3.normalize(normal, normal); + Cartesian3.multiplyByScalar(normal, 3 * this.selectedFeatureBoundingRadius, normal); + this.originAndNormalForFeatureZoom.next([this.selectedFeatureOrigin, normal]); + } + } else if (this.selectedFeatureCenter) { + this.mapService.moveToWgs84PositionTopic.next({ + x: this.selectedFeatureCenter.x, + y: this.selectedFeatureCenter.y, + z: this.selectedFeatureCenter.z + 3 * this.selectedFeatureBoundingRadius + }); + } + } + async loadSourceDataLayer(tileId: number, layerId: string, mapId: string) : Promise { console.log(`Loading SourceDataLayer layerId=${layerId} tileId=${tileId}`); @@ -206,10 +294,20 @@ export class InspectionService { return fetch.go() .then(_ => { if (!layer) - throw new Error(`Error loading layer.`); + throw new Error(`Unknown error while loading layer.`); + const error = layer.getError(); + if (error) { + layer.delete(); + throw new Error(`Error while loading layer: ${error}`); + } return layer; }); } + selectedFeatureGeoJsonCollection() { + return `{"type": "FeatureCollection", "features": [${this.selectedFeatureGeoJsonTexts.join(", ")}]}`; + } + protected readonly InspectionValueType = coreLib.ValueType; + protected readonly GeometryType = coreLib.GeomType; } diff --git a/erdblick_app/app/jump.service.ts b/erdblick_app/app/jump.service.ts index 5eda8f92..b8c48e85 100644 --- a/erdblick_app/app/jump.service.ts +++ b/erdblick_app/app/jump.service.ts @@ -7,8 +7,11 @@ import {InfoMessageService} from "./info.service"; import {coreLib} from "./wasm"; import {FeatureSearchService} from "./feature.search.service"; import {SidePanelService, SidePanelState} from "./sidepanel.service"; +import {HighlightMode} from "build/libs/core/erdblick-core"; export interface SearchTarget { + icon: string; + color: string; name: string; label: string; enabled: boolean; @@ -90,6 +93,8 @@ export class JumpTargetService { label += `
${simfilError}`; } return { + icon: "pi-bolt", + color: "blue", name: "Search Loaded Features", label: label, enabled: false, @@ -113,10 +118,12 @@ export class JumpTargetService { label += `
${fjt.error}`; } return { + icon: "pi-arrow-down-left-and-arrow-up-right-to-center", + color: "orange", name: `Jump to ${fjt.name}`, label: label, enabled: !fjt.error, - execute: (_: string) => { this.jumpToFeature(fjt).then(); }, + execute: (_: string) => { this.highlightByJumpTarget(fjt).then(); }, validate: (_: string) => { return !fjt.error; }, } }); @@ -129,17 +136,17 @@ export class JumpTargetService { ]); } - async highlightFeature(mapId: string, featureId: string) { + async highlightByJumpTargetFilter(mapId: string, featureId: string, mode: HighlightMode=coreLib.HighlightMode.SELECTION_HIGHLIGHT) { 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); + await this.highlightByJumpTarget(featureJumpTargets[validIndex], false, mapId, mode); } - async jumpToFeature(action: FeatureJumpAction, moveCamera: boolean=true, mapId?:string|null) { + async highlightByJumpTarget(action: FeatureJumpAction, moveCamera: boolean=true, mapId?:string|null, mode: HighlightMode=coreLib.HighlightMode.SELECTION_HIGHLIGHT) { // Select the map. if (!mapId) { if (action.maps.length > 1) { @@ -178,10 +185,10 @@ export class JumpTargetService { let selectThisFeature = extRefsResolved.responses[0][0]; // Set feature-to-select on MapService. - await this.mapService.selectFeature( - selectThisFeature.tileId, - selectThisFeature.typeId, - selectThisFeature.featureId, - moveCamera); + const featureId = `${selectThisFeature.typeId}.${selectThisFeature.featureId.filter((_, index) => index % 2 === 1).join('.')}`; + await this.mapService.highlightFeatures([{ + mapTileKey: selectThisFeature.tileId, + featureId: featureId + }], moveCamera, mode).then(); } } diff --git a/erdblick_app/app/keyboard.service.ts b/erdblick_app/app/keyboard.service.ts new file mode 100644 index 00000000..9642207e --- /dev/null +++ b/erdblick_app/app/keyboard.service.ts @@ -0,0 +1,81 @@ +import {Directive, ElementRef, HostListener, Injectable, Renderer2, RendererFactory2} from "@angular/core"; +import {Dialog} from "primeng/dialog"; + +@Directive({ + selector: '[onEnterClick]' +}) +export class OnEnterClickDirective { + constructor(private el: ElementRef) {} + + @HostListener('keydown', ['$event']) + handleKeyDown(event: KeyboardEvent) { + if (event.key === 'Enter') { + this.el.nativeElement.click(); + } + } +} + +@Injectable({providedIn: 'root'}) +export class KeyboardService { + private renderer: Renderer2; + private dialogStack: Array = []; + private shortcuts = new Map void>(); + + constructor(rendererFactory: RendererFactory2) { + this.renderer = rendererFactory.createRenderer(null, null); + this.listenToKeyboardEvents(); + } + + dialogOnShow(event: Dialog) { + this.dialogStack.push(event); + } + + dialogOnHide(event: Dialog) { + this.dialogStack = this.dialogStack.filter(dialog => event !== dialog); + } + + private listenToKeyboardEvents() { + this.renderer.listen('window', 'keydown', (event: KeyboardEvent) => { + const target = event.target as HTMLElement; + const isInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable; + const key = this.getKeyCombination(event); + + if (!isInput || key.includes("Ctrl")) { + if (key === 'Escape' || key === 'Esc') { + // TODO: make this work! + // if (this.dialogStack.length > 0) { + // event.preventDefault(); + // const topDialog = this.dialogStack.pop(); + // if (topDialog) { + // topDialog.close(new MouseEvent("mousedown")); + // } + // } + } else if (this.shortcuts.has(key)) { + event.preventDefault(); + this.shortcuts.get(key)?.(event); + } + } + }); + } + + private getKeyCombination(event: KeyboardEvent): string { + let key = ''; + if (event.ctrlKey) { + key += 'Ctrl+'; + } + key += event.key; + return key; + } + + registerShortcuts(keys: string[], callback: (event: KeyboardEvent) => void) { + keys.forEach(keys_ => this.registerShortcut(keys_, callback)); + } + + registerShortcut(keys: string, callback: (event: KeyboardEvent) => void) { + this.shortcuts.set(keys, callback); + } + + ngOnDestroy() { + this.shortcuts.clear(); + } +} \ 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 b0bc228d..6b92c9ac 100644 --- a/erdblick_app/app/map.panel.component.ts +++ b/erdblick_app/app/map.panel.component.ts @@ -1,4 +1,4 @@ -import {Component, ViewChild, Pipe, PipeTransform} from "@angular/core"; +import {Component, ViewChild} from "@angular/core"; import {InfoMessageService} from "./info.service"; import {CoverageRectItem, MapInfoItem, MapService} from "./map.service"; import {ErdblickStyle, StyleService} from "./style.service"; @@ -11,24 +11,33 @@ import {coreLib} from "./wasm"; import {SidePanelService, SidePanelState} from "./sidepanel.service"; import {MenuItem} from "primeng/api"; import {Menu} from "primeng/menu"; +import {KeyboardService} from "./keyboard.service"; +import {EditorService} from "./editor.service"; +import {DataSourcesService} from "./datasources.service"; @Component({ selector: 'map-panel', template: ` -
+ + + OSM Overlay: - + label="" pTooltip="Toggle OSM overlay" tooltipPosition="bottom" tabindex="0">
- + class="w-full slider-input" tabindex="0"/> +
@@ -43,25 +52,26 @@ import {Menu} from "primeng/menu";
- more_vert - + [label]="mapLayer.key" [binary]="true" tabindex="0"/>
- + [style]="{'padding-left': '0', 'padding-right': '0'}" tabindex="0"> {{ mapLayer.value.tileBorders ? 'select_all' : 'deselect' }} - + [style]="{'padding-left': '0', 'padding-right': '0'}" tabindex="0"> loupe + pTooltip="Change zoom level" tooltipPosition="bottom" tabindex="0">
- +
@@ -89,49 +100,55 @@ import {Menu} from "primeng/menu";
- + (click)="expandStyle(style.key)" tabindex="0"> expand_more - more_vert + (click)="showStylesToggleMenu($event, style.key)" tabindex="0"> + more_vert + - + [label]="style.key" [binary]="true" tabindex="0"/>
- -
- + tooltipPosition="bottom" tabindex="0"> - + tooltipPosition="bottom" tabindex="0"> - + tooltipPosition="bottom" tabindex="0">
-
+
- + style="margin-left: 2.25em; align-items: center; font-size: 0.9em; margin-top: 0.25em"> + + more_vert + + + +
@@ -146,42 +163,43 @@ import {Menu} from "primeng/menu";
- - +
- + icon="{{layerDialogVisible ? 'pi pi-times' : 'pi pi-images'}}" tabindex="0"> -
- -
+
Press Ctrl-S/Cmd-S to save changes
Press Esc to quit without saving
- - - +
+ + + +
@@ -191,11 +209,16 @@ import {Menu} from "primeng/menu";
+ `, - styles: [``] + styles: [` + .disabled { + pointer-events: none; + opacity: 0.5; + } + `] }) export class MapPanelComponent { - editorDialogVisible: boolean = false; layerDialogVisible: boolean = false; warningDialogVisible: boolean = false; mapItems: Map = new Map(); @@ -211,13 +234,18 @@ export class MapPanelComponent { @ViewChild('styleUploader') styleUploader: FileUpload | undefined; @ViewChild('editorDialog') editorDialog: Dialog | undefined; + @ViewChild('mapLayerDialog') mapLayerDialog: Dialog | undefined; constructor(public mapService: MapService, private messageService: InfoMessageService, public styleService: StyleService, public parameterService: ParametersService, - private sidePanelService: SidePanelService) - { + public keyboardService: KeyboardService, + public editorService: EditorService, + public dsService: DataSourcesService, + private sidePanelService: SidePanelService) { + this.keyboardService.registerShortcuts(['m', 'M'], this.showLayerDialog.bind(this)); + this.parameterService.parameters.subscribe(parameters => { this.osmEnabled = parameters.osm; this.osmOpacityValue = parameters.osmOpacity; @@ -229,13 +257,61 @@ export class MapPanelComponent { if (activePanel != SidePanelState.MAPS) { this.layerDialogVisible = false; } - }) + }); + this.editorService.editedSaveTriggered.subscribe(_ => this.applyEditedStyle()); } get osmOpacityString(): string { return 'Opacity: ' + this.osmOpacityValue; } + // TODO: Refactor these into a generic solution + showOptionsToggleMenu(event: MouseEvent, style: ErdblickStyle, optionId: string) { + this.toggleMenu.toggle(event); + this.toggleMenuItems = [ + { + label: 'Toggle All off but This', + command: () => { + for (const id in style.params.options) { + this.styleService.toggleOption(style.id, id, id == optionId); + } + this.applyStyleConfig(style); + // this.mapService.update(); + } + }, + { + label: 'Toggle All on but This', + command: () => { + for (const id in style.params.options) { + this.styleService.toggleOption(style.id, id, id != optionId); + } + this.applyStyleConfig(style); + // this.mapService.update(); + } + }, + { + label: 'Toggle All Off', + command: () => { + for (const id in style.params.options) { + this.styleService.toggleOption(style.id, id, false); + } + this.applyStyleConfig(style); + // this.mapService.update(); + } + }, + { + label: 'Toggle All On', + command: () => { + for (const id in style.params.options) { + this.styleService.toggleOption(style.id, id, true); + } + this.applyStyleConfig(style); + // this.mapService.update(); + } + } + ]; + } + showStylesToggleMenu(event: MouseEvent, styleId: string) { this.toggleMenu.toggle(event); this.toggleMenuItems = [ @@ -350,8 +426,9 @@ export class MapPanelComponent { ); } else { + const position = coreLib.getTilePosition(BigInt(coverage as number)); this.mapService.moveToWgs84PositionTopic.next( - coreLib.getTilePosition(BigInt(coverage as number)) + {x: position.x, y: position.y} ); } } @@ -382,6 +459,12 @@ export class MapPanelComponent { this.mapService.toggleMapLayerVisibility(mapName, layerName); } + expandStyle(styleId: string) { + const style = this.styleService.styles.get(styleId)!; + style.params.showOptions = !style.params.showOptions; + this.applyStyleConfig(style, false); + } + applyStyleConfig(style: ErdblickStyle, redraw: boolean=true) { if (redraw) { this.styleService.reapplyStyle(style.id); @@ -428,11 +511,14 @@ export class MapPanelComponent { } showStyleEditor(styleId: string) { - this.styleService.selectedStyleIdForEditing.next(styleId); - this.editorDialogVisible = true; - this.editedStyleSourceSubscription = this.styleService.styleEditedStateData.subscribe(editedStyleSource => { - const originalStyleSource = this.styleService.styles.get(styleId)?.source!; - this.sourceWasModified = !(editedStyleSource.replace(/\n+$/, '') == originalStyleSource.replace(/\n+$/, '')); + this.styleService.selectedStyleIdForEditing = styleId; + this.editorService.datasourcesEditorVisible = false; + this.editorService.editableData = `${this.styleService.styles.get(styleId)?.source!}\n\n\n\n\n` + this.editorService.readOnly = false; + this.editorService.updateEditorState.next(true); + this.editorService.styleEditorVisible = true; + this.editedStyleSourceSubscription = this.editorService.editedStateData.subscribe(editedStyleSource => { + this.sourceWasModified = !(editedStyleSource.replace(/\n+$/, '') == this.editorService.editableData.replace(/\n+$/, '')); }); this.savedStyleSourceSubscription = this.styleService.styleEditedSaveTriggered.subscribe(_ => { this.applyEditedStyle(); @@ -440,8 +526,9 @@ export class MapPanelComponent { } applyEditedStyle() { - const styleId = this.styleService.selectedStyleIdForEditing.getValue(); - const styleData = this.styleService.styleEditedStateData.getValue().replace(/\n+$/, ''); + const styleId = this.styleService.selectedStyleIdForEditing; + this.editorService.editableData = this.editorService.editedStateData.getValue(); + const styleData = this.editorService.editedStateData.getValue().replace(/\n+$/, ''); if (!styleId) { this.messageService.showError(`No cached style ID found!`); return; @@ -473,8 +560,7 @@ export class MapPanelComponent { } discardStyleEdits() { - const styleId = this.styleService.selectedStyleIdForEditing.getValue(); - this.styleService.selectedStyleIdForEditing.next(styleId); + this.editorService.updateEditorState.next(false); this.warningDialogVisible = false; } @@ -485,4 +571,9 @@ export class MapPanelComponent { unordered(a: KeyValue, b: KeyValue): number { return 0; } + + openDatasources() { + this.editorService.styleEditorVisible = false; + this.editorService.datasourcesEditorVisible = true; + } } diff --git a/erdblick_app/app/map.service.ts b/erdblick_app/app/map.service.ts index 9f9c2572..70a2ad01 100644 --- a/erdblick_app/app/map.service.ts +++ b/erdblick_app/app/map.service.ts @@ -3,24 +3,27 @@ import {Fetch} from "./fetch.model"; import {FeatureTile, FeatureWrapper} from "./features.model"; import {coreLib, uint8ArrayToWasm} from "./wasm"; import {TileVisualization} from "./visualization.model"; -import {BehaviorSubject, Subject} from "rxjs"; +import {BehaviorSubject, distinctUntilChanged, Subject} from "rxjs"; import {ErdblickStyle, StyleService} from "./style.service"; -import {FeatureLayerStyle, TileLayerParser, Feature} from '../../build/libs/core/erdblick-core'; -import {ParametersService} from "./parameters.service"; +import {FeatureLayerStyle, TileLayerParser, Feature, HighlightMode} from '../../build/libs/core/erdblick-core'; +import {ParametersService, TileFeatureId} from "./parameters.service"; import {SidePanelService, SidePanelState} from "./sidepanel.service"; import {InfoMessageService} from "./info.service"; import {MAX_ZOOM_LEVEL} from "./feature.search.service"; +import {PointMergeService} from "./pointmerge.service"; -export interface CoverageRectItem extends Object { +/** Expected structure of a LayerInfoItem's coverage entry. */ +export interface CoverageRectItem extends Record { min: number, max: number } -export interface LayerInfoItem extends Object { +/** Expected structure of a list entry in the MapInfoItem's layer entry. */ +export interface LayerInfoItem extends Record { canRead: boolean; canWrite: boolean; coverage: Array; - featureTypes: Array<{name: string, uniqueIdCompositions: Array}>; + featureTypes: Array<{name: string, uniqueIdCompositions: Array}>; layerId: string; type: string; version: {major: number, minor: number, patch: number}; @@ -30,8 +33,9 @@ export interface LayerInfoItem extends Object { tileBorders: boolean; } -export interface MapInfoItem extends Object { - extraJsonAttachment: Object; +/** Expected structure of a list entry in the /sources endpoint. */ +export interface MapInfoItem extends Record { + extraJsonAttachment: any; layers: Map; mapId: string; maxParallelJobs: number; @@ -44,6 +48,7 @@ export interface MapInfoItem extends Object { const infoUrl = "/sources"; const tileUrl = "/tiles"; +/** Redefinition of coreLib.Viewport. TODO: Check if needed. */ type ViewportProperties = { orientation: number; camPosLon: number; @@ -54,6 +59,13 @@ type ViewportProperties = { camPosLat: number }; +/** + * Determine if two lists of feature wrappers have the same features. + */ +function featureSetsEqual(rhs: FeatureWrapper[], lhs: FeatureWrapper[]) { + return rhs.length === lhs.length && rhs.every(rf => lhs.some(lf => rf.equals(lf))); +} + /** * Erdblick map service class. This class is responsible for keeping track * of the following objects: @@ -71,20 +83,21 @@ export class MapService { public maps: BehaviorSubject> = new BehaviorSubject>(new Map()); public loadedTileLayers: Map; private visualizedTileLayers: Map; - private currentFetch: any; + private currentFetch: Fetch|null = null; private currentViewport: ViewportProperties; private currentVisibleTileIds: Set; private currentHighDetailTileIds: Set; private tileStreamParsingQueue: any[]; private tileVisualizationQueue: [string, TileVisualization][]; private selectionVisualizations: TileVisualization[]; + private hoverVisualizations: TileVisualization[]; tileParser: TileLayerParser|null = null; tileVisualizationTopic: Subject; tileVisualizationDestructionTopic: Subject; - moveToWgs84PositionTopic: Subject<{x: number, y: number}>; - allViewportTileIds: Map = new Map(); - selectionTopic: BehaviorSubject = new BehaviorSubject(null); + moveToWgs84PositionTopic: Subject<{x: number, y: number, z?: number}>; + selectionTopic: BehaviorSubject> = new BehaviorSubject>([]); + hoverTopic: BehaviorSubject> = new BehaviorSubject>([]); selectionTileRequest: { remoteRequest: { mapId: string, @@ -100,7 +113,9 @@ export class MapService { constructor(public styleService: StyleService, public parameterService: ParametersService, private sidePanelService: SidePanelService, - private messageService: InfoMessageService) { + private messageService: InfoMessageService, + private pointMergeService: PointMergeService) + { this.loadedTileLayers = new Map(); this.visualizedTileLayers = new Map(); this.currentFetch = null; @@ -118,6 +133,7 @@ export class MapService { this.tileStreamParsingQueue = []; this.tileVisualizationQueue = []; this.selectionVisualizations = []; + this.hoverVisualizations = []; // Triggered when a tile layer is freshly rendered and should be added to the frontend. this.tileVisualizationTopic = new Subject(); // {FeatureTile} @@ -164,29 +180,15 @@ export class MapService { await this.reloadDataSources(); - this.selectionTopic.subscribe(selectedFeatureWrapper => { - this.selectionVisualizations.forEach(visu => this.tileVisualizationDestructionTopic.next(visu)); - this.selectionVisualizations = []; - - if (this.sidePanelService.panel != SidePanelState.FEATURESEARCH) { - this.sidePanelService.panel = SidePanelState.NONE; - } - if (!selectedFeatureWrapper) - return; + this.parameterService.parameters.pipe(distinctUntilChanged()).subscribe(parameters => { + this.highlightFeatures(parameters.selected).then(); + }); - // Apply additional highlight styles. - for (let [_, styleData] of this.styleService.styles) { - if (styleData.featureLayerStyle && styleData.params.visible) { - let visu = new TileVisualization( - selectedFeatureWrapper!.featureTile, - (tileKey: string)=>this.getFeatureTile(tileKey), - styleData.featureLayerStyle, - true, - selectedFeatureWrapper.peek((f: Feature) => f.id())); - this.tileVisualizationTopic.next(visu); - this.selectionVisualizations.push(visu); - } - } + this.selectionTopic.subscribe(selectedFeatureWrappers => { + this.visualizeHighlights(coreLib.HighlightMode.SELECTION_HIGHLIGHT, selectedFeatureWrappers); + }); + this.hoverTopic.subscribe(hoveredFeatureWrappers => { + this.visualizeHighlights(coreLib.HighlightMode.HOVER_HIGHLIGHT, hoveredFeatureWrappers); }); } @@ -393,7 +395,7 @@ export class MapService { // Evict present non-required tile layers. let newTileLayers = new Map(); let evictTileLayer = (tileLayer: FeatureTile) => { - return !tileLayer.preventCulling && (!this.currentVisibleTileIds.has(tileLayer.tileId) || + return !tileLayer.preventCulling && !this.selectionTopic.getValue().some(v => v.featureTile.mapTileKey == tileLayer.mapTileKey) && (!this.currentVisibleTileIds.has(tileLayer.tileId) || !this.getMapLayerVisibility(tileLayer.mapName, tileLayer.layerName) || tileLayer.level() != this.getMapLayerLevel(tileLayer.mapName, tileLayer.layerName)) } @@ -401,7 +403,7 @@ export class MapService { if (evictTileLayer(tileLayer)) { tileLayer.destroy(); } else { - newTileLayers.set(tileLayer.id, tileLayer); + newTileLayers.set(tileLayer.mapTileKey, tileLayer); } } this.loadedTileLayers = newTileLayers; @@ -448,12 +450,21 @@ export class MapService { // TODO: Consider tile TTL. let requests = []; if (this.selectionTileRequest) { - requests.push(this.selectionTileRequest.remoteRequest); - - if (this.currentFetch) { - // Disable the re-fetch filtering logic by setting the old - // fetches' body to null. - this.currentFetch.bodyJson = null; + // Do not go forward with the selection tile request, if it + // pertains to a map layer that is not available anymore. + const mapLayerItem = this.maps.getValue() + .get(this.selectionTileRequest.remoteRequest.mapId)?.layers + .get(this.selectionTileRequest.remoteRequest.layerId); + if (mapLayerItem) { + requests.push(this.selectionTileRequest.remoteRequest); + if (this.currentFetch) { + // Disable the re-fetch filtering logic by setting the old + // fetches' body to null. + this.currentFetch.bodyJson = null; + } + } + else { + this.selectionTileRequest.reject!("Map layer is not available."); } } @@ -527,23 +538,22 @@ export class MapService { let tileLayer = new FeatureTile(this.tileParser!, tileLayerBlob, preventCulling); // Consider, if this tile is a selection tile request. - if (this.selectionTileRequest && tileLayer.id == this.selectionTileRequest.tileKey) { + if (this.selectionTileRequest && tileLayer.mapTileKey == this.selectionTileRequest.tileKey) { this.selectionTileRequest.resolve!(tileLayer); this.selectionTileRequest = null; } - // Don't add a tile that is not supposed to be visible. - if (!preventCulling) { + else if (!preventCulling) { if (!this.currentVisibleTileIds.has(tileLayer.tileId)) return; } // If this one replaces an older tile with the same key, // then first remove the older existing one. - if (this.loadedTileLayers.has(tileLayer.id)) { - this.removeTileLayer(this.loadedTileLayers.get(tileLayer.id)); + if (this.loadedTileLayers.has(tileLayer.mapTileKey)) { + this.removeTileLayer(this.loadedTileLayers.get(tileLayer.mapTileKey)!); } - this.loadedTileLayers.set(tileLayer.id, tileLayer); + this.loadedTileLayers.set(tileLayer.mapTileKey, tileLayer); // Schedule the visualization of the newly added tile layer, // but don't do it synchronously to avoid stalling the main thread. @@ -558,11 +568,11 @@ export class MapService { }); } - private removeTileLayer(tileLayer: any) { - tileLayer.destroy() + private removeTileLayer(tileLayer: FeatureTile) { + tileLayer.destroy(); for (const styleId of this.visualizedTileLayers.keys()) { const tileVisus = this.visualizedTileLayers.get(styleId)?.filter(tileVisu => { - if (tileVisu.tile.id === tileLayer.id) { + if (tileVisu.tile.mapTileKey === tileLayer.mapTileKey) { this.tileVisualizationDestructionTopic.next(tileVisu); return false; } @@ -575,9 +585,9 @@ export class MapService { } } this.tileVisualizationQueue = this.tileVisualizationQueue.filter(([_, tileVisu]) => { - return tileVisu.tile.id !== tileLayer.id; + return tileVisu.tile.mapTileKey !== tileLayer.mapTileKey; }); - this.loadedTileLayers.delete(tileLayer.id); + this.loadedTileLayers.delete(tileLayer.mapTileKey); } private renderTileLayer(tileLayer: FeatureTile, style: ErdblickStyle|FeatureLayerStyle, styleId: string = "") { @@ -591,10 +601,12 @@ export class MapService { const layerName = tileLayer.layerName; let visu = new TileVisualization( tileLayer, + this.pointMergeService, (tileKey: string)=>this.getFeatureTile(tileKey), wasmStyle, tileLayer.preventCulling || this.currentHighDetailTileIds.has(tileLayer.tileId), - "", + coreLib.HighlightMode.NO_HIGHLIGHT, + [], this.getMapLayerBorderState(mapName, layerName), (style as ErdblickStyle).params !== undefined ? (style as ErdblickStyle).params.options : {}); this.tileVisualizationQueue.push([styleId, visu]); @@ -605,7 +617,7 @@ export class MapService { } } - setViewport(viewport: any) { + setViewport(viewport: ViewportProperties) { this.currentViewport = viewport; this.setTileLevelForViewport(); this.update(); @@ -624,62 +636,172 @@ export class MapService { return this.loadedTileLayers.get(tileKey) || null; } - async loadTileForSelection(tileKey: string) { - if (this.loadedTileLayers.has(tileKey)) { - return this.loadedTileLayers.get(tileKey)!; - } + async loadTiles(tileKeys: Set): Promise> { + let result = new Map(); - let [mapId, layerId, tileId] = coreLib.parseTileFeatureLayerKey(tileKey); - this.selectionTileRequest = { - remoteRequest: { - mapId: mapId, - layerId: layerId, - tileIds: [Number(tileId)], - }, - tileKey: tileKey, - resolve: null, - reject: null, - } + // TODO: Optimize this loop to make just a single update call. + for (let tileKey of tileKeys) { + if (!tileKey) { + continue; + } - let selectionTilePromise = new Promise((resolve, reject)=>{ - this.selectionTileRequest!.resolve = resolve; - this.selectionTileRequest!.reject = reject; - }) + let tile = this.loadedTileLayers.get(tileKey); + if (tile) { + result.set(tileKey, tile); + continue; + } - this.update(); - return selectionTilePromise; + let [mapId, layerId, tileId] = coreLib.parseTileFeatureLayerKey(tileKey); + this.selectionTileRequest = { + remoteRequest: { + mapId: mapId, + layerId: layerId, + tileIds: [Number(tileId)], + }, + tileKey: tileKey, + resolve: null, + reject: null, + } + + let selectionTilePromise = new Promise((resolve, reject)=>{ + this.selectionTileRequest!.resolve = resolve; + this.selectionTileRequest!.reject = reject; + }) + + this.update(); + tile = await selectionTilePromise; + result.set(tileKey, tile); + } + + return result; } - async selectFeature(tileKey: string, typeId: string, idParts: Array, focus: boolean=false) { - let tile = await this.loadTileForSelection(tileKey); - let feature = new FeatureWrapper( - tile.peek(layer => layer.findFeatureIndex(typeId, idParts)), - tile); - if (feature.index < 0) { - let [mapId, layerId, tileId] = coreLib.parseTileFeatureLayerKey(tileKey); - this.messageService.showError( - `The feature ${typeId+idParts.map((val, n)=>((n%2)==1?val:".")).join("")}`+ - `does not exist in the ${layerId} layer of tile ${tileId} of map ${mapId}.`); - return; + async highlightFeatures(tileFeatureIds: (TileFeatureId|null|string)[], focus: boolean=false, mode: HighlightMode=coreLib.HighlightMode.SELECTION_HIGHLIGHT) { + // Load the tiles for the selection. + const tiles = await this.loadTiles( + new Set(tileFeatureIds.filter(s => s && typeof s !== "string").map(s => (s as TileFeatureId).mapTileKey))); + + // Ensure that the feature really exists in the tile. + let features = new Array(); + for (let id of tileFeatureIds) { + if (typeof id == "string") { + // When clicking on geometry that represents a highlight, + // this is reflected in the feature id. By processing this + // info here, a hover highlight can be turned into a selection. + if (id == "hover-highlight") { + features = this.hoverTopic.getValue(); + } + else if (id == "selection-highlight") { + features = this.selectionTopic.getValue(); + } + continue; + } + + if (!id?.featureId) { + continue; + } + + const tile = tiles.get(id?.mapTileKey || ""); + if (!tile) { + console.error(`Could not load tile ${id?.mapTileKey} for highlighting!`); + continue; + } + if (!tile.has(id?.featureId || "")) { + const [mapId, layerId, tileId] = coreLib.parseTileFeatureLayerKey(id?.mapTileKey || ""); + this.messageService.showError( + `The feature ${id?.featureId} does not exist in the ${layerId} layer of tile ${tileId} of map ${mapId}.`); + continue; + } + + features.push(new FeatureWrapper(id!.featureId, tile)); + } + + if (mode == coreLib.HighlightMode.HOVER_HIGHLIGHT) { + if (features.length) { + if (featureSetsEqual(this.selectionTopic.getValue(), features)) { + return; + } + } + if (featureSetsEqual(this.hoverTopic.getValue(), features)) { + return; + } + this.hoverTopic.next(features); + } + else if (mode == coreLib.HighlightMode.SELECTION_HIGHLIGHT) { + if (featureSetsEqual(this.selectionTopic.getValue(), features)) { + return; + } + if (featureSetsEqual(this.hoverTopic.getValue(), features)) { + this.hoverTopic.next([]); + } + this.selectionTopic.next(features); + } + else { + console.error(`Unsupported highlight mode!`); } - this.selectionTopic.next(feature); - if (focus) { - this.focusOnFeature(feature); + + // TODO: Focus on bounding box of all features? + if (focus && features.length) { + this.focusOnFeature(features[0]); } } focusOnFeature(feature: FeatureWrapper) { const position = feature.peek((parsedFeature: Feature) => parsedFeature.center()); - this.moveToWgs84PositionTopic.next(position); + this.moveToWgs84PositionTopic.next({x: position.x, y: position.y}); } setTileLevelForViewport() { for (const level of [...Array(MAX_ZOOM_LEVEL + 1).keys()]) { - if (coreLib.getNumTileIds(this.currentViewport, level) >= 15) { + if (coreLib.getNumTileIds(this.currentViewport, level) >= 48) { this.zoomLevel.next(level); return; } } this.zoomLevel.next(MAX_ZOOM_LEVEL); } + + private visualizeHighlights(mode: HighlightMode, featureWrappers: Array) { + let visualizationCollection = null; + switch (mode) { + case coreLib.HighlightMode.SELECTION_HIGHLIGHT: + if (this.sidePanelService.panel != SidePanelState.FEATURESEARCH) { + this.sidePanelService.panel = SidePanelState.NONE; + } + visualizationCollection = this.selectionVisualizations; + break; + case coreLib.HighlightMode.HOVER_HIGHLIGHT: + visualizationCollection = this.hoverVisualizations; break; + default: + console.error(`Bad visualization mode ${mode}!`); + return; + } + + while (visualizationCollection.length) { + this.tileVisualizationDestructionTopic.next(visualizationCollection.pop()); + } + if (!featureWrappers.length) { + return; + } + + // Apply highlight styles. + const featureTile = featureWrappers[0].featureTile; + const featureIds = featureWrappers.map(fw => fw.featureId); + for (let [_, style] of this.styleService.styles) { + if (style.featureLayerStyle && style.params.visible) { + let visu = new TileVisualization( + featureTile, + this.pointMergeService, + (tileKey: string)=>this.getFeatureTile(tileKey), + style.featureLayerStyle, + true, + mode, + featureIds, + false, + style.params.options); + this.tileVisualizationTopic.next(visu); + visualizationCollection.push(visu); + } + } + } } diff --git a/erdblick_app/app/parameters.service.ts b/erdblick_app/app/parameters.service.ts index 1ef8ce2d..f21d0451 100644 --- a/erdblick_app/app/parameters.service.ts +++ b/erdblick_app/app/parameters.service.ts @@ -1,23 +1,32 @@ import {Injectable} from "@angular/core"; -import {BehaviorSubject, Subject} from "rxjs"; +import {BehaviorSubject} from "rxjs"; import {Cartesian3, Cartographic, CesiumMath, Camera} from "./cesium"; import {Params, Router} from "@angular/router"; -import {ErdblickStyle} from "./style.service"; -import {InspectionService, SelectedSourceData} from "./inspection.service"; +import {SelectedSourceData} from "./inspection.service"; export const MAX_NUM_TILES_TO_LOAD = 2048; export const MAX_NUM_TILES_TO_VISUALIZE = 512; +/** + * Combination of a tile id and a feature id, which may be resolved + * to a feature object. + */ +export interface TileFeatureId { + featureId: string, + mapTileKey: string, +} + export interface StyleParameters { visible: boolean, - options: Record, + options: Record, showOptions: boolean, } interface ErdblickParameters extends Record { + search: [number, string] | [], marker: boolean, markedPosition: Array, - selected: Array, + selected: TileFeatureId[], heading: number, pitch: number, roll: number, @@ -32,6 +41,7 @@ interface ErdblickParameters extends Record { tilesVisualizeLimit: number, enabledCoordsTileIds: Array, selectedSourceData: Array, + panel: Array } interface ParameterDescriptor { @@ -45,7 +55,29 @@ interface ParameterDescriptor { urlParam: boolean } +/** Function to create an object validator given a key-typeof-value dictionary. */ +function validateObject(fields: Record) { + return (o: object) => { + if (typeof o !== "object") { + return false; + } + for (let [key, value] of Object.entries(o)) { + let valueType = typeof value; + if (valueType !== fields[key]) { + return false; + } + } + return true; + }; +} + const erdblickParameters: Record = { + search: { + converter: val => JSON.parse(val), + validator: val => Array.isArray(val) && (val.length === 0 || (val.length === 2 && typeof val[0] === 'number' && typeof val[1] === 'string')), + default: [], + urlParam: true + }, marker: { converter: val => val === 'true', validator: val => typeof val === 'boolean', @@ -60,7 +92,7 @@ const erdblickParameters: Record = { }, selected: { converter: val => JSON.parse(val), - validator: val => Array.isArray(val) && val.every(item => typeof item === 'string'), + validator: val => Array.isArray(val) && val.every(validateObject({mapTileKey: "string", featureId: "string"})), default: [], urlParam: true }, @@ -120,8 +152,11 @@ const erdblickParameters: Record = { }, styles: { converter: val => JSON.parse(val), - validator: val => typeof val === "object" && Object.entries(val as Record).every(([_, v]) => typeof v["visible"] === "boolean" && typeof v["showOptions"] === "boolean" && typeof v["options"] === "object"), - default: new Map(), + validator: val => { + return typeof val === "object" && Object.entries(val as Record).every( + ([_, v]) => validateObject({visible: "boolean", showOptions: "boolean", options: "object"})(v)); + }, + default: {}, urlParam: true }, tilesLoadLimit: { @@ -147,6 +182,12 @@ const erdblickParameters: Record = { validator: Array.isArray, default: [], urlParam: true + }, + panel: { + converter: val => JSON.parse(val), + validator: val => Array.isArray(val) && (!val.length || val.length == 2 && val.every(item => typeof item === 'number')), + default: [], + urlParam: true } }; @@ -167,17 +208,38 @@ export class ParametersService { } }); + lastSearchHistoryEntry: BehaviorSubject<[number, string] | null> = new BehaviorSubject<[number, string] | null>(null); + + baseFontSize: number = 16; + inspectionContainerWidth: number = 40; + inspectionContainerHeight: number = (window.innerHeight - 10.5 * this.baseFontSize); + + private baseCameraMoveM = 100.0; + private baseCameraZoomM = 100.0; + private scalingFactor = 1; + constructor(public router: Router) { + this.baseFontSize = parseFloat(window.getComputedStyle(document.documentElement).fontSize); + let parameters = this.loadSavedParameters(); this.parameters = new BehaviorSubject(parameters!); this.saveParameters(); this.parameters.subscribe(parameters => { if (parameters) { + this.scalingFactor = Math.pow(parameters.alt / 1000, 1.1) / 2; this.saveParameters(); } }); } + get cameraMoveUnits() { + return this.baseCameraMoveM * this.scalingFactor / 75000; + } + + get cameraZoomUnits() { + return this.baseCameraZoomM * this.scalingFactor; + } + get replaceUrl() { const currentValue = this._replaceUrl; this._replaceUrl = true; @@ -194,7 +256,7 @@ export class ParametersService { selection.layerId, selection.mapId, selection.address.toString(), - selection.featureId, + selection.featureIds, ]; this.parameters.next(this.p()); } @@ -214,7 +276,7 @@ export class ParametersService { layerId: sd[1], mapId: sd[2], address: BigInt(sd[3] || '0'), - featureId: sd[4], + featureIds: sd[4], }; } @@ -236,18 +298,28 @@ export class ParametersService { this.parameters.next(this.p()); } - setSelectedFeature(mapId: string, featureId: string) { + setSelectedFeatures(newSelection: TileFeatureId[]) { const currentSelection = this.p().selected; - if (currentSelection && (currentSelection[0] != mapId || currentSelection[1] != featureId)) { - this.p().selected = [mapId, featureId]; - this._replaceUrl = false; - this.parameters.next(this.p()); + if (currentSelection.length == newSelection.length) { + let selectedFeaturesAreSame = true; + for (let i = 0; i < currentSelection.length; ++i) { + const a = currentSelection[i]; + const b = newSelection[i]; + if (a.featureId != b.featureId || a.mapTileKey != b.mapTileKey) { + selectedFeaturesAreSame = false; + break; + } + } + + if (selectedFeaturesAreSame) { + return false; + } } - } - unsetSelectedFeature() { - this.p().selected = []; + this.p().selected = newSelection; + this._replaceUrl = false; this.parameters.next(this.p()); + return true; } setMarkerState(enabled: boolean) { @@ -259,7 +331,7 @@ export class ParametersService { } } - setMarkerPosition(position: Cartographic | null) { + setMarkerPosition(position: Cartographic | null, delayUpdate: boolean=false) { if (position) { const longitude = CesiumMath.toDegrees(position.longitude); const latitude = CesiumMath.toDegrees(position.latitude); @@ -267,7 +339,10 @@ export class ParametersService { } else { this.p().markedPosition = []; } - this.parameters.next(this.p()); + if (!delayUpdate) { + this._replaceUrl = false; + this.parameters.next(this.p()); + } } mapLayerConfig(mapId: string, layerId: string, fallbackLevel: number): [boolean, number, boolean] { @@ -292,13 +367,14 @@ export class ParametersService { } styleConfig(styleId: string): StyleParameters { - if (this.p().styles.hasOwnProperty(styleId)) - return this.p().styles[styleId] + if (this.p().styles.hasOwnProperty(styleId)) { + return this.p().styles[styleId]; + } return { visible: !Object.entries(this.p().styles).length, options: {}, showOptions: true, - } + }; } setStyleConfig(styleId: string, params: StyleParameters) { @@ -315,6 +391,11 @@ export class ParametersService { this.p().pitch = camera.pitch; this.p().roll = camera.roll; this.parameters.next(this.p()); + this.setView(Cartesian3.fromDegrees(this.p().lon, this.p().lat, this.p().alt), { + heading: this.p().heading, + pitch: this.p().pitch, + roll: this.p().roll + }); } loadSavedParameters(): ErdblickParameters | null { @@ -405,4 +486,63 @@ export class ParametersService { } return false; } + + resetSearchHistoryState() { + this.p().search = []; + this.parameters.next(this.p()); + } + + setSearchHistoryState(value: [number, string] | null, saveHistory: boolean = true) { + if (value) { + value[1] = value[1].trim(); + if (saveHistory) { + this.saveHistoryStateValue(value); + } + } + this.p().search = value ? value : []; + this._replaceUrl = false; + this.parameters.next(this.p()) + this.lastSearchHistoryEntry.next(value); + } + + private saveHistoryStateValue(value: [number, string]) { + const searchHistoryString = localStorage.getItem("searchHistory"); + if (searchHistoryString) { + let searchHistory = JSON.parse(searchHistoryString) as Array<[number, string]>; + searchHistory = searchHistory.filter((entry: [number, string]) => !(entry[0] == value[0] && entry[1] == value[1])); + searchHistory.unshift(value); + let ldiff = searchHistory.length - 100; + while (ldiff > 0) { + searchHistory.pop(); + ldiff -= 1; + } + localStorage.setItem("searchHistory", JSON.stringify(searchHistory)); + } else { + localStorage.setItem("searchHistory", JSON.stringify(value)); + } + } + + onInspectionContainerResize(event: MouseEvent): void { + const element = event.target as HTMLElement; + if (!element.classList.contains("resizable-container")) { + return; + } + if (!element.offsetWidth || !element.offsetHeight) { + return; + } + this.baseFontSize = parseFloat(window.getComputedStyle(document.documentElement).fontSize); + const currentEmWidth = element.offsetWidth / this.baseFontSize; + if (currentEmWidth < 40.0) { + this.inspectionContainerWidth = 40 * this.baseFontSize; + } else { + this.inspectionContainerWidth = element.offsetWidth; + } + this.inspectionContainerHeight = element.offsetHeight; + + this.p().panel = [ + this.inspectionContainerWidth / this.baseFontSize, + this.inspectionContainerHeight / this.baseFontSize + ]; + this.parameters.next(this.p()); + } } diff --git a/erdblick_app/app/pointmerge.service.ts b/erdblick_app/app/pointmerge.service.ts new file mode 100644 index 00000000..f54289ee --- /dev/null +++ b/erdblick_app/app/pointmerge.service.ts @@ -0,0 +1,255 @@ +import {Injectable} from "@angular/core"; +import { + PointPrimitiveCollection, + LabelCollection, + Viewer, + Entity +} from "./cesium"; +import {coreLib} from "./wasm"; +import {TileFeatureId} from "./parameters.service"; + +type MapLayerStyleRule = string; +type PositionHash = string; +type Cartographic = {x: number, y: number, z: number}; + +/** + * Class which represents a set of merged point features for one location. + * Each merged point feature may be visualized as a label or a point. + * To this end, the visualization retains visualization parameters for + * calls to either/both Cesium PointPrimitiveCollection.add() and/or LabelCollection.add(). + */ +export interface MergedPointVisualization { + position: Cartographic, + positionHash: PositionHash, + pointParameters: any, // Point Visualization Parameters for call to PointPrimitiveCollection.add(). + labelParameters: any, // Label Visualization Parameters for call to LabelCollection.add(). + featureIds: Array +} + +/** + * Container of MergedPointVisualizations, sitting at the corner point of + * four surrounding tiles. It covers a quarter of the area of each surrounding + * tile. Note: A MergedPointsTile is always unique for its NW corner tile ID + * and its Map-Layer-Style-Rule ID. + */ +export class MergedPointsTile { + tileId: bigint = 0n; // NW tile ID + mapLayerStyleRuleId: MapLayerStyleRule = ""; + + referencingTiles: Array = []; + + pointPrimitives: PointPrimitiveCollection|null = null; + labelPrimitives: LabelCollection|null = null; + debugEntity: Entity|null = null; + + features: Map = new Map; + + add(point: MergedPointVisualization) { + let existingPoint = this.features.get(point.positionHash); + if (!existingPoint) { + this.features.set(point.positionHash, point); + } + else { + let anyNewFeatureIdAdded = false; + for (let fid of point.featureIds) { + if (existingPoint.featureIds.findIndex(v => v.featureId == fid.featureId) == -1) { + existingPoint.featureIds.push(fid); + anyNewFeatureIdAdded = true; + } + } + if (anyNewFeatureIdAdded) { + if (point.pointParameters) { + existingPoint.pointParameters = point.pointParameters; + } + if (point.labelParameters) { + existingPoint.labelParameters = point.labelParameters; + } + } + } + } + + count(positionHash: PositionHash) { + return this.features.has(positionHash) ? this.features.get(positionHash)!.featureIds.length : 0; + } + + render(viewer: Viewer) { + if (this.pointPrimitives || this.labelPrimitives) { + this.remove(viewer); + } + + this.pointPrimitives = new PointPrimitiveCollection(); + this.labelPrimitives = new LabelCollection(); + + for (let [_, feature] of this.features) { + if (feature.pointParameters) { + feature.pointParameters["id"] = feature.featureIds; + this.pointPrimitives.add(feature.pointParameters); + } + if (feature.labelParameters) { + feature.labelParameters["id"] = feature.featureIds; + this.labelPrimitives.add(feature.labelParameters); + } + } + + if (this.pointPrimitives.length) { + viewer.scene.primitives.add(this.pointPrimitives) + } + if (this.labelPrimitives.length) { + viewer.scene.primitives.add(this.labelPrimitives) + } + + // On-demand debug visualization: + // Adding debug bounding box and label for tile ID and feature count + // const tileBounds = coreLib.getCornerTileBox(this.tileId); + // this.debugEntity = viewer.entities.add({ + // rectangle: { + // coordinates: Rectangle.fromDegrees(...tileBounds), + // material: Color.BLUE.withAlpha(0.2), + // outline: true, + // outlineColor: Color.BLUE, + // outlineWidth: 3, + // height: HeightReference.CLAMP_TO_GROUND, + // }, + // position: Cartesian3.fromDegrees( + // (tileBounds[0]+tileBounds[2])*.5, + // (tileBounds[1]+tileBounds[3])*.5 + // ), + // label: { + // text: `Tile ID: ${this.tileId.toString()}\nPoints: ${this.features.size}\nreferencingTiles: ${this.referencingTiles}`, + // showBackground: true, + // font: '14pt monospace', + // eyeOffset: new Cartesian3(0, 0, -10), // Ensures label visibility at a higher altitude + // fillColor: Color.YELLOW, + // outlineColor: Color.BLACK, + // outlineWidth: 2, + // } + // }); + } + + remove(viewer: Viewer) { + if (this.pointPrimitives && this.pointPrimitives.length) { + viewer.scene.primitives.remove(this.pointPrimitives) + } + if (this.labelPrimitives && this.labelPrimitives.length) { + viewer.scene.primitives.remove(this.labelPrimitives) + } + if (this.debugEntity) { + viewer.entities.remove(this.debugEntity); + } + } + + /** + * Add a neighboring tile which keeps this corner tile alive + */ + addReference(sourceTileId: bigint) { + if (this.referencingTiles.findIndex(v => v == sourceTileId) == -1) { + this.referencingTiles.push(sourceTileId); + } + } +} + +/** + * Service which manages the CRUD cycle of MergedPointsTiles. + */ +@Injectable({providedIn: 'root'}) +export class PointMergeService +{ + mergedPointsTiles: Map> = new Map>(); + + /** + * Count how many points have been merged for the given position and style rule so far. + */ + count(geoPos: Cartographic, hashPos: PositionHash, level: number, mapLayerStyleRuleId: MapLayerStyleRule): number { + return this.getCornerTileByPosition(geoPos, level, mapLayerStyleRuleId).count(hashPos); + } + + /** + * Get or create a MergedPointsTile for a particular cartographic location. + * Calculates the tile ID of the given location. If the position + * is north if the tile center, the tile IDs y component is decremented (unless it is already 0). + * If the position is west of the tile center, the tile IDs x component is decremented (unless it is already 0). + */ + getCornerTileByPosition(geoPos: Cartographic, level: number, mapLayerStyleRuleId: MapLayerStyleRule): MergedPointsTile { + // Calculate the correct corner tile ID. + let tileId = coreLib.getTileIdFromPosition(geoPos.x, geoPos.y, level); + let tilePos = coreLib.getTilePosition(tileId); + let offsetX = 0; + let offsetY = 0; + if (geoPos.x < tilePos.x) + offsetX = -1; + if (geoPos.y > tilePos.y) + offsetY = -1; + tileId = coreLib.getTileNeighbor(tileId, offsetX, offsetY); + return this.getCornerTileById(tileId, mapLayerStyleRuleId); + } + + /** + * Get (or create) a corner tile by its style-rule-id + tile-id combo. + */ + getCornerTileById(tileId: bigint, mapLayerStyleRuleId: MapLayerStyleRule): MergedPointsTile { + // Get or create the tile-map for the mapLayerStyleRuleId. + let styleRuleMap = this.mergedPointsTiles.get(mapLayerStyleRuleId); + if (!styleRuleMap) { + styleRuleMap = new Map(); + this.mergedPointsTiles.set(mapLayerStyleRuleId, styleRuleMap); + } + + // Get or create the entry for the tile in the map. + let result = styleRuleMap.get(tileId); + if (!result) { + result = new MergedPointsTile(); + result.tileId = tileId; + result.mapLayerStyleRuleId = mapLayerStyleRuleId; + styleRuleMap.set(tileId, result); + } + return result; + } + + /** + * Insert (or update) a bunch of point visualizations. They will be dispatched into the + * MergedPointsTiles surrounding sourceTileId. Afterward, the sourceTileId is removed from + * the missingTiles of each. MergedPointsTiles with empty referencingTiles (requiring render) + * are yielded. The sourceTileId is also added to the MergedPointsTiles referencingTiles set. + */ + *insert(points: Array, sourceTileId: bigint, mapLayerStyleRuleId: MapLayerStyleRule): Generator { + // Insert the points into the relevant corner tiles. + let level = coreLib.getTileLevel(sourceTileId); + for (let point of points) { + let mergedPointsTile = this.getCornerTileByPosition(point.position, level, mapLayerStyleRuleId); + mergedPointsTile.add(point); + } + + // Remove the sourceTileId from the corner tile IDs. + let cornerTileIds = [ + sourceTileId, + coreLib.getTileNeighbor(sourceTileId, -1, 0), + coreLib.getTileNeighbor(sourceTileId, 0, -1), + coreLib.getTileNeighbor(sourceTileId, -1, -1), + ]; + for (let cornerTileId of cornerTileIds) { + let cornerTile = this.getCornerTileById(cornerTileId, mapLayerStyleRuleId); + cornerTile.addReference(sourceTileId); + yield cornerTile; + } + } + + /** + * Remove a sourceTileId reference from each surrounding corner tile whose mapLayerStyleRuleId has a + * prefix-match with the mapLayerStyleId. Yields MergedPointsTiles which now have empty referencingTiles, + * and whose visualization (if existing) must therefore be removed from the scene. + */ + *remove(sourceTileId: bigint, mapLayerStyleId: string): Generator { + for (let [mapLayerStyleRuleId, tiles] of this.mergedPointsTiles.entries()) { + if (mapLayerStyleRuleId.startsWith(mapLayerStyleId)) { + for (let [tileId, tile] of tiles) { + // Yield the corner tile as to-be-deleted, if it does not have any referencing tiles. + tile.referencingTiles = tile.referencingTiles.filter(val => val != sourceTileId); + if (!tile.referencingTiles.length) { + yield tile; + tiles.delete(tileId); + } + } + } + } + } +} diff --git a/erdblick_app/app/preferences.component.ts b/erdblick_app/app/preferences.component.ts index 4aa8d920..dcbe3bdc 100644 --- a/erdblick_app/app/preferences.component.ts +++ b/erdblick_app/app/preferences.component.ts @@ -9,10 +9,14 @@ import {MAX_NUM_TILES_TO_LOAD, MAX_NUM_TILES_TO_VISUALIZE, ParametersService} fr selector: 'pref-components', template: `
- + + keyboard +
@@ -50,8 +54,60 @@ import {MAX_NUM_TILES_TO_LOAD, MAX_NUM_TILES_TO_VISUALIZE, ParametersService} fr - - + +
+
    +
  • +
    + Ctrl + K +
    +
    Open Search
    +
  • +
  • +
    + Ctrl + J +
    +
    Zoom to Target Feature
    +
  • +
  • + M +
    Open Maps & Styles Panel
    +
  • +
  • + W +
    Move Camera Up
    +
  • +
  • + A +
    Move Camera Left
    +
  • +
  • + S +
    Move Camera Down
    +
  • +
  • + D +
    Move Camera Right
    +
  • +
  • + Q +
    Zoom In
    +
  • +
  • + E +
    Zoom Out
    +
  • +
  • + R +
    Reset Camera Orientation
    +
  • +
+
+ +
`, styles: [` .slider-container { @@ -69,6 +125,71 @@ import {MAX_NUM_TILES_TO_LOAD, MAX_NUM_TILES_TO_VISUALIZE, ParametersService} fr padding: 0.5em; } + .keyboard-dialog { + width: 25em; + text-align: center; + background-color: white; + } + + h2 { + font-size: 1.5em; + color: #333; + margin-bottom: 1em; + font-weight: bold; + } + + .keyboard-list { + list-style-type: none; + padding: 0; + } + + .keyboard-list li { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1em; + } + + .keyboard-list li span { + display: inline-block; + background-color: #eef1f7; + padding: 0.5em 0.75em; + border-radius: 0.5em; + color: #333; + font-weight: bold; + min-width: 4em; + text-align: center; + } + + .control-desc { + color: #666; + font-size: 0.9em; + } + + /* Keyboard key styling */ + .key { + border-radius: 0.5em; + background-color: #ffcc00; + font-size: 1em; + padding: 0.5em 0.75em; + color: #333; + } + + .key-multi { + display: flex; + gap: 0.25em; + } + + .key-multi .key { + background-color: #00bcd4; + padding: 0.3em 0.6em; + } + + .highlight { + background-color: #ff5722; + color: white; + } + @media only screen and (max-width: 56em) { .elevated { bottom: 3.5em; @@ -82,6 +203,8 @@ export class PreferencesComponent { tilesToLoadInput: number = 0; tilesToVisualizeInput: number = 0; + controlsDialogVisible = false; + constructor(private messageService: InfoMessageService, public mapService: MapService, public styleService: StyleService, @@ -112,6 +235,10 @@ export class PreferencesComponent { this.dialogVisible = true; } + showControlsDialog() { + this.controlsDialogVisible = true; + } + openHelp() { window.open("https://developer.nds.live/tools/the-new-mapviewer/user-guide", "_blank"); } diff --git a/erdblick_app/app/search.panel.component.ts b/erdblick_app/app/search.panel.component.ts index 462ae56f..aa47c29e 100644 --- a/erdblick_app/app/search.panel.component.ts +++ b/erdblick_app/app/search.panel.component.ts @@ -1,4 +1,4 @@ -import {Component} from "@angular/core"; +import {AfterViewInit, Component, ElementRef, Renderer2, ViewChild} from "@angular/core"; import {Cartesian3} from "./cesium"; import {InfoMessageService} from "./info.service"; import {SearchTarget, JumpTargetService} from "./jump.service"; @@ -6,29 +6,78 @@ import {MapService} from "./map.service"; import {coreLib} from "./wasm"; import {ParametersService} from "./parameters.service"; import {SidePanelService, SidePanelState} from "./sidepanel.service"; -import {FeatureSearchService} from "./feature.search.service"; -import {FeatureSearchComponent} from "./feature.search.component"; +import {Dialog} from "primeng/dialog"; +import {KeyboardService} from "./keyboard.service"; +import {distinctUntilChanged} from "rxjs"; + +interface ExtendedSearchTarget extends SearchTarget { + index: number; +} @Component({ selector: 'search-panel', template: ` - - - - - -
- -

- {{ item.name }}
-

+
+
+ +
- +
+ +
+
+
+ + + +
+ {{ item.name }} +
+ +
+
+
+
+
+
+ +
+
+
+ {{ item.input }} +
+ +
+ +
+
+
+
+
+ + + +
+ {{ item.name }} +
+ +
+
+
+
+
+
+
+
@@ -45,20 +94,118 @@ import {FeatureSearchComponent} from "./feature.search.component"; } `] }) -export class SearchPanelComponent { +export class SearchPanelComponent implements AfterViewInit { searchItems: Array = []; + activeSearchItems: Array = []; + inactiveSearchItems: Array = []; searchInputValue: string = ""; searchMenuVisible: boolean = false; + searchHistory: Array = []; + visibleSearchHistory: Array = []; mapSelectionVisible: boolean = false; mapSelection: Array = []; - constructor(public mapService: MapService, + @ViewChild('textarea') textarea!: ElementRef; + @ViewChild('actionsdialog') dialog!: Dialog; + cursorPosition: number = 0; + private clickListener: () => void; + + public get staticTargets() { + const targetsArray: Array = []; + const value = this.searchInputValue.trim(); + let label = "tileId = ?"; + if (this.validateMapgetTileId(value)) { + label = `tileId = ${value}`; + } else { + label += `
Insufficient parameters`; + } + targetsArray.push({ + icon: "pi-table", + color: "green", + name: "Mapget Tile ID", + label: label, + enabled: false, + jump: (value: string) => { return this.parseMapgetTileId(value) }, + validate: (value: string) => { return this.validateMapgetTileId(value) } + }); + label = "lon = ? | lat = ? | (level = ?)" + if (this.validateWGS84(value, true)) { + const coords = this.parseWgs84Coordinates(value, true); + if (coords !== undefined) { + label = `lon = ${coords[0]} | lat = ${coords[1]}${coords.length === 3 && coords[3] ? ' | level = ' + coords[2] : ''}`; + } + } else { + label += `
Insufficient parameters`; + } + targetsArray.push({ + icon: "pi-map-marker", + color: "green", + name: "WGS84 Lon-Lat Coordinates", + label: label, + enabled: false, + jump: (value: string) => { return this.parseWgs84Coordinates(value, true) }, + validate: (value: string) => { return this.validateWGS84(value, true) } + }); + label = "lat = ? | lon = ? | (level = ?)" + if (this.validateWGS84(value, false)) { + const coords = this.parseWgs84Coordinates(value, true); + if (coords !== undefined) { + label = `lat = ${coords[0]} | lon = ${coords[1]}${coords.length === 3 && coords[3] ? ' | level = ' + coords[2] : ''}`; + } + } else { + label += `
Insufficient parameters`; + } + targetsArray.push({ + icon: "pi-map-marker", + color: "green", + name: "WGS84 Lat-Lon Coordinates", + label: label, + enabled: false, + jump: (value: string) => { return this.parseWgs84Coordinates(value, false) }, + validate: (value: string) => { return this.validateWGS84(value, false) } + }); + label = "lat = ? | lon = ?" + if (this.validateWGS84(value, false)) { + const coords = this.parseWgs84Coordinates(value, true); + if (coords !== undefined) { + label = `lat = ${coords[0]} | lon = ${coords[1]}`; + } + } else { + label += `
Insufficient parameters`; + } + targetsArray.push({ + icon: "pi-map-marker", + color: "green", + name: "Open WGS84 Lat-Lon in Google Maps", + label: label, + enabled: false, + jump: (value: string) => { return this.openInGM(value) }, + validate: (value: string) => { return this.validateWGS84(value, false) } + }); + targetsArray.push({ + icon: "pi-map-marker", + color: "green", + name: "Open WGS84 Lat-Lon in Open Street Maps", + label: label, + enabled: false, + jump: (value: string) => { return this.openInOSM(value) }, + validate: (value: string) => { return this.validateWGS84(value, false) } + }); + return targetsArray; + } + + constructor(private renderer: Renderer2, + private elRef: ElementRef, + public mapService: MapService, public parametersService: ParametersService, + private keyboardService: KeyboardService, private messageService: InfoMessageService, private jumpToTargetService: JumpTargetService, 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)); this.jumpToTargetService.targetValueSubject.subscribe((event: string) => { this.validateMenuItems(); @@ -71,50 +218,84 @@ export class SearchPanelComponent { this.jumpToTargetService.jumpTargets.subscribe((jumpTargets: Array) => { this.searchItems = [ ...jumpTargets, - ...[ - { - name: "Tile ID", - label: "Jump to Tile by its Mapget ID", - enabled: false, - jump: (value: string) => { return this.parseMapgetTileId(value) }, - validate: (value: string) => { return this.validateMapgetTileId(value) } - }, - { - name: "WGS84 Lat-Lon Coordinates", - label: "Jump to WGS84 Coordinates", - enabled: false, - jump: (value: string) => { return this.parseWgs84Coordinates(value, false) }, - validate: (value: string) => { return this.validateWGS84(value, false) } - }, - { - name: "WGS84 Lon-Lat Coordinates", - label: "Jump to WGS84 Coordinates", - enabled: false, - jump: (value: string) => { return this.parseWgs84Coordinates(value, true) }, - validate: (value: string) => { return this.validateWGS84(value, true) } - }, - { - name: "Open WGS84 Lat-Lon in Google Maps", - label: "Open Location in External Map Service", - enabled: false, - jump: (value: string) => { return this.openInGM(value) }, - validate: (value: string) => { return this.validateWGS84(value, false) } - }, - { - name: "Open WGS84 Lat-Lon in Open Street Maps", - label: "Open Location in External Map Service", - enabled: false, - jump: (value: string) => { return this.openInOSM(value) }, - validate: (value: string) => { return this.validateWGS84(value, false) } - } - ] + ...this.staticTargets ]; }); + // TODO: Get rid of map selection, as soon as we support + // multi-selection from different maps. Then we can + // just search all maps simultaneously. jumpToTargetService.mapSelectionSubject.subscribe(maps => { this.mapSelection = maps; this.mapSelectionVisible = true; }); + + 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]) { + this.parametersService.lastSearchHistoryEntry.next(parameters.search); + } + } + }); + + this.parametersService.lastSearchHistoryEntry.subscribe(entry => { + // TODO: Temporary cosmetic solution. Replace with a SIMFIL fix. + if (entry) { + const query = entry[1] + .replace(/ä/g, "ae") + .replace(/ö/g, "oe") + .replace(/ü/g, "ue") + .replace(/ß/g, "ss") + .replace(/Ä/g, "Ae") + .replace(/Ö/g, "Oe") + .replace(/Ü/g, "Ue"); + this.searchInputValue = query; + this.runTarget(entry[0]); + } + this.reloadSearchHistory(); + }); + + this.reloadSearchHistory(); + } + + ngAfterViewInit() { + this.dialog.onShow.subscribe(() => { + setTimeout(() => { + this.expandTextarea(); + }, 10); + }); + + this.dialog.onHide.subscribe(() => { + setTimeout(() => { + this.shrinkTextarea(); + }, 10); + }); + } + + private reloadSearchHistory() { + const searchHistoryString = localStorage.getItem("searchHistory"); + if (searchHistoryString) { + const searchHistory = JSON.parse(searchHistoryString) as Array<[number, string]>; + this.searchHistory = []; + searchHistory.forEach(value => { + if (0 <= value[0] && value[0] < this.searchItems.length) { + const item = this.searchItems[value[0]]; + this.searchHistory.push({label: item.name, index: value[0], input: value[1]}); + } + }); + this.visibleSearchHistory = this.searchHistory; + } + } + + removeSearchHistoryEntry(index: number) { + this.searchHistory.splice(index, 1); + const searchHistory: [number, string][] = this.searchHistory.map(entry => [entry.index, entry.input]); + localStorage.setItem("searchHistory", JSON.stringify(searchHistory)); + this.reloadSearchHistory(); + if (index == 0) { + this.parametersService.resetSearchHistoryState(); + } } parseMapgetTileId(value: string): number[] | undefined { @@ -262,7 +443,7 @@ export class SearchPanelComponent { } validateMapgetTileId(value: string) { - return value.length > 0 && !/\s/g.test(value.trim()); + return value.length > 0 && !/\s/g.test(value.trim()) && !isNaN(+value.trim()); } validateWGS84(value: string, isLonLat: boolean = false) { @@ -273,11 +454,43 @@ export class SearchPanelComponent { showSearchOverlay(event: Event) { event.stopPropagation(); this.sidePanelService.panel = SidePanelState.SEARCH; + this.setSearchValue(this.searchInputValue); } setSearchValue(value: string) { this.searchInputValue = value; + if (!value) { + this.parametersService.setSearchHistoryState(null); + this.jumpToTargetService.targetValueSubject.next(value); + this.searchItems = [...this.jumpToTargetService.jumpTargets.getValue(), ...this.staticTargets]; + this.activeSearchItems = []; + this.inactiveSearchItems = this.searchItems; + this.visibleSearchHistory = this.searchHistory; + return; + } this.jumpToTargetService.targetValueSubject.next(value); + this.activeSearchItems = []; + this.inactiveSearchItems = []; + for (let i = 0; i < this.searchItems.length; i++) { + if (this.searchItems[i].validate(this.searchInputValue)) { + const target = this.searchItems[i] as ExtendedSearchTarget; + target.index = i; + this.activeSearchItems.push(target); + } else { + this.inactiveSearchItems.push(this.searchItems[i]); + } + } + this.visibleSearchHistory = Object.values( + this.searchHistory.reduce((acc, obj) => { + if (obj.input.includes(value)) { + const key = `${obj.label}-${obj.index}-${obj.input}`; + if (!acc[key]) { + acc[key] = obj; + } + } + return acc; + }, {} as Record) + ); } setSelectedMap(value: string|null) { @@ -285,7 +498,12 @@ export class SearchPanelComponent { this.mapSelectionVisible = false; } - runTarget(item: SearchTarget) { + targetToHistory(index: number) { + this.parametersService.setSearchHistoryState([index, this.searchInputValue]); + } + + runTarget(index: number) { + const item = this.searchItems[index]; if (item.jump !== undefined) { const coord = item.jump(this.searchInputValue); this.jumpToWGS84(coord); @@ -303,9 +521,71 @@ export class SearchPanelComponent { onKeydown(event: KeyboardEvent) { if (event.key === 'Enter') { - this.runTarget(this.searchItems[0]); + if (this.searchInputValue.trim()) { + this.parametersService.setSearchHistoryState([0, this.searchInputValue]); + } else { + this.parametersService.setSearchHistoryState(null); + } + this.textarea.nativeElement.blur(); } else if (event.key === 'Escape') { - this.searchInputValue = ""; + event.stopPropagation(); + if (this.searchInputValue) { + this.setSearchValue(""); + return; + } + this.dialog.close(event); } } + + selectHistoryEntry(index: number) { + const entry = this.searchHistory[index]; + if (entry.index !== undefined && entry.input !== undefined) { + this.parametersService.setSearchHistoryState([entry.index, entry.input]); + } + } + + expandTextarea() { + this.sidePanelService.searchOpen = true; + this.renderer.setAttribute(this.textarea.nativeElement, 'rows', '3'); + this.renderer.removeClass(this.textarea.nativeElement, 'single-line'); + setTimeout(() => { + this.textarea.nativeElement.focus(); + this.textarea.nativeElement.setSelectionRange(this.cursorPosition, this.cursorPosition); + }, 100) + } + + shrinkTextarea() { + this.cursorPosition = this.textarea.nativeElement.selectionStart; + this.renderer.setAttribute(this.textarea.nativeElement, 'rows', '1'); + this.renderer.addClass(this.textarea.nativeElement, 'single-line'); + this.sidePanelService.searchOpen = false; + } + + clickOnSearchToStart() { + // this.textarea.nativeElement.setSelectionRange(this.cursorPosition, this.cursorPosition); + this.textarea.nativeElement.click(); + } + + handleClickOut(event: MouseEvent): void { + const clickedInsideComponent = this.elRef.nativeElement.contains(event.target); + + // Check if the clicked element is a button or a file input + const clickedOnButton = event.target instanceof HTMLElement && event.target.tagName === 'BUTTON'; + const clickedOnUploader = event.target instanceof HTMLElement && + (event.target.tagName === 'INPUT' && event.target.getAttribute('type') === 'file'); + + if (!clickedInsideComponent && !clickedOnButton && !clickedOnUploader) { + this.dialog.close(event); + } + } + + ngOnDestroy(): void { + if (this.clickListener) { + this.clickListener(); + } + } + + onFileSelected($event: any) { + alert($event) + } } diff --git a/erdblick_app/app/sidepanel.service.ts b/erdblick_app/app/sidepanel.service.ts index 4fddca48..c742830e 100644 --- a/erdblick_app/app/sidepanel.service.ts +++ b/erdblick_app/app/sidepanel.service.ts @@ -10,6 +10,9 @@ export enum SidePanelState { @Injectable({providedIn: 'root'}) export class SidePanelService { + + featureSearchOpen: boolean = false; + searchOpen: boolean = false; previousState: string = SidePanelState.NONE; private _activeSidePanel = new BehaviorSubject(SidePanelState.NONE); diff --git a/erdblick_app/app/sourcedata.panel.component.ts b/erdblick_app/app/sourcedata.panel.component.ts index 3214d194..9bac6a13 100644 --- a/erdblick_app/app/sourcedata.panel.component.ts +++ b/erdblick_app/app/sourcedata.panel.component.ts @@ -1,24 +1,36 @@ -import {Component, OnInit, Input, ViewChild} from "@angular/core"; +import { + Component, + OnInit, + Input, + ViewChild, + OnDestroy, + AfterViewInit, + ElementRef, + Renderer2 +} from "@angular/core"; import {TreeTableNode} from "primeng/api"; import {InspectionService, SelectedSourceData} from "./inspection.service"; import {MapService} from "./map.service"; import {coreLib} from "./wasm"; import {SourceDataAddressFormat} from "build/libs/core/erdblick-core"; import {TreeTable} from "primeng/treetable"; -import {Menu} from "primeng/menu"; +import {ParametersService} from "./parameters.service"; +import {Subscription} from "rxjs"; @Component({ selector: 'sourcedata-panel', template: ` -
-
- - -
+
+ + + + -
- - + -
@@ -61,7 +70,7 @@ import {Menu} from "primeng/menu"; - + @@ -70,25 +79,30 @@ import {Menu} from "primeng/menu";
-
- - -
-
- Error
- {{ errorMessage }} -
-
-
- - ` + +
+
+ Error
+ {{ errorMessage }} +
+
+
+
+ `, + styles: [` + @media only screen and (max-width: 56em) { + .resizable-container-expanded { + height: calc(100vh - 3em); + } + } + `] }) -export class SourceDataPanelComponent implements OnInit { - @Input() sourceData!: SelectedSourceData; +export class SourceDataPanelComponent implements OnInit, AfterViewInit, OnDestroy { + @Input() sourceData!: SelectedSourceData; @ViewChild('tt') table!: TreeTable; - @ViewChild('layerMenuItemsMenu') layerListMenu!: Menu; + @ViewChild('resizeableContainer') resizeableContainer!: ElementRef; treeData: TreeTableNode[] = []; filterFields = [ @@ -108,10 +122,12 @@ export class SourceDataPanelComponent implements OnInit { errorMessage = ""; isExpanded = false; - layerMenuItems: any[] = []; + inspectionContainerWidth: number; + inspectionContainerHeight: number; + containerSizeSubscription: Subscription; /** - * Returns a human readable layer name for a layer id. + * Returns a human-readable layer name for a layer id. * * @param layerId Layer id to get the name for */ @@ -122,7 +138,22 @@ export class SourceDataPanelComponent implements OnInit { return layerId; } - constructor(private inspectionService: InspectionService, public mapService: MapService) {} + constructor(private inspectionService: InspectionService, + public parameterService: ParametersService, + private renderer: Renderer2, + public mapService: MapService) { + this.inspectionContainerWidth = this.parameterService.inspectionContainerWidth * this.parameterService.baseFontSize; + this.inspectionContainerHeight = this.parameterService.inspectionContainerHeight * this.parameterService.baseFontSize; + this.containerSizeSubscription = this.parameterService.parameters.subscribe(parameter => { + if (parameter.panel.length == 2) { + this.inspectionContainerWidth = parameter.panel[0] * this.parameterService.baseFontSize; + this.inspectionContainerHeight = (parameter.panel[1] + 3) * this.parameterService.baseFontSize; + } else { + this.inspectionContainerWidth = this.parameterService.inspectionContainerWidth * this.parameterService.baseFontSize; + this.inspectionContainerHeight = (window.innerHeight - (this.parameterService.inspectionContainerHeight + 3) * this.parameterService.baseFontSize) * this.parameterService.baseFontSize; + } + }); + } ngOnInit(): void { this.inspectionService.loadSourceDataLayer(this.sourceData.tileId, this.sourceData.layerId, this.sourceData.mapId) @@ -146,34 +177,10 @@ export class SourceDataPanelComponent implements OnInit { .finally(() => { this.loading = false; }); + } - this.mapService.maps.subscribe(maps => { - const map = maps.get(this.sourceData.mapId); - if (map) { - this.layerMenuItems = [ - { - label: "Switch Layer", - items: Array.from(map.layers.values()) - .filter(item => item.type == "SourceData") - .map(item => { - return { - label: SourceDataPanelComponent.layerNameForLayerId(item.layerId), - disabled: item.layerId === this.sourceData.layerId, - command: () => { - let sourceData = {...this.sourceData}; - sourceData.layerId = item.layerId; - sourceData.address = BigInt(0); - - this.inspectionService.selectedSourceData.next(sourceData); - }, - }; - }), - }, - ]; - } else { - this.layerMenuItems = []; - } - }); + ngAfterViewInit() { + this.detectSafari(); } /** @@ -250,7 +257,7 @@ export class SourceDataPanelComponent implements OnInit { } // Virtual row index (visible row index) of the first highlighted row, or undefined. - let firstHighlightedItemIndex : number | undefined; + let firstHighlightedItemIndex: number | undefined; let select = (node: TreeTableNode, parents: TreeTableNode[], highlight: boolean, virtualRowIndex: number) => { if (!node.data) { @@ -299,4 +306,15 @@ export class SourceDataPanelComponent implements OnInit { this.clearFilter(); } } + + ngOnDestroy() { + this.containerSizeSubscription.unsubscribe(); + } + + detectSafari() { + const isSafari = /Safari/i.test(navigator.userAgent); + if (isSafari) { + this.renderer.addClass(this.resizeableContainer.nativeElement, 'safari'); + } + } } diff --git a/erdblick_app/app/style.service.ts b/erdblick_app/app/style.service.ts index a01e08a5..829ee8f2 100644 --- a/erdblick_app/app/style.service.ts +++ b/erdblick_app/app/style.service.ts @@ -9,7 +9,7 @@ import { catchError, Subject } from "rxjs"; import {FileUpload} from "primeng/fileupload"; -import {FeatureLayerStyle, FeatureStyleOption, FeatureStyleOptionType} from "../../build/libs/core/erdblick-core"; +import {FeatureLayerStyle, FeatureStyleOptionType} from "../../build/libs/core/erdblick-core"; import {coreLib, uint8ArrayToWasm} from "./wasm"; import {ParametersService, StyleParameters} from "./parameters.service"; @@ -22,7 +22,7 @@ export type FeatureStyleOptionWithStringType = { label: string, id: string, type: FeatureStyleOptionType, - defaultValue: string, + defaultValue: any, description: string }; @@ -47,9 +47,7 @@ export class StyleService { private erdblickBuiltinStyles: Array = []; erroredStyleIds: Map = new Map(); - selectedStyleIdForEditing: BehaviorSubject = new BehaviorSubject(""); - styleBeingEdited: boolean = false; - styleEditedStateData: BehaviorSubject = new BehaviorSubject(""); + selectedStyleIdForEditing: string = ""; styleEditedSaveTriggered: Subject = new Subject(); builtinStylesCount = 0; @@ -61,7 +59,7 @@ export class StyleService { constructor(private httpClient: HttpClient, private parameterService: ParametersService) { - this.parameterService.parameters.subscribe(params => { + this.parameterService.parameters.subscribe(_ => { // This subscription exists specifically to catch the values of the query parameters. if (this.parameterService.initialQueryParamsSet) { return; @@ -171,37 +169,48 @@ export class StyleService { } } - exportStyleYamlFile(styleId: string) { - const content = this.styles.get(styleId)!; - if (content === undefined) { + exportStyleYamlFile(styleId: string): boolean { + const content = this.styles.get(styleId); + if (!content || !content.source) { + console.error('No content found or invalid content structure.'); return false; } + try { + // Ensure content.source is a string or convert to string if needed + const blobContent = typeof content.source === 'string' ? content.source : JSON.stringify(content.source); // Create a blob from the content - const blob = new Blob([content.source], { type: 'text/plain;charset=utf-8' }); + const blob = new Blob([blobContent], { type: 'application/x-yaml;charset=utf-8' }); // Create a URL for the blob const url = window.URL.createObjectURL(blob); - // Create a temporary anchor tag to trigger the download + // Check if URL creation was successful + if (!url) { + console.error('Failed to create object URL for the blob.'); + return false; + } + // Create a temporary anchor tag to trigger the download. const a = document.createElement('a'); a.href = url; a.download = `${styleId}.yaml`; - // Append the anchor to the body, trigger the download, and remove the anchor - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - // Revoke the blob URL to free up resources + // Trigger the download. + const event = new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true, + }); + a.dispatchEvent(event); + + // Revoke the blob URL to free up resources. window.URL.revokeObjectURL(url); } catch (e) { - console.error(e); + console.error('Error while exporting YAML file:', e); return false; } - return true; } async importStyleYamlFile(event: any, file: File, styleId: string, fileUploader: FileUpload | undefined): Promise { - // Prevent the default upload behavior - // Dummy XHR, as we handle the file ourselves + // Prevent the default upload behavior Dummy XHR, as we handle the file ourselves event.xhr = new XMLHttpRequest(); const fileReader = new FileReader(); const loadFilePromise = new Promise((resolve, reject) => { @@ -333,8 +342,15 @@ export class StyleService { style.options = []; // Transport FeatureStyleOptions from WASM array to JS. let options = style.featureLayerStyle.options(); - for (let i = 0; i < options.size(); ++i) - style.options.push(options.get(i)! as FeatureStyleOptionWithStringType); + for (let i = 0; i < options.size(); ++i) { + const option = options.get(i)! as FeatureStyleOptionWithStringType; + style.options.push(option); + + // Apply the default value for the option, if no value is stored yet. + if (!style.params.options.hasOwnProperty(option.id)) { + style.params.options[option.id] = option.defaultValue; + } + } options.delete(); return true; } @@ -363,7 +379,6 @@ export class StyleService { if (style.params.visible) { this.styleAddedForId.next(styleId); } - console.log(`${style.params.visible ? 'Activated' : 'Deactivated'} style: ${styleId}.`); } reapplyStyles(styleIds: Array) { @@ -378,11 +393,16 @@ export class StyleService { if (!this.styles.has(styleId)) { return; } - let style = this.styles.get(styleId)!; + const style = this.styles.get(styleId)!; style.params.visible = enabled !== undefined ? enabled : !style.params.visible; if (delayRepaint) { this.reapplyStyle(styleId); } this.parameterService.setStyleConfig(styleId, style.params); } + + toggleOption(styleId: string, optionId: string, enabled: boolean) { + const style = this.styles.get(styleId)!; + style.params.options[optionId] = enabled; + } } diff --git a/erdblick_app/app/treetablefilter-patch.directive.ts b/erdblick_app/app/treetablefilter-patch.directive.ts index 260c5964..48c712e0 100644 --- a/erdblick_app/app/treetablefilter-patch.directive.ts +++ b/erdblick_app/app/treetablefilter-patch.directive.ts @@ -1,7 +1,6 @@ import {AfterContentInit, Directive} from "@angular/core"; -import {TreeTable, TreeTableFilterEvent, TreeTableFilterOptions} from "primeng/treetable"; +import {TreeTable} from "primeng/treetable"; import {TreeTableNode} from "primeng/api"; -import {ObjectUtils} from "primeng/utils"; /** * This is a monkey-patched version of PrimNG's findFilteredNodes with the following changes: diff --git a/erdblick_app/app/view.component.ts b/erdblick_app/app/view.component.ts index d659f5f5..ebace4c4 100644 --- a/erdblick_app/app/view.component.ts +++ b/erdblick_app/app/view.component.ts @@ -1,6 +1,3 @@ -"use strict"; - -import {FeatureWrapper} from "./features.model"; import {TileVisualization} from "./visualization.model" import { Cartesian2, @@ -8,25 +5,29 @@ import { Cartographic, CesiumMath, Color, - ColorGeometryInstanceAttribute, Entity, ImageryLayer, ScreenSpaceEventHandler, ScreenSpaceEventType, UrlTemplateImageryProvider, Viewer, - HeightReference + HeightReference, + Billboard, + defined } from "./cesium"; import {ParametersService} from "./parameters.service"; import {AfterViewInit, Component} from "@angular/core"; import {MapService} from "./map.service"; import {DebugWindow, ErdblickDebugApi} from "./debugapi.component"; import {StyleService} from "./style.service"; -import {FeatureSearchService, MAX_ZOOM_LEVEL} from "./feature.search.service"; +import {FeatureSearchService, MAX_ZOOM_LEVEL, SearchResultPrimitiveId} from "./feature.search.service"; import {CoordinatesService} from "./coordinates.service"; import {JumpTargetService} from "./jump.service"; import {distinctUntilChanged} from "rxjs"; import {SearchResultPosition} from "./featurefilter.worker"; +import {InspectionService} from "./inspection.service"; +import {KeyboardService} from "./keyboard.service"; +import {coreLib} from "./wasm"; // Redeclare window with extended interface declare let window: DebugWindow; @@ -47,12 +48,10 @@ declare let window: DebugWindow; }) export class ErdblickViewComponent implements AfterViewInit { viewer!: Viewer; - private hoveredFeature: any = null; - private hoveredFeatureOrigColor: Color | null = null; private mouseHandler: ScreenSpaceEventHandler | null = null; - private tileVisForPrimitive: Map; private openStreetMapLayer: ImageryLayer | null = null; private marker: Entity | null = null; + /** * Construct a Cesium View with a Model. * @param mapService The map model service providing access to data @@ -67,45 +66,36 @@ export class ErdblickViewComponent implements AfterViewInit { public featureSearchService: FeatureSearchService, public parameterService: ParametersService, public jumpService: JumpTargetService, + public inspectionService: InspectionService, + public keyboardService: KeyboardService, public coordinatesService: CoordinatesService) { - this.tileVisForPrimitive = new Map(); - this.mapService.tileVisualizationTopic.subscribe((tileVis: TileVisualization) => { tileVis.render(this.viewer).then(wasRendered => { if (wasRendered) { - tileVis.forEachPrimitive((primitive: any) => { - this.tileVisForPrimitive.set(primitive, tileVis); - }) this.viewer.scene.requestRender(); } }); }); this.mapService.tileVisualizationDestructionTopic.subscribe((tileVis: TileVisualization) => { - if (this.hoveredFeature && this.tileVisForPrimitive.get(this.hoveredFeature.primitive) === tileVis) { - this.setHoveredCesiumFeature(null); - } - tileVis.forEachPrimitive((primitive: any) => { - this.tileVisForPrimitive.delete(primitive); - }) tileVis.destroy(this.viewer); this.viewer.scene.requestRender(); }); - this.mapService.moveToWgs84PositionTopic.subscribe((pos: {x: number, y: number}) => { - this.parameterService.cameraViewData.next({ - // Convert lon/lat to Cartesian3 using current camera altitude. - destination: Cartesian3.fromDegrees( + this.mapService.moveToWgs84PositionTopic.subscribe((pos: {x: number, y: number, z?: number}) => { + // Convert lon/lat to Cartesian3 using current camera altitude. + this.parameterService.setView( + Cartesian3.fromDegrees( pos.x, pos.y, - Cartographic.fromCartesian(this.viewer.camera.position).height), - orientation: { + pos.z !== undefined? pos.z : Cartographic.fromCartesian(this.viewer.camera.position).height), + { heading: CesiumMath.toRadians(0), // East, in radians. pitch: CesiumMath.toRadians(-90), // Directly looking down. roll: 0 // No rotation. } - }); + ); }); } @@ -136,31 +126,55 @@ export class ErdblickViewComponent implements AfterViewInit { // Add a handler for selection. this.mouseHandler.setInputAction((movement: any) => { const position = movement.position; + let feature = this.viewer.scene.pick(position); + if (defined(feature) && feature.primitive instanceof Billboard && feature.primitive.id.type === "SearchResult") { + if (feature.primitive.id) { + const featureInfo = this.featureSearchService.searchResults[feature.primitive.id.index]; + if (featureInfo.mapId && featureInfo.featureId) { + this.jumpService.highlightByJumpTargetFilter(featureInfo.mapId, featureInfo.featureId).then(() => { + if (this.inspectionService.selectedFeatures) { + this.inspectionService.zoomToFeature(); + } + }); + } + } else { + this.mapService.moveToWgs84PositionTopic.next({ + x: feature.primitive.position.x, + y: feature.primitive.position.y, + z: feature.primitive.position.z + 1000 + }); + } + } + this.mapService.highlightFeatures( + Array.isArray(feature?.id) ? feature.id : [feature?.id], + false, + coreLib.HighlightMode.SELECTION_HIGHLIGHT).then(); + // Handle position update after highlighting, because otherwise + // there is a race condition between the parameter updates for + // feature selection and position update. const coordinates = this.viewer.camera.pickEllipsoid(position, this.viewer.scene.globe.ellipsoid); if (coordinates !== undefined) { this.coordinatesService.mouseClickCoordinates.next(Cartographic.fromCartesian(coordinates)); } - let feature = this.viewer.scene.pick(position); - if (this.isKnownCesiumFeature(feature)) { - this.setPickedCesiumFeature(feature); - } else { - this.setPickedCesiumFeature(null); - } }, ScreenSpaceEventType.LEFT_CLICK); // Add a handler for hover (i.e., MOUSE_MOVE) functionality. this.mouseHandler.setInputAction((movement: any) => { const position = movement.endPosition; // Notice that for MOUSE_MOVE, it's endPosition + // Do not handle mouse move here, if the first element + // under the cursor is not the Cesium view. + if (document.elementFromPoint(position.x, position.y)?.tagName.toLowerCase() !== "canvas") { + return; + } const coordinates = this.viewer.camera.pickEllipsoid(position, this.viewer.scene.globe.ellipsoid); if (coordinates !== undefined) { this.coordinatesService.mouseMoveCoordinates.next(Cartographic.fromCartesian(coordinates)) } let feature = this.viewer.scene.pick(position); - if (this.isKnownCesiumFeature(feature)) { - this.setHoveredCesiumFeature(feature); - } else { - this.setHoveredCesiumFeature(null); - } + this.mapService.highlightFeatures( + Array.isArray(feature?.id) ? feature.id : [feature?.id], + false, + coreLib.HighlightMode.HOVER_HIGHLIGHT).then(); }, ScreenSpaceEventType.MOUSE_MOVE); // Add a handler for camera movement. @@ -174,7 +188,7 @@ export class ErdblickViewComponent implements AfterViewInit { // Remove fullscreen button as unnecessary this.viewer.fullscreenButton.destroy(); - this.parameterService.cameraViewData.subscribe(cameraData => { + this.parameterService.cameraViewData.pipe(distinctUntilChanged()).subscribe(cameraData => { this.viewer.camera.setView({ destination: cameraData.destination, orientation: cameraData.orientation @@ -218,110 +232,40 @@ export class ErdblickViewComponent implements AfterViewInit { this.parameterService.setMarkerPosition(Cartographic.fromDegrees(position[1], position[0])); } }); - } - - /** - * Check if two cesium features are equal. A cesium feature is a - * combination of a feature id and a primitive which contains it. - */ - private cesiumFeaturesAreEqual(f1: any, f2: any) { - return (!f1 && !f2) || (f1 && f2 && f1.id === f2.id && f1.primitive === f1.primitive); - } - - /** Check if the given feature is known and can be selected. */ - isKnownCesiumFeature(f: any) { - return f && f.id !== undefined && f.primitive !== undefined && ( - this.tileVisForPrimitive.has(f.primitive) || - this.tileVisForPrimitive.has(f.primitive._pointPrimitiveCollection)) - } - - /** - * Set or re-set the hovered feature. - */ - private setHoveredCesiumFeature(feature: any) { - if (this.cesiumFeaturesAreEqual(feature, this.hoveredFeature)) { - return; - } - // Restore the previously hovered feature to its original color. - if (this.hoveredFeature && this.hoveredFeatureOrigColor) { - this.setFeatureColor(this.hoveredFeature, this.hoveredFeatureOrigColor); - } - this.hoveredFeature = null; - let resolvedFeature = feature ? this.resolveFeature(feature.primitive, feature.id) : null; - if (resolvedFeature && !resolvedFeature?.equals(this.mapService.selectionTopic.getValue())) { - // Highlight the new hovered feature and remember its original color. - this.hoveredFeatureOrigColor = this.getFeatureColor(feature); - this.setFeatureColor(feature, Color.YELLOW); - this.hoveredFeature = feature; - } - } - - /** - * Set or re-set the picked feature. - */ - private setPickedCesiumFeature(feature: any) { - // Get the actual mapget feature for the picked Cesium feature. - let resolvedFeature = feature ? this.resolveFeature(feature.primitive, feature.id) : null; - if (!resolvedFeature) { - this.mapService.selectionTopic.next(null); - return; - } - - if (resolvedFeature.equals(this.mapService.selectionTopic.getValue())) { - return; - } - // Make sure that if the hovered feature is picked, we don't - // remember the hover color as the original color. - if (this.cesiumFeaturesAreEqual(feature, this.hoveredFeature)) { - this.setHoveredCesiumFeature(null); - } - this.mapService.selectionTopic.next(resolvedFeature); - } - - /** Set the color of a cesium feature through its associated primitive. */ - private setFeatureColor(feature: any, color: Color) { - if (feature.primitive.color !== undefined) { - // Special treatment for point primitives. - feature.primitive.color = color; - this.viewer.scene.requestRender(); - return; - } - if (feature.primitive.isDestroyed()) { - return; - } - const attributes = feature.primitive.getGeometryInstanceAttributes(feature.id); - attributes.color = ColorGeometryInstanceAttribute.toValue(color); - this.viewer.scene.requestRender(); - } + this.inspectionService.originAndNormalForFeatureZoom.subscribe(values => { + const [origin, normal] = values; + const direction = Cartesian3.subtract(normal, new Cartesian3(), new Cartesian3()); + const endPoint = Cartesian3.add(origin, direction, new Cartesian3()); + Cartesian3.normalize(direction, direction); + Cartesian3.negate(direction, direction); + const up = this.viewer.scene.globe.ellipsoid.geodeticSurfaceNormal(endPoint, new Cartesian3()); + const right = Cartesian3.cross(direction, up, new Cartesian3()); + Cartesian3.normalize(right, right); + const cameraUp = Cartesian3.cross(right, direction, new Cartesian3()); + Cartesian3.normalize(cameraUp, cameraUp); + this.viewer.camera.flyTo({ + destination: endPoint, + orientation: { + direction: direction, + up: cameraUp, + } + }); + }); - /** Read the color of a cesium feature through its associated primitive. */ - private getFeatureColor(feature: any): Color | null { - if (feature.primitive.color !== undefined) { - // Special treatment for point primitives. - return feature.primitive.color.clone(); - } - if (feature.primitive.isDestroyed()) { - return null; + this.keyboardService.registerShortcuts(['q', 'Q'], this.zoomIn.bind(this)); + this.keyboardService.registerShortcuts(['e', 'E'], this.zoomOut.bind(this)); + this.keyboardService.registerShortcuts(['w', 'W'], this.moveUp.bind(this)); + this.keyboardService.registerShortcuts(['a', 'A'], this.moveLeft.bind(this)); + this.keyboardService.registerShortcuts(['s', 'S'], this.moveDown.bind(this)); + this.keyboardService.registerShortcuts(['d', 'D'], this.moveRight.bind(this)); + this.keyboardService.registerShortcuts(['r', 'R'], this.resetOrientation.bind(this)); + + // Hide the global loading spinner. + const spinner = document.getElementById('global-spinner-container'); + if (spinner) { + spinner.style.display = 'none'; } - const attributes = feature.primitive.getGeometryInstanceAttributes(feature.id); - if (attributes.color === undefined) { - return null; - } - return Color.fromBytes(...attributes.color); - } - - /** Get a mapget feature from a cesium feature. */ - private resolveFeature(primitive: any, index: number) { - let tileVis = this.tileVisForPrimitive.get(primitive); - if (!tileVis) { - tileVis = this.tileVisForPrimitive.get(primitive._pointPrimitiveCollection); - if (!tileVis) { - console.error("Failed find tileLayer for primitive!"); - return null; - } - } - return new FeatureWrapper(index, tileVis.tile); } /** @@ -358,8 +302,8 @@ export class ErdblickViewComponent implements AfterViewInit { // Handle the antimeridian. // TODO: Must also handle north pole. - if (west > -180 && sizeLon > 180.) { - sizeLon = 360. - sizeLon; + if (west > -180 && sizeLon > 180.0) { + sizeLon = 360.0 - sizeLon; } // Grow the viewport rectangle by 25% @@ -411,12 +355,12 @@ export class ErdblickViewComponent implements AfterViewInit { renderFeatureSearchResultTree(level: number) { this.featureSearchService.visualization.removeAll(); const color = Color.fromCssColorString(this.featureSearchService.pointColor); - let markers: Array = []; + let markers: Array<[SearchResultPrimitiveId, SearchResultPosition]> = []; const nodes = this.featureSearchService.resultTree.getNodesAtLevel(level); for (const node of nodes) { if (node.markers.length) { markers.push(...node.markers); - } else if (node.count > 0) { + } else if (node.count > 0 && node.center) { this.featureSearchService.visualization.add({ position: node.center, image: this.featureSearchService.getPinGraphics(node.count), @@ -428,9 +372,10 @@ export class ErdblickViewComponent implements AfterViewInit { } if (markers.length) { - markers.forEach(position => { + markers.forEach(marker => { this.featureSearchService.visualization.add({ - position: position.cartesian as Cartesian3, + id: marker[0], + position: marker[1].cartesian as Cartesian3, image: this.featureSearchService.markerGraphics(), width: 32, height: 32, @@ -441,4 +386,46 @@ export class ErdblickViewComponent implements AfterViewInit { }); } } + + moveUp() { + this.moveCameraOnSurface(0, this.parameterService.cameraMoveUnits); + } + + moveDown() { + this.moveCameraOnSurface(0, -this.parameterService.cameraMoveUnits); + } + + moveLeft() { + this.moveCameraOnSurface(-this.parameterService.cameraMoveUnits, 0); + } + + moveRight() { + this.moveCameraOnSurface(this.parameterService.cameraMoveUnits, 0); + } + + private moveCameraOnSurface(longitudeOffset: number, latitudeOffset: number) { + // Get the current camera position in Cartographic coordinates (longitude, latitude, height) + const cameraPosition = this.viewer.camera.positionCartographic; + const lon = cameraPosition.longitude + CesiumMath.toRadians(longitudeOffset); + const lat = cameraPosition.latitude + CesiumMath.toRadians(latitudeOffset); + const alt = cameraPosition.height; + const newPosition = Cartesian3.fromRadians(lon, lat, alt); + this.parameterService.setView(newPosition, this.parameterService.getCameraOrientation()); + } + + zoomIn() { + this.viewer.camera.zoomIn(this.parameterService.cameraZoomUnits); + } + + zoomOut() { + this.viewer.camera.zoomOut(this.parameterService.cameraZoomUnits); + } + + resetOrientation() { + this.parameterService.setView(this.parameterService.getCameraPosition(), { + heading: CesiumMath.toRadians(0.0), + pitch: CesiumMath.toRadians(-90.0), + roll: 0.0 + }); + } } diff --git a/erdblick_app/app/visualization.model.ts b/erdblick_app/app/visualization.model.ts index eb08f02b..0db041ee 100644 --- a/erdblick_app/app/visualization.model.ts +++ b/erdblick_app/app/visualization.model.ts @@ -9,7 +9,8 @@ import { CallbackProperty, HeightReference } from "./cesium"; -import {FeatureLayerStyle, TileFeatureLayer} from "../../build/libs/core/erdblick-core"; +import {FeatureLayerStyle, TileFeatureLayer, HighlightMode} from "../../build/libs/core/erdblick-core"; +import {MergedPointVisualization, PointMergeService} from "./pointmerge.service"; export interface LocateResolution { tileId: string, @@ -89,19 +90,23 @@ export class TileVisualization { showTileBorder: boolean = false; private readonly style: StyleWithIsDeleted; + private readonly styleName: string; private lowDetailVisu: TileBoxVisualization|null = null; private primitiveCollection: PrimitiveCollection|null = null; private hasHighDetailVisualization: boolean = false; private hasTileBorder: boolean = false; private renderingInProgress: boolean = false; - private readonly highlight: string; + private readonly highlightMode: HighlightMode; + private readonly featureIdSubset: string[]; private deleted: boolean = false; private readonly auxTileFun: (key: string)=>FeatureTile|null; - private readonly options: Record; + private readonly options: Record; + private readonly pointMergeService: PointMergeService; /** * Create a tile visualization. - * @param tile {FeatureTile} The tile to visualize. + * @param tile The tile to visualize. + * @param pointMergeService Instance of the central PointMergeService, used to visualize merged point features. * @param auxTileFun Callback which may be called to resolve external references * for relation visualization. * @param style The style to use for visualization. @@ -109,27 +114,41 @@ export class TileVisualization { * a low-detail representation is indicated by `false`, and * will result in a dot representation. A high-detail representation * based on the style can be triggered using `true`. - * @param highlight Controls whether the visualization will run rules that - * have `mode: highlight` set, otherwise, only rules with the default - * `mode: normal` are executed. + * @param highlightMode Controls whether the visualization will run rules that + * have a specific highlight mode. + * @param featureIdSubset Subset of feature IDs for visualization. If not set, + * all features in the tile will be visualized. * @param boxGrid Sets a flag to wrap this tile visualization into a bounding box * @param options Option values for option variables defined by the style sheet. */ - constructor(tile: FeatureTile, auxTileFun: (key: string)=>FeatureTile|null, style: FeatureLayerStyle, highDetail: boolean, highlight: string = "", boxGrid?: boolean, options?: Record) { + constructor( + tile: FeatureTile, + pointMergeService: PointMergeService, + auxTileFun: (key: string) => FeatureTile | null, + style: FeatureLayerStyle, + highDetail: boolean, + highlightMode: HighlightMode = coreLib.HighlightMode.NO_HIGHLIGHT, + featureIdSubset?: string[], + boxGrid?: boolean, + options?: Record) + { this.tile = tile; this.style = style as StyleWithIsDeleted; + this.styleName = this.style.name(); this.isHighDetail = highDetail; this.renderingInProgress = false; - this.highlight = highlight; + this.highlightMode = highlightMode; + this.featureIdSubset = featureIdSubset || []; this.deleted = false; this.auxTileFun = auxTileFun; this.showTileBorder = boxGrid === undefined ? false : boxGrid; this.options = options || {}; + this.pointMergeService = pointMergeService; } /** * Actually create the visualization. - * @param viewer {Cesium.Viewer} The viewer to add the rendered entity to. + * @param viewer {Viewer} The viewer to add the rendered entity to. * @return True if anything was rendered, false otherwise. */ async render(viewer: Viewer) { @@ -140,8 +159,8 @@ export class TileVisualization { this.destroy(viewer); this.deleted = false; - // Do not try to render if the underlying data is disposed. - if (this.tile.disposed || this.style.isDeleted()) { + // Do not continue if the style was deleted while we were waiting. + if (this.style.isDeleted()) { return false; } @@ -150,13 +169,16 @@ export class TileVisualization { let returnValue = true; if (this.isHighDetailAndNotEmpty()) { returnValue = await this.tile.peekAsync(async (tileFeatureLayer: TileFeatureLayer) => { - let visualization = new coreLib.FeatureLayerVisualization( + let wasmVisualization = new coreLib.FeatureLayerVisualization( + this.tile.mapTileKey, this.style, this.options, - this.highlight!); - visualization.addTileFeatureLayer(tileFeatureLayer); + this.pointMergeService, + this.highlightMode, + this.featureIdSubset); + wasmVisualization.addTileFeatureLayer(tileFeatureLayer); try { - visualization.run(); + wasmVisualization.run(); } catch (e) { console.error(`Exception while rendering: ${e}`); @@ -164,7 +186,7 @@ export class TileVisualization { } // Try to resolve externally referenced auxiliary tiles. - let extRefs = {requests: visualization.externalReferences()}; + let extRefs = {requests: wasmVisualization.externalReferences()}; if (extRefs.requests && extRefs.requests.length > 0) { let response = await fetch("/locate", { body: JSON.stringify(extRefs, (_, value) => @@ -178,9 +200,8 @@ export class TileVisualization { } let extRefsResolved = await response.json() as LocateResponse; - if (this.tile.disposed || this.style.isDeleted()) { - // Do not continue if any of the tiles or the style - // were deleted while we were waiting. + if (this.style.isDeleted()) { + // Do not continue if the style was deleted while we were waiting. return false; } @@ -203,18 +224,27 @@ export class TileVisualization { // add them to the visualization, and let it process them. await FeatureTile.peekMany(auxTiles, async (tileFeatureLayers: Array) => { for (let auxTile of tileFeatureLayers) - visualization.addTileFeatureLayer(auxTile); + wasmVisualization.addTileFeatureLayer(auxTile); try { - visualization.processResolvedExternalReferences(extRefsResolved.responses); + wasmVisualization.processResolvedExternalReferences(extRefsResolved.responses); } catch (e) { console.error(`Exception while rendering: ${e}`); } }); } - this.primitiveCollection = visualization.primitiveCollection(); - visualization.delete(); + + + if (!this.deleted) { + this.primitiveCollection = wasmVisualization.primitiveCollection(); + for (const [mapLayerStyleRuleId, mergedPointVisualizations] of Object.entries(wasmVisualization.mergedPointFeatures())) { + for (let finishedCornerTile of this.pointMergeService.insert(mergedPointVisualizations as MergedPointVisualization[], this.tile.tileId, mapLayerStyleRuleId)) { + finishedCornerTile.render(viewer); + } + } + } + wasmVisualization.delete(); return true; }); if (this.primitiveCollection) { @@ -237,7 +267,7 @@ export class TileVisualization { /** * Destroy any current visualization. - * @param viewer {Cesium.Viewer} The viewer to remove the rendered entity from. + * @param viewer {Viewer} The viewer to remove the rendered entity from. */ destroy(viewer: Viewer) { this.deleted = true; @@ -245,6 +275,14 @@ export class TileVisualization { return; } + // Remove point-merge contributions that were made by this map-layer+style visualization combo. + let removedCornerTiles = this.pointMergeService.remove( + this.tile.tileId, + this.mapLayerStyleId()); + for (let removedCornerTile of removedCornerTiles) { + removedCornerTile.remove(viewer); + } + if (this.primitiveCollection) { viewer.scene.primitives.remove(this.primitiveCollection); if (!this.primitiveCollection.isDestroyed()) @@ -259,17 +297,6 @@ export class TileVisualization { this.hasTileBorder = false; } - /** - * Iterate over all Cesium primitives of this visualization. - */ - forEachPrimitive(callback: any) { - if (this.primitiveCollection) { - for (let i = 0; i < this.primitiveCollection.length; ++i) { - callback(this.primitiveCollection.get(i)); - } - } - } - /** * Check if the visualization is high-detail, and the * underlying data is not empty. @@ -289,4 +316,13 @@ export class TileVisualization { (this.showTileBorder != this.hasTileBorder) ); } + + /** + * Combination of map name, layer name, style name and highlight mode which + * (in combination with the tile id) uniquely identifies that rendered contents + * if this TileVisualization as expected by the surrounding MergedPointsTiles. + */ + private mapLayerStyleId() { + return `${this.tile.mapName}:${this.tile.layerName}:${this.styleName}:${this.highlightMode.value}`; + } } diff --git a/erdblick_app/app/wasm.ts b/erdblick_app/app/wasm.ts index b9ff412f..470673ca 100644 --- a/erdblick_app/app/wasm.ts +++ b/erdblick_app/app/wasm.ts @@ -1,6 +1,6 @@ import MainModuleFactory, {MainModule as ErdblickCore, SharedUint8Array} from '../../build/libs/core/erdblick-core'; -interface ErdblickCore_ extends ErdblickCore { +export interface ErdblickCore_ extends ErdblickCore { HEAPU8: Uint8Array } @@ -67,3 +67,10 @@ export async function uint8ArrayToWasmAsync(fun: (d: SharedUint8Array)=>any, inp sharedGlbArray.delete(); return (result === false) ? null : result; } + +/** Memory usage log. */ +export function logFreeMemory() { + let avail = coreLib!.getFreeMemory()/1024/1024; + let total = coreLib!.getTotalMemory()/1024/1024; + console.log(`Free memory: ${Math.round(avail*1000)/1000} MiB (${avail/total}%)`) +} diff --git a/erdblick_app/index.html b/erdblick_app/index.html index d3dbd4a6..ff1f7a02 100644 --- a/erdblick_app/index.html +++ b/erdblick_app/index.html @@ -6,12 +6,44 @@ + + + - +
+
+
- diff --git a/erdblick_app/styles.scss b/erdblick_app/styles.scss index eb0d3b6a..58b36d44 100644 --- a/erdblick_app/styles.scss +++ b/erdblick_app/styles.scss @@ -1,882 +1,1260 @@ @import "primeicons/primeicons.css"; + @layer primeng, erdblick; +:root { + font-family: Inter, sans-serif; + font-feature-settings: 'liga' 1, 'calt' 1; /* fix for Chrome */ +} + +@supports (font-variation-settings: normal) { + :root { + font-family: InterVariable, sans-serif; + } +} + +.source-layer-dropdown { + .p-dropdown-label { + padding: 0.4em; + min-width: 8em; + } +} + body { - overflow: hidden; - height: 100vh !important; - width: 100vw !important; + overflow: hidden; + height: 100vh !important; + width: 100vw !important; - .p-multiselect-header { - padding: 0.5em 0.75em; + .p-multiselect-header { + padding: 0.5em 0.75em; - .p-inputtext { - padding: 0.5em; - } + .p-inputtext { + padding: 0.5em; } + } - .p-multiselectitem { - font-size: 0.9em; + .p-multiselectitem { + font-size: 0.9em; - li { - padding: 0.5em 0.75em; - } + li { + padding: 0.5em 0.75em; } + } } .mapviewer-renderlayer { - width: 100%; - height: 100%; - position: fixed; - left: 0; - top: 0; - overflow: hidden; - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select:none; - -o-user-select:none; + width: 100%; + height: 100%; + position: fixed; + left: 0; + top: 0; + overflow: hidden; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + -o-user-select: none; } #mapViewContainer .cesium-widget-credits { - display:none !important; + display: none !important; } /* Shared styles for both panels */ #info { - position: absolute; - right: 0; - bottom: 0; - justify-content: center; - display: flex; - font-size: smaller; - background-color: rgba(#fff, 0.25); - color: black; - padding-right: 0.5em; - padding-left: 0.5em; - line-height: 1.5em; - padding-bottom: 0.25em; - border-radius: 5px 0 0 0; + position: absolute; + right: 0; + bottom: 0; + justify-content: center; + display: flex; + font-size: 0.8em; + background-color: rgba(#fff, 0.25); + color: black; + padding-right: 0.5em; + padding-left: 0.5em; + line-height: 1.5em; + padding-bottom: 0.25em; + border-radius: 5px 0 0 0; } .help-button { - width: 3em; - height: 3em; - margin-left: 0.5em; - margin-bottom: 0.5em; - - .p-button { - border-radius: 50%; - padding: 0.8em 0; - } + width: 3em; + height: 3em; + margin-left: 0.5em; + margin-bottom: 0.5em; + + .p-button { + border-radius: 50%; + padding: 0.8em 0; + } } .pref-button { + width: 3em; + height: 3em; + margin-left: 0.5em; + margin-bottom: 0.5em; + + .p-button { + border-radius: 50%; + padding: 0.8em 0; width: 3em; height: 3em; - margin-left: 0.5em; - margin-bottom: 0.5em; + } +} - .p-button { - border-radius: 50%; - padding: 0.8em 0; - } +.controls-button { + .p-button { + background: none; + border: none; + } +} + +.pref-button { + width: 3em; + height: 3em; + margin-left: 0.5em; + margin-bottom: 0.5em; + + .p-button { + border-radius: 50%; + padding: 0.8em 0; + } } .layers-button { - position: absolute; - top: 0.5em; - left: 0.5em; + position: absolute; + top: 0.5em; + left: 0.5em; - button { - width: 3em; - height: 3em; - box-shadow: none; - } + button { + width: 3em; + height: 3em; + box-shadow: none; + } } .card { - background-color: rgba(#fff, 1.0); - border-radius: 10px; - margin-bottom: 1em; + background-color: rgba(#fff, 1.0); + border-radius: 10px; + margin-bottom: 1em; - .p-tree { - border: none; - } + .p-tree { + border: none; + } } .p-tooltip { - max-width: none; + max-width: none; } .clear-icon { - right: 0.75rem; + right: 0.75rem; +} + +.inspect-panel-small-header { + .p-accordion-header .p-accordion-header-link { + padding-top: 0.5em !important; + padding-bottom: 0.5em !important; + } + + .resizable-container { + height: calc(100vh - 7.5em); + max-height: calc(100vh - 7.5em) !important; + } } .inspect-panel { - position: absolute; - top: 0.5em; - right: 1em; - min-width: 30em; - z-index: 110; + position: absolute; + top: 0.5em; + right: 1em; + min-width: 30em; + z-index: 110; + + .p-accordion-header .p-accordion-header-link { + padding: 0.95em; + } + + .p-accordion-content { + padding: 0; + } - .p-accordion-header .p-accordion-header-link { - padding: 0.8em; + .filter-container { + width: 100%; + display: flex; + gap: 4px; + justify-content: center; + + .p-button { + width: 32px; + height: 32px; } + } - .p-accordion-content { - padding: 0; + .filter-input { + height: 2em; + width: 100%; + padding-left: 2.5em; + padding-right: 2.5em; + border-radius: 6px; + border: solid 1px lightgray; + direction: ltr; + } + + .resizable-container { + width: 40em; + height: calc(100vh - 10.5em); + min-width: 30em; + min-height: 18em; + resize: both; + overflow: auto; + direction: rtl; + max-width: calc(100vw - 28em); + max-height: calc(100vh - 10.5em); + + * { + direction: ltr; } - .filter-container { - width: 100%; - display: flex; - gap: 4px; - justify-content: center; + .source-data-ref-container { + button { + width: 1.1em; + height: 1em; + padding: 0.1em; + margin-bottom: 0.2em; + font-family: monospace; + font-size: 1em; + } + + span { + font-family: monospace; + } + } - .p-button { - width: 32px; - height: 32px; - } + .p-tree { + border: none; + padding: 0; + direction: ltr; } - .filter-input { - height: 2em; - width: 100%; - padding-left: 2.5em; - padding-right: 2.5em; - border-radius: 6px; - border: solid 1px lightgray; - direction: ltr; + .p-treetable-header { + padding: 0.5em; + direction: ltr; } - .resizable-container { - width: 30em; - height: 30em; - min-width: 30em; - min-height: 18em; - resize: both; - overflow: auto; - direction: rtl; - max-width: calc(100vw - 27em); - max-height: calc(100vh - 8.5em); - - * { - direction: ltr; - } + table { + direction: ltr; + border-collapse: collapse; + font-size: 0.9em !important; - .resize-handle { - display: none; - } + td { + padding: 0 0 0 0.5em; + height: 26px; + border-right: 1px solid #e5e7eb; + } - .p-tree { - border: none; - padding: 0; - direction: ltr; - } + th { + padding: 0 0 0 0.5em; + height: 26px; + border-right: 1px solid lightgrey; + } - .p-treetable-header { - padding: 0.5em; - direction: ltr; - } + .p-treetable-toggler { + margin: 0; + width: 1.25em; + height: 1.25em; + } - table { - direction: ltr; - font-size: smaller; - border-collapse: collapse; - - td { - padding: 0px; - height: 26px; - } - - th { - padding: 0px; - height: 26px; - } - - .p-treetable-toggler { - margin: 0; - width: 1.25em; - height: 1.25em; - } - - /* Style used for highlighting individual table rows. */ - tr.highlight { - background: hsl(105, 30%, 90%); - } + /* Style used for highlighting individual table rows. */ + tr.highlight { + color: black; + background: var(--green-100); + + td { + border-color: darkgrey; } + } } + } } -.search-input { - width: 21.5em; - height: 3em; - position: absolute; - top: 0.5em; - left: 4em; +.side-menu-dialog { + .search-menu { + &:first-child { + .p-divider { + display: none; + } + } - input { - width: 100%; - height: 100%; - padding-left: 2em; - border-radius: 6px; - border: solid 1px lightgray; - font-size: medium; + .p-divider { + margin: 0.9em 0; + } + + .search-option { + text-align: left; + font-size: 0.9em; + margin: 0; + background: none; + border: none; + cursor: pointer; + + .search-option-name { + font-weight: bold; + } + + .search-option-warning { + color: burlywood; + } } + } } -.search-menu-dialog { - .search-menu { - &:first-child { - .p-divider { - display: none; - } - } +.search-wrapper { + position: absolute; + top: 0.5em; + left: 4em; + min-width: 22em; + min-height: 3.5em; + max-width: calc(100vw - 5em); + max-height: calc(100vh - 5em); + z-index: 150; + + .resizable-container { + min-width: 22em; + min-height: 0; + resize: horizontal; + overflow: auto; + max-width: calc(100vw - 5em); + max-height: calc(100vh - 10.5em); + } + + .resizable-container.multiline { + max-height: calc(100vh - 10.8em) !important; + } + + .search-input { + width: 100%; + position: relative; + line-height: 0; + + textarea { + width: 100% !important; + transition: all 0.3s; + resize: none; + animation-duration: 0.1s !important; + transition-duration: 0.1s !important; + } - .p-divider { - margin: 0.9em 0; - } + textarea.single-line { + min-height: 3em; + width: 22em !important; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } - .search-option { - text-align: left; - font-size: 0.9em; - margin: 0; - background: none; - border: none; - cursor: pointer; - - .search-option-name { - font-weight: bold; - } - - .search-option-warning { - color: burlywood; - } - } + .p-dialog-mask { + position: relative; + } + + .p-dialog { + width: 100%; + margin: 0; + overflow: auto; + max-height: calc(100vh - 10.5em) + } + + .p-dialog-content { + padding: 0; + } + + .p-dialog-header { + display: none; + } + + .search-menu { + &:first-child { + .p-divider { + display: none; + } } -} -.map-layer-dialog, .search-menu-dialog { - .p-dialog { - width: 25em; - min-width: 25em; - margin: 0; - position: absolute; - top: 4em; - left: 0.5em; - overflow: auto; - max-height: calc(100vh - 9em); + .p-divider { + margin: 0.9em 0; } - .p-dialog-content { - padding: 0.5em; + .search-option-wrapper { + padding: 0.5em; + display: flex; + gap: 0.5em; + flex-direction: row; + + &:hover { + background-color: var(--blue-100); + } + + &:focus-visible { + background-color: var(--blue-100); + outline: none; + } } - .p-dialog-header { - padding: 0.5em; + .search-option-container { + display: flex; + flex-direction: row; + justify-content: space-between; + width: calc(100% - 2em); + + button { + border: none; + background: none; + color: gray; + width: 2em; + height: 2em; - .p-dialog-title { - font-size: medium; + &:hover { + background-color: var(--red-500); + color: white; } + } + } + + .search-option { + text-align: left; + font-size: 0.9em; + margin: 0; + background: none; + border: none; + cursor: pointer; + + .search-option-name { + font-weight: bold; + } + + .search-option-warning { + color: burlywood; + } + } + + .icon-circle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2em; + height: 2em; + border-radius: 50%; + } + + .icon-circle i { + font-size: 1em; + color: white; + } + + .blue { + background-color: var(--primary-color); + } + + .orange { + background-color: darkorange; + } + + .green { + background-color: limegreen; + } + + .grey { + background-color: darkgrey; + } + + .violet { + background-color: blueviolet; + } + } +} + +.map-layer-dialog, .side-menu-dialog { + .p-dialog { + width: 25.5em; + min-width: 25.5em; + margin: 0; + position: absolute; + top: 4em; + left: 0.5em; + overflow: auto; + max-height: calc(100vh - 9em); + } + + .p-dialog-content { + padding: 0.5em; + } + + .p-dialog-header { + padding: 0.5em; + + .p-dialog-title { + font-size: medium; } + } } .map-layer-dialog { - .p-dialog-header { - display: none; + .p-dialog-header { + display: none; + } + + .p-dialog-content { + border-top-right-radius: 6px; + border-top-left-radius: 6px; + + *:focus-visible { + outline: 2px solid var(--blue-500); + } + } + + .p-checkbox-box { + width: 1.5em; + height: 1.5em; + + .p-checkbox-icon { + transition-duration: 0.1s; + font-size: 1em; } - .p-dialog-content { - border-top-right-radius: 6px; - border-top-left-radius: 6px; + .p-icon { + width: 1em; + height: 1em; } + } } .map-tab { - .osm-controls { - display: flex; - align-items: center; - gap: 1em; - margin-top: 0.25em; + .osm-controls { + display: flex; + align-items: center; + gap: 1em; + margin-top: 0.25em; - button { - width: 2em; - height: 2em; - box-shadow: none; - } + button { + width: 2em; + height: 2em; + box-shadow: none; + } - .slider-input { - width: 14em; - font-size: 0.9em; - padding: 0.5em 0.75em; - } + .slider-input { + width: 11em; + font-size: 0.9em; + padding: 0.5em 0.75em; } - .grid-controls { - display: flex; - align-items: flex-start; - gap: 1em; - margin-top: 0.25em; - flex-direction: column; - justify-content: flex-start; - - & > div { - display: flex; - align-items: center; - gap: 1em; - margin-top: 0.25em; - flex-direction: row; - } + .p-divider-vertical { + margin: 0; + padding: 0.5em 0; + } + } - button { - width: 2em; - height: 2em; - box-shadow: none; - } + .grid-controls { + display: flex; + align-items: flex-start; + gap: 1em; + margin-top: 0.25em; + flex-direction: column; + justify-content: flex-start; + + & > div { + display: flex; + align-items: center; + gap: 1em; + margin-top: 0.25em; + flex-direction: row; + } - .slider-input { - width: 14em; - } + button { + width: 2em; + height: 2em; + box-shadow: none; + } - .grid-level { - display: flex; - gap: 0.5em; - justify-content: flex-end; - - button { - width: 2em; - height: 2em; - box-shadow: none; - } - - input { - margin: 0; - } - - .p-inputnumber-buttons-horizontal .p-inputnumber-input { - order: 2; - border-radius: 0; - width: 3em; - text-align: center; - height: 2em; - } - } + .slider-input { + width: 11em; } - .p-fieldset { - margin-bottom: 0.5em; + .grid-level { + display: flex; + gap: 0.5em; + justify-content: flex-end; - .p-fieldset-legend { - padding: 0.5em; - border: 1px solid #dee2e6; - color: #343a40; - background: #f8f9fa; - font-weight: 700; - border-radius: 6px; - font-size: 0.9em; - } + button { + width: 2em; + height: 2em; + box-shadow: none; + } - .p-fieldset-content { - padding: 0; - font-size: 0.9em - } + input { + margin: 0; + } + + .p-inputnumber-buttons-horizontal .p-inputnumber-input { + order: 2; + border-radius: 0; + width: 3em; + text-align: center; + height: 2em; + } + } + } + + .p-fieldset { + margin-bottom: 0.5em; + + .p-fieldset-legend { + padding: 0.5em; + border: 1px solid #dee2e6; + color: #343a40; + background: #f8f9fa; + font-weight: 700; + border-radius: 6px; + font-size: 0.9em; } - .p-divider.p-divider-horizontal { - margin: 0.5em 0; + .p-fieldset-content { + padding: 0; + font-size: 0.9em } + } - .maps-container { - margin-top: 0.25em; - padding-right: 0.75em; - margin-right: -0.75em; + .p-divider.p-divider-horizontal { + margin: 0.5em 0; + } - .map-container { - margin-top: 0.5em; + .maps-container { + margin-top: 0.25em; + padding-right: 0.75em; + margin-right: -0.75em; - .map-header { - font-size: 0.9em; - } + .map-container { + margin-top: 0.5em; - &:first-child { - margin-top: 0; - } - } + .map-header { + font-size: 0.9em; + } + + &:first-child { + margin-top: 0; + } } + } - .styles-container { - margin-top: 0.25em; - padding-right: 0.75em; - margin-right: -0.75em; + .styles-container { + margin-top: 0.25em; + padding-right: 0.75em; + margin-right: -0.75em; - .rotated-icon { - transform: rotate(-90deg); - transition: transform 0.3s ease-in-out; - } + .rotated-icon { + transform: rotate(-90deg); + transition: transform 0.3s ease-in-out; } + } - .flex-container { - margin-top: 0.25em; - padding: 0; - display: flex; - justify-content: space-between; - align-items: center; + .flex-container { + margin-top: 0.25em; + padding: 0; + display: flex; + justify-content: space-between; + align-items: center; - span { - font-size: 0.9em; - } + span { + font-size: 0.9em; + } - &:hover { - .layer-controls { - visibility: visible; - opacity: 1; - transition: opacity 0.25s, visibility 0.25s; - } - - .level-indicator { - display: none; - } - } + &:hover { + .layer-controls { + visibility: visible; + opacity: 1; + transition: opacity 0.25s, visibility 0.25s; + } - .level-indicator { - display: block; - margin: 0; - order: 2; - width: 2.5em; - text-align: center; - height: 2em; - padding: 0; - } + .level-indicator { + display: none; + } + } - .layer-controls { - display: flex; - gap: 0.5em; - width: 8em; - justify-content: flex-end; - visibility: hidden; - opacity: 0; - transition: opacity 0s, visibility 0s; - - button { - width: 2em; - height: 2em; - box-shadow: none; - } - - .p-fileupload { - span { - box-shadow: none; - min-width: 6.4em; - } - } - - input { - margin: 0; - } - - .p-inputnumber-buttons-horizontal .p-inputnumber-input { - order: 2; - border-radius: 0; - width: 2.5em; - text-align: center; - height: 2em; - padding: 0; - } - } + .level-indicator { + display: block; + margin: 0; + order: 2; + width: 2.5em; + text-align: center; + height: 2em; + padding: 0; + } - .style-controls { - justify-content: flex-end; - } + .layer-controls { + display: flex; + gap: 0.5em; + width: 8em; + justify-content: flex-end; + visibility: hidden; + opacity: 0; + transition: opacity 0s, visibility 0s; + + button { + width: 2em; + height: 2em; + box-shadow: none; + } + + input { + margin: 0; + } + + .p-inputnumber-buttons-horizontal .p-inputnumber-input { + order: 2; + border-radius: 0; + width: 2.5em; + text-align: center; + height: 2em; + padding: 0; + } } - .styles-import { - justify-content: center; - align-items: center; - display: flex; + .style-controls { + justify-content: flex-end; + } + } - .p-fileupload { - span { - box-shadow: none; - min-width: 6.4em; - font-size: 0.9em; - } - } + .styles-import { + justify-content: center; + align-items: center; + display: flex; + padding-top: 0.5em; + + .p-fileupload { + .p-fileupload-choose { + padding: 0.5em 1em 0.5em 1em; + } + + .p-button-label { + box-shadow: none; + min-width: 6.4em; + font-size: 0.8em; + } } + } } .map-selection-dialog { - .p-button { - width: 100%; - margin: 0.5em 0.5em 0.5em 0; - } + .p-button { + width: 100%; + margin: 0.5em 0.5em 0.5em 0; + } } .pref-dialog { - margin: 0; + margin: 0; - .p-dialog-content { - .p-button { - margin: 0.5em 0.5em 0.5em 0; - width: 8em; - } + .p-dialog-content { + .p-button { + margin: 0.5em 0.5em 0.5em 0; + width: 8em; } + } - .button-container { - width: 30em; - display: flex; - justify-content: space-between; - align-items: center; - } + .button-container { + width: 30em; + display: flex; + justify-content: space-between; + align-items: center; + } } .bttn-container { - position: absolute; - left: 0; - bottom: 0; - padding-bottom: 0.5em; - display: flex; - flex-direction: row; - z-index: 110; + position: absolute; + left: 0; + bottom: 0; + padding-bottom: 0.5em; + display: flex; + flex-direction: row; + z-index: 110; } .editor-dialog { - .p-dialog { - .p-dialog-content { - padding: 0 1.5rem 1rem 1.5rem; - max-height: calc(75vh + 3em); - } - } + .spinner { + display: flex; + justify-content: center; + width: 100%; + height: calc(100% - 4em); + align-items: center; + } - .editor-container { - min-width: 30em; - width: 100%; - height: calc(100% - 4em); - max-height: calc(75vh - 2em); - margin: 0; - overflow: auto; - border: 1px solid silver; - display: flex; - flex-direction: row; + .p-dialog { + .p-dialog-content { + padding: 0 1.5rem 1rem 1.5rem; + max-height: calc(75vh + 3em); } + } - .cm-editor { - width: 0; - flex-grow: 1; - } + .editor-container { + min-width: 46em; + width: 100%; + height: calc(100% - 4em); + max-height: calc(75vh - 2em); + margin: 0; + overflow: auto; + border: 1px solid silver; + display: flex; + flex-direction: row; + } + + .cm-editor { + width: 0; + flex-grow: 1; + } } .filter-panel { - .p-overlaypanel-content { - padding: 0.75em; - } + .p-overlaypanel-content { + padding: 0.75em; + } } -.feature-search-controls { - button { - height: 2em; - width: 2em; - box-shadow: none; - } - - padding: 0 0 0.5em 0; - display: flex; - gap: 0.5em; - align-items: center; +.z-index-low { + position: absolute; + top: 4em; + left: 0.5em; + z-index: 149 !important; +} - .progress-bar-container { - width: 100%; +.feature-search-controls { + button { + height: 2em; + width: 2em; + box-shadow: none; + } + + padding: 0 0 0.5em 0; + display: flex; + gap: 0.5em; + align-items: center; + + .progress-bar-container { + width: 100%; - .p-progressbar { - height: 2em; - width: 100%; - font-size: 0.9em; + .p-progressbar { + height: 2em; + width: 100%; + font-size: 0.9em; - .p-progressbar-value-animate { - transition: none !important; - } - } + .p-progressbar-value-animate { + transition: none !important; + } } + } } .p-colorpicker { - input { - height: 1em; - width: 1em; - margin: 0; - } + input { + height: 1em; + width: 1em; + margin: 0; + } } .trace-entries { + font-size: 0.9em; + + .p-accordion-header .p-accordion-header-link { + padding: 0.8em; font-size: 0.9em; + } - .p-accordion-header .p-accordion-header-link { - padding: 0.8em; - font-size: 0.9em; + .p-accordion-content { + font-size: 0.9em; + } +} + +.coordinates-container { + z-index: 100; + position: absolute; + bottom: 1em; + left: 0; + margin: 0 auto; + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + .coordinates-panel { + .coordinates-entries { + display: flex; + flex-direction: row; + padding-bottom: 0; + gap: 0.5em; + overflow-y: auto; + max-width: calc(100vw - 25em); + align-items: center; + + & > .coordinates-entry:nth-child(1) { + margin-left: 0.5em; + } + + & > .coordinates-entry:last-child { + margin-right: 0.5em; + } } - .p-accordion-content { - font-size: 0.9em; + .coordinates-button { + button { + padding-left: 0; + padding-right: 0; + width: 2em; + height: 2em; + box-shadow: none; + background: none; + border: none; + color: var(--text-color); + } + } + + .p-card { + background: #ffffffdd; + } + + .p-card:hover { + background: #ffffffff; + } + + .p-card-body { + padding: 0; + } + + .p-card-content { + padding: 0; + display: flex; + flex-direction: row; + + p-multiselect { + margin-bottom: -0.2em; + } + + .p-multiselect { + box-shadow: none; + border: none; + } + + .p-multiselect-label { + display: none; + } + + .p-multiselect-trigger { + width: 2.25em; + height: 2em; + } } + + .coordinates-entry { + display: flex; + align-items: center; + flex-direction: row; + flex-wrap: nowrap; + gap: 0.5em; + } + } } -.coordinates-container { - z-index: 100; - position: absolute; - bottom: 1em; - left: 0; - margin: 0 auto; - width: 100%; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; +.results-listbox { + li { + padding: 0.5em 0.75em; + font-size: 0.9em; + } - .coordinates-panel { - .coordinates-entries { - display: flex; - flex-direction: row; - padding-bottom: 0; - gap: 0.5em; - overflow-y: auto; - max-width: calc(100vw - 25em); - align-items: center; - - & > .coordinates-entry:nth-child(1) { - margin-left: 0.5em; - } - - & > .coordinates-entry:last-child { - margin-right: 0.5em; - } - } + .p-scroller { + height: calc(100vh - 23em) !important; + } +} - .coordinates-button { - button { - padding-left: 0; - padding-right: 0; - width: 2em; - height: 2em; - box-shadow: none; - background: none; - border: none; - color: var(--text-color); - } - } +.ds-config-dialog { + .p-dialog { + width: 50vw; + min-width: 25em; + margin: 0; + } - .p-card { - background: #ffffffdd; - } + .p-dialog-content { + padding: 0 1em 1em 1em; + } - .p-card:hover { - background: #ffffffff; - } + .p-dialog-header { + padding: 1em 1em 0 1em; - .p-card-body { - padding: 0; - } + .p-dialog-title { + font-size: large; + } + } +} + +.ds-fieldset { + .p-fieldset { + margin-bottom: 0.5em; - .p-card-content { - padding: 0; - display: flex; - flex-direction: row; + .p-fieldset-legend { + padding: 0.5em; + border: 1px solid #dee2e6; + color: #343a40; + background: #f8f9fa; + font-weight: 700; + border-radius: 6px; + font-size: 1em; + } - p-multiselect { - margin-bottom: -0.2em; - } + .p-fieldset-content { + padding: 0; + font-size: 0.9em; - .p-multiselect { - box-shadow: none; - border: none; - } + formly-field { + width: 100%; + } - .p-multiselect-label { - display: none; - } + .p-field { + display: flex; + gap: 1em; + align-items: center; + margin-bottom: 0.25em; - .p-multiselect-trigger { - width: 2.25em; - height: 2em; - } + label { + width: 10em; } - .coordinates-entry { - display: flex; - align-items: center; - flex-direction: row; - flex-wrap: nowrap; - gap: 0.5em; + formly-field-primeng-input { + width: 100%; } - } -} -.results-listbox { - li { - padding: 0.5em 0.75em; - font-size: 0.9em; + formly-field-primeng-select { + width: 100%; + } + + input { + width: 100%; + } + } } + } } @media only screen and (max-width: 56em) { - .help-button { - width: 3em; - height: 3em; + .help-button { + width: 3em; + height: 3em; - .p-button { - border-radius: 50%; - padding: 0.8em 0; - } + .p-button { + border-radius: 50%; + padding: 0.8em 0; } + } - .pref-button { - width: 3em; - height: 3em; + .pref-button { + width: 3em; + height: 3em; - .p-button { - border-radius: 50%; - padding: 0.8em 0; - } + .p-button { + border-radius: 50%; + padding: 0.8em 0; } + } + + .bttn-container { + flex-direction: column-reverse; + } + + .layers-button { + position: absolute; + top: 0; + left: 0; + width: 3em; - .bttn-container { - flex-direction: column-reverse; + .p-button { + border-radius: 0; + height: 3em; } + } - .layers-button { - position: absolute; - top: 0; - left: 0; - width: 3em; + .search-wrapper { + position: absolute; + top: 0; + left: 3em; + min-width: 100%; + min-height: 3.5em; + max-width: 100%; + max-height: calc(100vh - 5em); + z-index: 150; - .p-button { - border-radius: 0; - height: 3em; - } + .resizable-container { + min-width: 100%; + min-height: 0; + overflow: auto; + max-width: 100%; + max-height: calc(100vh - 11.5em); + border-radius: 0; + margin-left: -3em; + resize: none; + } + + .p-dialog { + max-height: calc(100vh - 11.5em); + border-radius: 0; + + .p-dialog-content:last-of-type { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } + + .p-dialog-content:first-of-type { + border-top-right-radius: 0; + border-top-left-radius: 0; + } } .search-input { - width: calc(100vw - 3em); - height: 3em; - position: absolute; - top: 0; - left: 3em; + width: calc(100vw - 3em); + height: 3em; + position: relative; + top: 0; + left: 0; - input { - width: 100%; - height: 100%; - padding-left: 2em; - border-radius: 0; - border: none; - } - } + textarea { + width: 100%; + height: 100%; + border-radius: 0; + border: none; + } - .pref-dialog { - .p-dialog { - width: 100vw !important; - height: 100vh !important; - max-width: 100vw !important; - max-height: 100vh !important; - margin: 0; - } + textarea.single-line { + width: 100% !important; + height: 100%; + padding-left: 2em; + border-radius: 0; + border: none; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } + } - .p-button { - margin: 0.5em 0.5em 0.5em 0; - } + .pref-dialog { + .p-dialog { + width: 100vw !important; + height: 100vh !important; + max-width: 100vw !important; + max-height: 100vh !important; + margin: 0; } - .map-layer-dialog, .search-menu-dialog { - .p-dialog { - width: 100vw !important; - height: calc(100vh - 3em) !important; - max-width: 100vw !important; - max-height: calc(100vh - 3em) !important; - margin: 0; - top: 3em; - left: 0; - } + .p-button { + margin: 0.5em 0.5em 0.5em 0; + } + } - .p-dialog-header { - padding: 1.5em 1.5em 0 1.5em; - } + .map-layer-dialog, .side-menu-dialog { + .p-dialog { + width: 100vw !important; + height: calc(100vh - 3em) !important; + max-width: 100vw !important; + max-height: calc(100vh - 3em) !important; + margin: 0; + top: 3em; + left: 0; + border-radius: 0; + } - .p-dialog-content { - padding: 0.5em; - border-radius: 0; - } + .p-dialog-header { + padding: 1.5em 1.5em 0 1.5em; + } - .tabs-container { - overflow: auto; - max-height: 100vh; - } + .p-dialog-content { + padding: 0.5em; + border-radius: 0; } - .p-overlaypanel:after, .p-overlaypanel:before{ - left: unset !important; - right: 1.25rem !important; + .tabs-container { + overflow: auto; + max-height: 100vh; } + } + + .p-overlaypanel:after, .p-overlaypanel:before { + left: unset !important; + right: 1.25rem !important; + } + + .inspect-panel { + position: absolute; + top: inherit; + left: 0; + bottom: 0; + right: inherit; + min-width: 100vw; + width: 100vw; + transform: rotate(180deg); + z-index: 120; + + .resizable-container { + min-width: 100vw; + max-width: 100vw; + max-height: calc(100vh - 3em); + margin: 0; + resize: none; + overflow: auto; + direction: ltr; + - .inspect-panel { - position: absolute; - top: inherit; - left: 0; - bottom: 0; - right: inherit; - min-width: 100vw; + .resize-handle { + display: block; width: 100vw; - transform: rotate(180deg); - z-index: 120; - - .resizable-container { - min-width: 100vw; - max-width: 100vw; - max-height: calc(100vh - 3em); - margin: 0; - resize: none; - overflow: auto; - direction: ltr; - - - .resize-handle { - display: block; - width: 100vw; - height: 1.5em; - background-color: var(--surface-b); - text-align: center; - - i { - font-size: 1em; - font-weight: bolder; - color: darkgrey; - margin-top: 0.25em; - } - } + height: 1.5em; + background-color: var(--surface-b); + text-align: center; + + i { + font-size: 1em; + font-weight: bolder; + color: darkgrey; + margin-top: 0.25em; } + } + } - .p-accordion .p-accordion-header, - .p-accordion .p-accordion-content { - transform: rotate(180deg); - border-radius: 0; - } + .p-accordion .p-accordion-header, + .p-accordion .p-accordion-content { + transform: rotate(180deg); + border-radius: 0; + } - .p-accordion-header { - .p-accordion-toggle-icon { - transform: scaleY(-1); - } + .p-accordion-header { + .p-accordion-toggle-icon { + transform: scaleY(-1); + } - .p-accordion-header-link { - border-radius: 0; - } - } + .p-accordion-header-link { + border-radius: 0; + } } + } - .editor-dialog { - .p-dialog { - height: 100vh; - width: 100vw; - } + .editor-dialog { + .p-dialog { + height: 100vh; + width: 100vw; + } - .editor-container { - width: 100%; - max-height: calc(100% - 3em); - } + .editor-container { + width: 100%; + max-height: calc(100% - 3em); } + } +} + +.safari { + padding-bottom: 0.5em; } diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..fcfb1240 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,69 @@ +import typescriptEslint from "@typescript-eslint/eslint-plugin"; +import globals from "globals"; +import tsParser from "@typescript-eslint/parser"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +export default [ + ...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"), + { + plugins: { + "@typescript-eslint": typescriptEslint, + }, + + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + Atomics: "readonly", + SharedArrayBuffer: "readonly", + }, + + parser: tsParser, + ecmaVersion: 2020, + sourceType: "module", + }, + + rules: { + "no-restricted-imports": ["error", { + paths: [{ + name: "cesium", + message: "Please import Cesium modules only in erdblick_app/app/cesium.ts.", + }], + + patterns: [{ + group: ["cesium/*"], + message: "Please import Cesium modules only in erdblick_app/app/cesium.ts.", + }], + }], + + "prefer-const": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/ban-types": "off", + "no-extra-semi": "off", + "no-prototype-builtins": "off", + "@typescript-eslint/no-non-null-asserted-optional-chain": "off", + "no-constant-condition": "off", + "no-useless-escape": "off", + "@typescript-eslint/no-loss-of-precision": "off", + }, + }, + { + files: ["erdblick_app/app/cesium.ts"], + + rules: { + "no-restricted-imports": "off", + }, + }, +]; \ No newline at end of file diff --git a/libs/core/CMakeLists.txt b/libs/core/CMakeLists.txt index 5c435a7c..0943d8d1 100644 --- a/libs/core/CMakeLists.txt +++ b/libs/core/CMakeLists.txt @@ -13,7 +13,7 @@ set(ERDBLICK_SOURCE_FILES include/erdblick/geometry.h include/erdblick/inspection.h include/erdblick/search.h - include/erdblick/sourcedata.hpp + include/erdblick/layer.h include/erdblick/cesium-interface/object.h include/erdblick/cesium-interface/primitive.h @@ -32,7 +32,7 @@ set(ERDBLICK_SOURCE_FILES src/geometry.cpp src/inspection.cpp src/search.cpp - src/sourcedata.cpp + src/layer.cpp src/cesium-interface/object.cpp src/cesium-interface/primitive.cpp @@ -45,8 +45,10 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten") list(APPEND ERDBLICK_SOURCE_FILES src/bindings.cpp) add_executable(erdblick-core ${ERDBLICK_SOURCE_FILES}) target_compile_definitions(erdblick-core PUBLIC EMSCRIPTEN) - # target_compile_options(erdblick-core PRIVATE -fwasm-exceptions) - # target_link_options(erdblick-core PRIVATE -fwasm-exceptions) + # For Address Sanitization, uncomment the next line and + # add sanitize-address to the LINK_FLAGS. + # target_compile_options(erdblick-core PRIVATE -fsanitize=address) + # -fsanitize=address \ set_target_properties(erdblick-core PROPERTIES LINK_FLAGS "\ --bind \ --profiling \ diff --git a/libs/core/include/erdblick/cesium-interface/labels.h b/libs/core/include/erdblick/cesium-interface/labels.h index add46baa..6d216768 100644 --- a/libs/core/include/erdblick/cesium-interface/labels.h +++ b/libs/core/include/erdblick/cesium-interface/labels.h @@ -7,9 +7,19 @@ namespace erdblick { -struct CesiumPrimitiveLabelsCollection { +struct CesiumLabelCollection +{ + CesiumLabelCollection(); - CesiumPrimitiveLabelsCollection(); + /** + * Get the parameter object for a call to LabelCollection.add(). + */ + JsValue labelParams( + JsValue const &position, + const std::string& labelText, + FeatureStyleRule const &style, + JsValue const& id, + BoundEvalFun const& evalFun); /** * Add an individual label to the collection @@ -18,7 +28,7 @@ struct CesiumPrimitiveLabelsCollection { JsValue const &position, const std::string& labelText, FeatureStyleRule const &style, - uint32_t id, + JsValue const& id, BoundEvalFun const& evalFun); /** diff --git a/libs/core/include/erdblick/cesium-interface/object.h b/libs/core/include/erdblick/cesium-interface/object.h index 75c28cdf..aa314d8c 100644 --- a/libs/core/include/erdblick/cesium-interface/object.h +++ b/libs/core/include/erdblick/cesium-interface/object.h @@ -67,6 +67,11 @@ struct JsValue */ static JsValue Float64Array(std::vector const& coordinates); + /** + * Construct an undefined value. + */ + static JsValue Undefined(); + /** Construct a JsValue from a variant with specific alternatives. */ template static JsValue fromVariant(T const& variant) { diff --git a/libs/core/include/erdblick/cesium-interface/points.h b/libs/core/include/erdblick/cesium-interface/points.h index 07b387c2..0173e451 100644 --- a/libs/core/include/erdblick/cesium-interface/points.h +++ b/libs/core/include/erdblick/cesium-interface/points.h @@ -13,12 +13,21 @@ struct CesiumPointPrimitiveCollection CesiumPointPrimitiveCollection(); /** - * Add an individual point to the collection + * Add an individual point to the collection. */ void addPoint( const JsValue& position, FeatureStyleRule const& style, - uint32_t id, + JsValue const& id, + BoundEvalFun const& evalFun); + + /** + * Get the parameters for a PointPrimitiveCollection::add() call. + */ + JsValue pointParams( + const JsValue& position, + FeatureStyleRule const& style, + JsValue const& id, BoundEvalFun const& evalFun); /** diff --git a/libs/core/include/erdblick/cesium-interface/primitive.h b/libs/core/include/erdblick/cesium-interface/primitive.h index fc3fafde..7e69b465 100644 --- a/libs/core/include/erdblick/cesium-interface/primitive.h +++ b/libs/core/include/erdblick/cesium-interface/primitive.h @@ -64,7 +64,7 @@ struct CesiumPrimitive void addPolyLine( JsValue const& vertices, FeatureStyleRule const& style, - uint32_t id, + JsValue const& id, BoundEvalFun const& evalFun); /** @@ -77,7 +77,7 @@ struct CesiumPrimitive void addPolygon( JsValue const& vertices, FeatureStyleRule const& style, - uint32_t id, + JsValue const& id, BoundEvalFun const& evalFun); /** @@ -91,7 +91,7 @@ struct CesiumPrimitive void addTriangles( JsValue const& float64Array, FeatureStyleRule const& style, - uint32_t id, + JsValue const& id, BoundEvalFun const& evalFun); /** @@ -111,7 +111,7 @@ struct CesiumPrimitive */ void addGeometryInstance( const FeatureStyleRule& style, - uint32_t id, + JsValue const& id, const JsValue& geom, BoundEvalFun const& evalFun); diff --git a/libs/core/include/erdblick/geometry.h b/libs/core/include/erdblick/geometry.h index 27a5841b..f8949552 100644 --- a/libs/core/include/erdblick/geometry.h +++ b/libs/core/include/erdblick/geometry.h @@ -3,7 +3,7 @@ #include -using namespace mapget; +namespace m = mapget; namespace erdblick { @@ -12,25 +12,37 @@ namespace erdblick * Function to calculate the "side" (or relative position) * of a point to a line defined by a start point and a direction vector. */ -double pointSideOfLine(Point const& lineVector, Point const& lineStart, Point const& p); +double pointSideOfLine(m::Point const& lineVector, m::Point const& lineStart, m::Point const& p); /** * Function to check if a triangle intersects with an infinite 2D line, * using start point and direction vector for the line */ -bool checkIfTriangleIntersectsWithInfinite2dLine(Point const& lineStart, Point const& lineVector, Point const& triA, Point const& triB, Point const& triC); +bool checkIfTriangleIntersectsWithInfinite2dLine(m::Point const& lineStart, m::Point const& lineVector, m::Point const& triA, m::Point const& triB, m::Point const& triC); /** * Returns true if the given point is inside the given 2d triangle. */ -bool isPointInsideTriangle(Point const& p, Point const& p0, Point const& p1, Point const& p2); +bool isPointInsideTriangle(m::Point const& p, m::Point const& p0, m::Point const& p1, m::Point const& p2); /** * Calculate a reasonable center point for the given geometry. * This is used as a location for labels, and as the origin * for relation vectors. */ -Point geometryCenter(model_ptr const& g); +m::Point geometryCenter(m::model_ptr const& g); + +/** + * Calculate a point furthest from the center for the given geometry. + * Used to properly scale the camera in the viewer + * relative to the feature's bounding sphere. + */ +m::Point boundingRadiusEndPoint(m::model_ptr const& g); + +/** + * Get type of the geometry. + */ +m::GeomType getGeometryType(m::model_ptr const& g); /** * Calculate a local WGS84 coordinate system for the geometry. @@ -38,6 +50,6 @@ Point geometryCenter(model_ptr const& g); * in real-world length. The y-axis will point in the direction * (first-point -> last-point). The x-axis is perpendicular. */ -glm::dmat3x3 localWgs84UnitCoordinateSystem(const model_ptr& g); +glm::dmat3x3 localWgs84UnitCoordinateSystem(const m::model_ptr& g); } // namespace erdblick diff --git a/libs/core/include/erdblick/inspection.h b/libs/core/include/erdblick/inspection.h index 8e89b968..7d9b8b08 100644 --- a/libs/core/include/erdblick/inspection.h +++ b/libs/core/include/erdblick/inspection.h @@ -7,6 +7,7 @@ #include "sfl/small_vector.hpp" #include #include +#include namespace erdblick { @@ -28,10 +29,11 @@ class InspectionConverter { JsValue key_; JsValue value_; + std::optional mapId_; ValueType type_ = ValueType::Null; - std::string hoverId_; + std::string hoverId_; // For highlight attribs/relations on hovering. std::string info_; - std::vector children_; + std::deque children_; JsValue direction_; std::string geoJsonPath_; diff --git a/libs/core/include/erdblick/layer.h b/libs/core/include/erdblick/layer.h new file mode 100644 index 00000000..7e1c0a5d --- /dev/null +++ b/libs/core/include/erdblick/layer.h @@ -0,0 +1,110 @@ +#pragma once + +#include "mapget/model/featurelayer.h" +#include "mapget/model/sourcedatalayer.h" +#include "cesium-interface/object.h" +#include "mapget/model/sourcedata.h" + +namespace erdblick +{ + +/** Wrapper class around the mapget `TileFeatureLayer` smart pointer. */ +struct TileFeatureLayer +{ + /** + * Constructor accepting a shared pointer to the original `TileFeatureLayer` class. + * @param self Shared pointer to `mapget::TileFeatureLayer`. + */ + TileFeatureLayer(std::shared_ptr self); + + /** + * Retrieves the ID of the tile feature layer as a string. + * @return The ID string. + */ + std::string id(); + + /** + * Retrieves the tile ID as a 64-bit unsigned integer. + * @return The tile ID. + */ + uint64_t tileId() const; + + /** + * Gets the number of features in the tile. + * @return The number of features. + */ + uint32_t numFeatures() const; + + /** + * Retrieves the center point of the tile, including the zoom level as the Z coordinate. + * @return The center point of the tile. + */ + mapget::Point center() const; + + /** + * Finds a feature within the tile by its ID. + * @param id The ID of the feature to find. + * @return A pointer to the found feature, or `nullptr` if not found. + */ + mapget::model_ptr find(const std::string& id) const; + + /** + * Finds the index of a feature based on its type and ID parts. + * @param type The type of the feature. + * @param idParts The parts of the feature's ID. + * @return The index of the feature, or `-1` if not found. + */ + int32_t findFeatureIndex(std::string type, NativeJsValue idParts) const; + + ~TileFeatureLayer(); + + /** Shared pointer to the underlying `mapget::TileFeatureLayer`. */ + mapget::TileFeatureLayer::Ptr model_; +}; + +/** Wrapper class around the mapget `TileSourceDataLayer` smart pointer. */ +struct TileSourceDataLayer +{ + /** + * Constructor accepting a shared pointer to the original `TileSourceDataLayer` class. + * @param self Shared pointer to `mapget::TileSourceDataLayer`. + */ + TileSourceDataLayer(std::shared_ptr self); + + /** + * Retrieves the source data address format of the layer. + * @return The address format. + */ + mapget::TileSourceDataLayer::SourceDataAddressFormat addressFormat() const; + + /** + * Converts the layer's data to a JSON string with indentation. + * @return The JSON representation of the layer. + */ + std::string toJson() const; + + /** Obtain the error string of the layer, if there is one. */ + std::string getError() const; + + /** + * Converts the `SourceDataLayer` hierarchy to a tree model compatible structure. + * + * **Layout:** + * ```json + * [ + * { + * "data": {"key": "...", "value": ...}, + * "children": [{ ... }] + * }, + * ... + * ] + * ``` + * @return A `NativeJsValue` representing the hierarchical data structure. + */ + NativeJsValue toObject() const; + + /** Shared pointer to the underlying `mapget::TileSourceDataLayer`. */ + std::shared_ptr model_; +}; + +} // namespace erdblick diff --git a/libs/core/include/erdblick/parser.h b/libs/core/include/erdblick/parser.h index 9e014538..dcbeb1fb 100644 --- a/libs/core/include/erdblick/parser.h +++ b/libs/core/include/erdblick/parser.h @@ -1,10 +1,12 @@ #pragma once #include "mapget/model/stream.h" +#include "mapget/model/featurelayer.h" #include "buffer.h" #include "cesium-interface/object.h" #include "mapget/model/featurelayer.h" #include "mapget/model/sourcedatalayer.h" +#include "layer.h" namespace erdblick { @@ -30,12 +32,12 @@ class TileLayerParser /** * Parse a TileFeatureLayer from a buffer as returned by writeTileFeatureLayer. */ - mapget::TileFeatureLayer::Ptr readTileFeatureLayer(SharedUint8Array const& buffer); + TileFeatureLayer readTileFeatureLayer(SharedUint8Array const& buffer); /** * Parse a TileSourceDataLayer from a buffer. */ - mapget::TileSourceDataLayer::Ptr readTileSourceDataLayer(SharedUint8Array const& buffer); + TileSourceDataLayer readTileSourceDataLayer(SharedUint8Array const& buffer); /** * Parse only the stringified MapTileKey and tile id from the tile layer blob. diff --git a/libs/core/include/erdblick/rule.h b/libs/core/include/erdblick/rule.h index a02fea36..e3338d95 100644 --- a/libs/core/include/erdblick/rule.h +++ b/libs/core/include/erdblick/rule.h @@ -2,6 +2,7 @@ #include "mapget/model/feature.h" #include "simfil/model/nodes.h" +#include "simfil/overlay.h" #include "yaml-cpp/yaml.h" #include "color.h" @@ -12,14 +13,18 @@ namespace erdblick { /** - * Simfil expression evaluation lambda, bound to a particular model node. + * Simfil expression evaluation lambda, bound to a particular context model node. */ -using BoundEvalFun = std::function; +struct BoundEvalFun +{ + simfil::OverlayNode context_; + std::function eval_; +}; class FeatureStyleRule { public: - explicit FeatureStyleRule(YAML::Node const& yaml); + explicit FeatureStyleRule(YAML::Node const& yaml, uint32_t index=0); FeatureStyleRule(FeatureStyleRule const& other, bool resetNonInheritableAttrs=false); enum Aspect { @@ -28,9 +33,10 @@ class FeatureStyleRule Attribute }; - enum Mode { - Normal, - Highlight + enum HighlightMode { + NoHighlight, + HoverHighlight, + SelectionHighlight }; enum Arrow { @@ -40,9 +46,9 @@ class FeatureStyleRule DoubleArrow }; - FeatureStyleRule const* match(mapget::Feature& feature) const; + FeatureStyleRule const* match(mapget::Feature& feature, BoundEvalFun const& evalFun) const; [[nodiscard]] Aspect aspect() const; - [[nodiscard]] Mode mode() const; + [[nodiscard]] HighlightMode mode() const; [[nodiscard]] bool selectable() const; [[nodiscard]] bool supports(mapget::GeomType const& g) const; @@ -58,6 +64,7 @@ class FeatureStyleRule [[nodiscard]] float outlineWidth() const; [[nodiscard]] std::optional> const& nearFarScale() const; [[nodiscard]] glm::dvec3 const& offset() const; + [[nodiscard]] std::optional const& pointMergeGridCellSize() const; [[nodiscard]] std::optional const& relationType() const; [[nodiscard]] float relationLineHeightOffset() const; @@ -92,6 +99,8 @@ class FeatureStyleRule [[nodiscard]] std::optional> const& scaleByDistance() const; [[nodiscard]] std::optional> const& offsetScaleByDistance() const; + [[nodiscard]] uint32_t const& index() const; + private: void parse(YAML::Node const& yaml); @@ -100,7 +109,7 @@ class FeatureStyleRule } Aspect aspect_ = Feature; - Mode mode_ = Normal; + HighlightMode mode_ = NoHighlight; bool selectable_ = true; uint32_t geometryTypes_ = 0; // bitfield from GeomType enum std::optional type_; @@ -119,6 +128,7 @@ class FeatureStyleRule float outlineWidth_ = .0; std::optional> nearFarScale_; glm::dvec3 offset_{.0, .0, .0}; + std::optional pointMergeGridCellSize_; // Labels' rules std::string labelFont_ = "24px Helvetica"; @@ -154,6 +164,9 @@ class FeatureStyleRule std::optional attributeValidityGeometry_; std::vector firstOfRules_; + + // Index of the rule within the style sheet + uint32_t index_ = 0; }; } diff --git a/libs/core/include/erdblick/search.h b/libs/core/include/erdblick/search.h index 181b29db..a7686a7f 100644 --- a/libs/core/include/erdblick/search.h +++ b/libs/core/include/erdblick/search.h @@ -1,7 +1,7 @@ #pragma once -#include "mapget/model/featurelayer.h" #include "cesium-interface/object.h" +#include "layer.h" namespace erdblick { @@ -16,7 +16,7 @@ std::string anyWrap(std::string_view const& q); class FeatureLayerSearch { public: - explicit FeatureLayerSearch(mapget::TileFeatureLayer& tfl); + explicit FeatureLayerSearch(TileFeatureLayer& tfl); /** Returns a list of Tuples of (Map Tile Key, Feature ID). */ NativeJsValue filter(std::string const& q); @@ -25,7 +25,7 @@ class FeatureLayerSearch NativeJsValue traceResults(); private: - mapget::TileFeatureLayer& tfl_; + TileFeatureLayer& tfl_; }; } diff --git a/libs/core/include/erdblick/sourcedata.hpp b/libs/core/include/erdblick/sourcedata.hpp deleted file mode 100644 index a955fddb..00000000 --- a/libs/core/include/erdblick/sourcedata.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#include - -#include "mapget/model/sourcedatalayer.h" -#include "cesium-interface/object.h" - -namespace erdblick -{ - -/** - * Convert a SourceDataLayar hierarchy to a tree model compatible - * structure. - * - * Layout: - * [{ data: [{key: "...", value: ...}, ...], children: [{ ... }] }, ...] - * - **/ -erdblick::JsValue tileSourceDataLayerToObject(const mapget::TileSourceDataLayer& layer); - -} diff --git a/libs/core/include/erdblick/style.h b/libs/core/include/erdblick/style.h index c1555ec4..cbab1da3 100644 --- a/libs/core/include/erdblick/style.h +++ b/libs/core/include/erdblick/style.h @@ -43,11 +43,13 @@ class FeatureLayerStyle [[nodiscard]] bool isValid() const; [[nodiscard]] const std::vector& rules() const; [[nodiscard]] const std::vector& options() const; + [[nodiscard]] std::string const& name() const; private: std::vector rules_; std::vector options_; bool valid_ = false; + std::string name_; }; } \ No newline at end of file diff --git a/libs/core/include/erdblick/visualization.h b/libs/core/include/erdblick/visualization.h index 5cf01d9c..cfb1f408 100644 --- a/libs/core/include/erdblick/visualization.h +++ b/libs/core/include/erdblick/visualization.h @@ -2,12 +2,14 @@ #include #include +#include #include "cesium-interface/point-conversion.h" #include "cesium-interface/points.h" #include "cesium-interface/primitive.h" #include "cesium-interface/labels.h" #include "style.h" #include "simfil/overlay.h" +#include "layer.h" namespace erdblick { @@ -18,7 +20,7 @@ class FeatureLayerVisualization; * Feature ID which is used when the rendered representation is not * supposed to be selectable. */ -static constexpr uint32_t UnselectableId = 0xffffffff; +static std::string UnselectableId; /** * Covers the state for the visualization of a single Relation-Style+Feature @@ -79,15 +81,23 @@ class FeatureLayerVisualization * Convert a TileFeatureLayer into Cesium primitives based on the provided style. */ FeatureLayerVisualization( + std::string const& mapTileKey, const FeatureLayerStyle& style, - NativeJsValue const& optionValues, - std::string highlightFeatureIndex = ""); + NativeJsValue const& rawOptionValues, + NativeJsValue const& rawFeatureMergeService, + FeatureStyleRule::HighlightMode const& highlightMode = FeatureStyleRule::NoHighlight, + NativeJsValue const& rawFeatureIdSubset = {}); + + /** + * Destructor for memory diagnostics. + */ + ~FeatureLayerVisualization(); /** * Add a tile which is considered for visualization. All tiles added after * the first one are only considered to resolve external relations. */ - void addTileFeatureLayer(std::shared_ptr tile); + void addTileFeatureLayer(TileFeatureLayer const& tile); /** * Run visualization for the added tile feature layers. @@ -124,14 +134,21 @@ class FeatureLayerVisualization */ [[nodiscard]] NativeJsValue primitiveCollection() const; + /** + * Returns all merged point features as a dict form mapLayerStyleRuleId + * to MergedPointVisualization primitives. + */ + [[nodiscard]] NativeJsValue mergedPointFeatures() const; + private: /** * Add all geometry of some feature which is compatible with the given rule. */ void addFeature( mapget::model_ptr& feature, - uint32_t id, - FeatureStyleRule const& rule); + BoundEvalFun& evalFun, + FeatureStyleRule const& rule, + std::string const& mapLayerStyleRuleId); /** * Visualize an attribute. @@ -140,8 +157,9 @@ class FeatureLayerVisualization mapget::model_ptr const& feature, std::string_view const& layer, mapget::model_ptr const& attr, - uint32_t id, + std::string_view const& id, const FeatureStyleRule& rule, + std::string const& mapLayerStyleRuleId, uint32_t& offsetFactor, glm::dvec3 const& offset); @@ -151,9 +169,10 @@ class FeatureLayerVisualization */ void addGeometry( mapget::model_ptr const& geom, - uint32_t id, + std::string_view id, FeatureStyleRule const& rule, - BoundEvalFun const& evalFun, + std::string const& mapLayerStyleRuleId, + BoundEvalFun& evalFun, glm::dvec3 const& offset = {.0, .0, .0}); /** @@ -164,9 +183,9 @@ class FeatureLayerVisualization void addLine( mapget::Point const& wgsA, mapget::Point const& wgsB, - uint32_t id, + std::string_view const& id, FeatureStyleRule const& rule, - BoundEvalFun const& evalFun, + BoundEvalFun& evalFun, glm::dvec3 const& offset, double labelPositionHint=0.5); @@ -176,10 +195,22 @@ class FeatureLayerVisualization void addPolyLine( std::vector const& vertsCartesian, const FeatureStyleRule& rule, - uint32_t id, - BoundEvalFun const& evalFun); + JsValue const& id, + BoundEvalFun& evalFun); + + /** + * Add a merged point feature. + */ + void addMergedPointGeometry( + const std::string_view& id, + const std::string& mapLayerStyleRuleId, + const std::optional& gridCellSize, + mapget::Point const& pointCartographic, + const char* geomField, + BoundEvalFun& evalFun, + std::function const& makeGeomParams); - /** + /** * Get some cartesian points as a list of Cesium Cartesian points. */ static JsValue encodeVerticesAsList(std::vector const& points); @@ -199,13 +230,13 @@ class FeatureLayerVisualization * Get an initialised primitive for a particular PolylineDashMaterialAppearance. */ CesiumPrimitive& - getPrimitiveForDashMaterial(const FeatureStyleRule& rule, BoundEvalFun const& evalFun); + getPrimitiveForDashMaterial(const FeatureStyleRule& rule, BoundEvalFun& evalFun); /** * Get an initialised primitive for a particular PolylineArrowMaterialAppearance. */ CesiumPrimitive& - getPrimitiveForArrowMaterial(const FeatureStyleRule& rule, BoundEvalFun const& evalFun); + getPrimitiveForArrowMaterial(const FeatureStyleRule& rule, BoundEvalFun& evalFun); /** * Simfil expression evaluation function for the tile which this visualization belongs to. @@ -217,8 +248,20 @@ class FeatureLayerVisualization */ void addOptionsToSimfilContext(simfil::OverlayNode& context); + /** + * Create a feature primitive ID struct from the mapTileKey_ and the given feature ID. + */ + JsValue makeTileFeatureId(std::string_view const& featureId) const; + + /** + * Get a unique identifier for the map+layer+style+rule-id+highlight-mode. + * In combination with a tile id, this uniquely identifiers a merged corner tile. + */ + std::string getMapLayerStyleRuleId(uint32_t ruleIndex) const; + /// =========== Generic Members =========== + JsValue mapTileKey_; bool featuresAdded_ = false; CesiumPrimitive coloredLines_; std::map, CesiumPrimitive> dashLines_; @@ -230,14 +273,22 @@ class FeatureLayerVisualization std::map arrowGroundLines_; CesiumPrimitive coloredGroundMeshes_; CesiumPointPrimitiveCollection coloredPoints_; - CesiumPrimitiveLabelsCollection labelCollection_; + CesiumLabelCollection labelCollection_; + + // Map from map-layer-style-rule-id to map from grid-position-hash + // to pair of feature-id-set and MergedPointVisualization. + std::map, std::optional>>> mergedPointsPerStyleRuleId_; + JsValue featureMergeService_; FeatureLayerStyle const& style_; mapget::TileFeatureLayer::Ptr tile_; - std::vector> allTiles_; - std::string highlightFeatureId_; + std::vector allTiles_; + std::set featureIdSubset_; std::shared_ptr internalStringPoolCopy_; std::map optionValues_; + FeatureStyleRule::HighlightMode highlightMode_; /// ===== Relation Processing Members ===== diff --git a/libs/core/src/bindings.cpp b/libs/core/src/bindings.cpp index 928c7700..f47b0457 100644 --- a/libs/core/src/bindings.cpp +++ b/libs/core/src/bindings.cpp @@ -1,426 +1,509 @@ -#include -#include - -#include "aabb.h" -#include "buffer.h" -#include "cesium-interface/object.h" -#include "mapget/model/info.h" -#include "mapget/model/sourcedatalayer.h" -#include "simfil/model/nodes.h" -#include "visualization.h" -#include "parser.h" -#include "style.h" -#include "testdataprovider.h" -#include "inspection.h" -#include "geometry.h" -#include "search.h" -#include "sourcedata.hpp" - -#include "cesium-interface/point-conversion.h" -#include "cesium-interface/primitive.h" -#include "simfil/exception-handler.h" - -#include "mapget/log.h" - -using namespace erdblick; -namespace em = emscripten; - -namespace -{ - -/** - * WGS84 Viewport Descriptor, which may be used with the - * `getTileIds` function below. - */ -struct Viewport { - double south = .0; // The southern boundary of the viewport (degrees). - double west = .0; // The western boundary of the viewport (degrees). - double width = .0; // The width of the viewport (degrees). - double height = .0; // The height of the viewport (degrees). - double camPosLon = .0; // The longitude of the camera position (degrees). - double camPosLat = .0; // The latitude of the camera position (degrees). - double orientation = .0; // The compass orientation of the camera (radians). -}; - -/** - * Gets the prioritized list of tile IDs for a given viewport, zoom level, and tile limit. - * - * This function takes a viewport, a zoom level, and a tile limit, and returns an array of tile IDs - * that are visible in the viewport, prioritized by radial distance from the camera position. - * - * The function first extracts the viewport properties and creates an Axis-Aligned Bounding Box (AABB) - * from the viewport boundaries. If the number of tile IDs in the AABB at the given zoom level exceeds - * the specified limit, a new AABB is created from the camera position and tile limit. - * - * The function then populates a vector of prioritized tile IDs by calculating the radial distance - * from the camera position to the center of each tile in the AABB. The tile IDs are then sorted by - * their radial distance, and the sorted array is converted to an emscripten value to be returned. - * Duplicate tile IDs are removed from the array before it is returned. - * - * @param viewport The viewport descriptor for which tile ids are needed. - * @param level The zoom level for which to get the tile IDs. - * @param limit The maximum number of tile IDs to return. - * - * @return An emscripten value representing an array of prioritized tile IDs. - */ -em::val getTileIds(Viewport const& vp, int level, int limit) -{ - Wgs84AABB aabb(Wgs84Point{vp.west, vp.south, .0}, {vp.width, vp.height}); - if (aabb.numTileIds(level) > limit) - // Create a size-limited AABB from the tile limit. - aabb = Wgs84AABB::fromCenterAndTileLimit(Wgs84Point{vp.camPosLon, vp.camPosLat, .0}, limit, level); - - std::vector> prioritizedTileIds; - prioritizedTileIds.reserve(limit); - aabb.tileIdsWithPriority( - level, - prioritizedTileIds, - Wgs84AABB::radialDistancePrioFn({vp.camPosLon, vp.camPosLat}, vp.orientation)); - - std::sort( - prioritizedTileIds.begin(), - prioritizedTileIds.end(), - [](auto const& l, auto const& r) { return l.second < r.second; }); - - em::val resultArray = em::val::array(); - int64_t prevTileId = -1; - for (const auto& tileId : prioritizedTileIds) { - if (tileId.first.value_ == prevTileId) - continue; - resultArray.call("push", tileId.first.value_); - prevTileId = tileId.first.value_; - } - - return resultArray; -} - -uint32_t getNumTileIds(Viewport const& vp, int level) { - Wgs84AABB aabb(Wgs84Point{vp.west, vp.south, .0}, {vp.width, vp.height}); - return aabb.numTileIds(level); -} - -double getTilePriorityById(Viewport const& vp, uint64_t tileId) { - return Wgs84AABB::radialDistancePrioFn({vp.camPosLon, vp.camPosLat}, vp.orientation)(tileId); -} - -/** Get the center position for a mapget tile id in WGS84. */ -mapget::Point getTilePosition(uint64_t tileIdValue) { - mapget::TileId tid(tileIdValue); - return tid.center(); -} - -uint64_t getTileIdFromPosition(double longitude, double latitude, uint16_t level) { - return mapget::TileId::fromWgs84(longitude, latitude, level).value_; -} - -/** Get the bounding box for a mapget tile id in WGS84. */ -em::val getTileBox(uint64_t tileIdValue) { - mapget::TileId tid(tileIdValue); - return *JsValue::List({ - JsValue(tid.sw().x), - JsValue(tid.sw().y), - JsValue(tid.ne().x), - JsValue(tid.ne().y) - }); -} - -/** Get the neighbor for a mapget tile id. */ -uint64_t getTileNeighbor(uint64_t tileIdValue, int32_t offsetX, int32_t offsetY) { - mapget::TileId tid(tileIdValue); - return mapget::TileId(tid.x() + offsetX, tid.y() + offsetY, tid.z()).value_; -} - -/** Get the full string key of a map tile feature layer. */ -std::string getTileFeatureLayerKey(std::string const& mapId, std::string const& layerId, uint64_t tileId) { - auto tileKey = mapget::MapTileKey(); - tileKey.layer_ = mapget::LayerType::Features; - tileKey.mapId_ = mapId; - tileKey.layerId_ = layerId; - tileKey.tileId_ = tileId; - return tileKey.toString(); -} - -/** Get mapId, layerId and tileId of a MapTileKey. */ -NativeJsValue parseTileFeatureLayerKey(std::string const& key) { - auto tileKey = mapget::MapTileKey(key); - return *JsValue::List({JsValue(tileKey.mapId_), JsValue(tileKey.layerId_), JsValue(tileKey.tileId_.value_)}); -} - -/** Create a test tile over New York. */ -void generateTestTile(SharedUint8Array& output, TileLayerParser& parser) { - auto tile = TestDataProvider(parser).getTestLayer(-74.0060, 40.7128, 9); - std::stringstream blob; - tile->write(blob); - output.writeToArray(blob.str()); -} - -/** Create a test style. */ -FeatureLayerStyle generateTestStyle() { - return TestDataProvider::style(); -} - - -/** Demangle a C++ type name. */ -std::string demangle(const char* name) { - int status = -4; // some arbitrary value to eliminate the compiler warning - // enable c++11 by passing the flag -std=c++11 to g++ - std::unique_ptr res { - abi::__cxa_demangle(name, NULL, NULL, &status), - std::free - }; - return (status==0) ? res.get() : name ; -} - -/** Create a test style. */ -void setExceptionHandler(em::val handler) { - simfil::ThrowHandler::instance().set([handler](auto&& type, auto&& message){ - handler(demangle(type.c_str()), message); - }); -} - -/** Validate provided SIMFIL query */ -void validateSimfil(const std::string &query) { - auto simfilEnv = std::make_shared(simfil::Environment::WithNewStringCache); - simfil::compile(*simfilEnv, query, false); -} - -} - -EMSCRIPTEN_BINDINGS(erdblick) -{ - // Activate this to see a lot more output from the WASM lib. - // mapget::log().set_level(spdlog::level::debug); - - ////////// LayerType - em::enum_("LayerType") - .value("FEATURES", mapget::LayerType::Features) - .value("HEIGHTMAP", mapget::LayerType::Heightmap) - .value("ORTHOiMAGE", mapget::LayerType::OrthoImage) - .value("GLTF", mapget::LayerType::GLTF) - .value("SOURCEDATA", mapget::LayerType::SourceData); - - ////////// ValueType - em::enum_("ValueType") - .value("NULL", InspectionConverter::ValueType::Null) - .value("NUMBER", InspectionConverter::ValueType::Number) - .value("STRING", InspectionConverter::ValueType::String) - .value("BOOLEAN", InspectionConverter::ValueType::Boolean) - .value("FEATUREID", InspectionConverter::ValueType::FeatureId) - .value("SECTION", InspectionConverter::ValueType::Section) - .value("ARRAY", InspectionConverter::ValueType::ArrayBit); - - ////////// SharedUint8Array - em::class_("SharedUint8Array") - .constructor() - .constructor() - .function("getSize", &SharedUint8Array::getSize) - .function("getPointer", &SharedUint8Array::getPointer); - - ////////// Point - em::value_object("Point") - .field("x", &mapget::Point::x) - .field("y", &mapget::Point::y) - .field("z", &mapget::Point::z); - - ////////// Viewport - em::value_object("Viewport") - .field("south", &Viewport::south) - .field("west", &Viewport::west) - .field("width", &Viewport::width) - .field("height", &Viewport::height) - .field("camPosLon", &Viewport::camPosLon) - .field("camPosLat", &Viewport::camPosLat) - .field("orientation", &Viewport::orientation); - - ////////// FeatureStyleOptionType - em::enum_("FeatureStyleOptionType") - .value("Bool", FeatureStyleOptionType::Bool); - - ////////// FeatureStyleOption - em::value_object("FeatureStyleOption") - .field("label", &FeatureStyleOption::label_) - .field("id", &FeatureStyleOption::id_) - .field("type", &FeatureStyleOption::type_) - .field("defaultValue", &FeatureStyleOption::defaultValue_) // Ensure correct binding/conversion for YAML::Node - .field("description", &FeatureStyleOption::description_); - - ////////// FeatureLayerStyle - em::register_vector("FeatureStyleOptions"); - em::class_("FeatureLayerStyle").constructor() - .function("options", &FeatureLayerStyle::options, em::allow_raw_pointers()); - - ////////// SourceDataAddressFormat - em::enum_("SourceDataAddressFormat") - .value("UNKNOWN", mapget::TileSourceDataLayer::SourceDataAddressFormat::Unknown) - .value("BIT_RANGE", mapget::TileSourceDataLayer::SourceDataAddressFormat::BitRange); - - ////////// TileSourceDataLayer - em::class_("TileSourceDataLayer") - .smart_ptr>( - "std::shared_ptr") - .function( - "addressFormat", - &mapget::TileSourceDataLayer::sourceDataAddressFormat) - .function( - "toJson", - std::function([](const mapget::TileSourceDataLayer& self) { - return self.toJson().dump(2); - })) - .function( - "toObject", std::function([](const mapget::TileSourceDataLayer& self) { - return *tileSourceDataLayerToObject(self); - })); - - ////////// Feature - using FeaturePtr = mapget::model_ptr; - em::class_("Feature") - .function( - "id", - std::function( - [](FeaturePtr& self) { return self->id()->toString(); })) - .function( - "geojson", - std::function( - [](FeaturePtr& self) { - return self->toJson().dump(4); })) - .function( - "inspectionModel", - std::function( - [](FeaturePtr& self) { - return *InspectionConverter().convert(self); })) - .function( - "center", - std::function( - [](FeaturePtr& self){ - return geometryCenter(self->firstGeometry()); - })); - - ////////// TileFeatureLayer - em::class_("TileFeatureLayer") - .smart_ptr>( - "std::shared_ptr") - .function( - "id", - std::function( - [](mapget::TileFeatureLayer const& self) { return self.id().toString(); })) - .function( - "tileId", - std::function( - [](mapget::TileFeatureLayer const& self) { return self.tileId().value_; })) - .function( - "numFeatures", - std::function( - [](mapget::TileFeatureLayer const& self) { return self.numRoots(); })) - .function( - "center", - std::function( - [](mapget::TileFeatureLayer const& self) - { - em::val result = em::val::object(); - result.set("x", self.tileId().center().x); - result.set("y", self.tileId().center().y); - result.set("z", self.tileId().z()); - return result; - })) - .function( - "at", - std::function< - mapget::model_ptr(mapget::TileFeatureLayer const&, int i)>( - [](mapget::TileFeatureLayer const& self, int i) - { - if (i < 0 || i >= self.numRoots()) { - mapget::log().error("TileFeatureLayer::at(): Index {} is oob.", i); - } - return self.at(i); - })) - .function( - "findFeatureIndex", - std::function< - - int32_t(mapget::TileFeatureLayer const&, std::string, em::val)>( - [](mapget::TileFeatureLayer const& self, std::string type, em::val idParts) -> int32_t - { - auto idPartsKvp = JsValue(idParts).toKeyValuePairs(); - if (auto result = self.find(type, idPartsKvp)) - return result->addr().index(); - return -1; - })); - em::register_vector>("TileFeatureLayers"); - - ////////// FeatureLayerVisualization - em::class_("FeatureLayerVisualization") - .constructor() - .function("addTileFeatureLayer", &FeatureLayerVisualization::addTileFeatureLayer) - .function("run", &FeatureLayerVisualization::run) - .function("primitiveCollection", &FeatureLayerVisualization::primitiveCollection) - .function("externalReferences", &FeatureLayerVisualization::externalReferences) - .function("processResolvedExternalReferences", &FeatureLayerVisualization::processResolvedExternalReferences); - - ////////// FeatureLayerSearch - em::class_("FeatureLayerSearch") - .constructor() - .function("filter", &FeatureLayerSearch::filter) - .function("traceResults", &FeatureLayerSearch::traceResults); - - ////////// TileLayerMetadata - em::value_object("TileLayerMetadata") - .field("id", &TileLayerParser::TileLayerMetadata::id) - .field("nodeId", &TileLayerParser::TileLayerMetadata::nodeId) - .field("mapName", &TileLayerParser::TileLayerMetadata::mapName) - .field("layerName", &TileLayerParser::TileLayerMetadata::layerName) - .field("tileId", &TileLayerParser::TileLayerMetadata::tileId) - .field("numFeatures", &TileLayerParser::TileLayerMetadata::numFeatures); - - ////////// TileLayerParser - em::class_("TileLayerParser") - .constructor<>() - .function("setDataSourceInfo", &TileLayerParser::setDataSourceInfo) - .function("getDataSourceInfo", &TileLayerParser::getDataSourceInfo) - .function("getFieldDictOffsets", &TileLayerParser::getFieldDictOffsets) - .function("getFieldDict", &TileLayerParser::getFieldDict) - .function("addFieldDict", &TileLayerParser::addFieldDict) - .function("readFieldDictUpdate", &TileLayerParser::readFieldDictUpdate) - .function("readTileFeatureLayer", &TileLayerParser::readTileFeatureLayer) - .function("readTileSourceDataLayer", &TileLayerParser::readTileSourceDataLayer) - .function("readTileLayerMetadata", &TileLayerParser::readTileLayerMetadata) - .function( - "filterFeatureJumpTargets", - std::function< - NativeJsValue(TileLayerParser const&, std::string)>( - [](TileLayerParser const& self, std::string input) - { - auto result = self.filterFeatureJumpTargets(input); - auto convertedResult = JsValue::List(); - for (auto const& r : result) - convertedResult.push(r.toJsValue()); - return *convertedResult; - })) - .function("reset", &TileLayerParser::reset); - - ////////// Viewport TileID calculation - em::function("getTileIds", &getTileIds); - em::function("getNumTileIds", &getNumTileIds); - em::function("getTilePriorityById", &getTilePriorityById); - em::function("getTilePosition", &getTilePosition); - em::function("getTileIdFromPosition", &getTileIdFromPosition); - - ////////// Return coordinates for a rectangle representing the bounding box of the tile - em::function("getTileBox", &getTileBox); - - ////////// Get/Parse full id of a TileFeatureLayer - em::function("getTileFeatureLayerKey", &getTileFeatureLayerKey); - em::function("parseTileFeatureLayerKey", &parseTileFeatureLayerKey); - - ////////// Get tile id with vertical/horizontal offset - em::function("getTileNeighbor", &getTileNeighbor); - - ////////// Get a test tile/style - em::function("generateTestTile", &generateTestTile); - em::function("generateTestStyle", &generateTestStyle); - - ////////// Set an exception handler - em::function("setExceptionHandler", &setExceptionHandler); - - ////////// Validate SIMFIL query - em::function("validateSimfilQuery", &validateSimfil); -} +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#if defined(__has_feature) +#if __has_feature(address_sanitizer) +const char *__lsan_default_options() { + return "verbosity=1:malloc_context_size=64"; +} +#endif +#endif + +#include "aabb.h" +#include "buffer.h" +#include "cesium-interface/object.h" +#include "mapget/model/info.h" +#include "mapget/model/sourcedatalayer.h" +#include "simfil/model/nodes.h" +#include "visualization.h" +#include "parser.h" +#include "style.h" +#include "testdataprovider.h" +#include "inspection.h" +#include "geometry.h" +#include "search.h" +#include "layer.h" + +#include "cesium-interface/point-conversion.h" +#include "cesium-interface/primitive.h" +#include "simfil/exception-handler.h" + +#include "mapget/log.h" + +using namespace erdblick; +namespace em = emscripten; + +namespace +{ + +/** + * Check for memory leaks - + * see https://emscripten.org/docs/debugging/Sanitizers.html#memory-leaks + */ +void reportMemoryLeaks() { +#if defined(__has_feature) +#if __has_feature(address_sanitizer) + std::cout << "Running __lsan_do_recoverable_leak_check." << std::endl; + __lsan_do_recoverable_leak_check(); +#endif +#endif +} + +/** + * DisableLeakCheck - + * see https://emscripten.org/docs/debugging/Sanitizers.html#memory-leaks + */ +void disableLeakCheck() { +#if defined(__has_feature) +#if __has_feature(address_sanitizer) + std::cout << "Running __lsan_disable." << std::endl; + __lsan_disable(); +#endif +#endif +} + +/** + * Enable leak check - + * see https://emscripten.org/docs/debugging/Sanitizers.html#memory-leaks + */ +void enableLeakCheck() { +#if defined(__has_feature) +#if __has_feature(address_sanitizer) + std::cout << "Running __lsan_enable." << std::endl; + __lsan_enable(); +#endif +#endif +} + +/** + * Memory debug utilities - + * see https://github.com/emscripten-core/emscripten/blob/main/test/core/test_mallinfo.c. + */ +size_t getTotalMemory() { + return (size_t)EM_ASM_PTR(return HEAP8.length); +} + +/** Same link as above. */ +size_t getFreeMemory() { + struct mallinfo i = mallinfo(); + uintptr_t totalMemory = getTotalMemory(); + uintptr_t dynamicTop = (uintptr_t)sbrk(0); + return totalMemory - dynamicTop + i.fordblks; +} + +/** + * WGS84 Viewport Descriptor, which may be used with the + * `getTileIds` function below. + */ +struct Viewport { + double south = .0; // The southern boundary of the viewport (degrees). + double west = .0; // The western boundary of the viewport (degrees). + double width = .0; // The width of the viewport (degrees). + double height = .0; // The height of the viewport (degrees). + double camPosLon = .0; // The longitude of the camera position (degrees). + double camPosLat = .0; // The latitude of the camera position (degrees). + double orientation = .0; // The compass orientation of the camera (radians). +}; + +/** + * Gets the prioritized list of tile IDs for a given viewport, zoom level, and tile limit. + * + * This function takes a viewport, a zoom level, and a tile limit, and returns an array of tile IDs + * that are visible in the viewport, prioritized by radial distance from the camera position. + * + * The function first extracts the viewport properties and creates an Axis-Aligned Bounding Box (AABB) + * from the viewport boundaries. If the number of tile IDs in the AABB at the given zoom level exceeds + * the specified limit, a new AABB is created from the camera position and tile limit. + * + * The function then populates a vector of prioritized tile IDs by calculating the radial distance + * from the camera position to the center of each tile in the AABB. The tile IDs are then sorted by + * their radial distance, and the sorted array is converted to an emscripten value to be returned. + * Duplicate tile IDs are removed from the array before it is returned. + * + * @param viewport The viewport descriptor for which tile ids are needed. + * @param level The zoom level for which to get the tile IDs. + * @param limit The maximum number of tile IDs to return. + * + * @return An emscripten value representing an array of prioritized tile IDs. + */ +em::val getTileIds(Viewport const& vp, int level, int limit) +{ + Wgs84AABB aabb(Wgs84Point{vp.west, vp.south, .0}, {vp.width, vp.height}); + if (aabb.numTileIds(level) > limit) + // Create a size-limited AABB from the tile limit. + aabb = Wgs84AABB::fromCenterAndTileLimit(Wgs84Point{vp.camPosLon, vp.camPosLat, .0}, limit, level); + + std::vector> prioritizedTileIds; + prioritizedTileIds.reserve(limit); + aabb.tileIdsWithPriority( + level, + prioritizedTileIds, + Wgs84AABB::radialDistancePrioFn({vp.camPosLon, vp.camPosLat}, vp.orientation)); + + std::sort( + prioritizedTileIds.begin(), + prioritizedTileIds.end(), + [](auto const& l, auto const& r) { return l.second < r.second; }); + + em::val resultArray = em::val::array(); + int64_t prevTileId = -1; + for (const auto& tileId : prioritizedTileIds) { + if (tileId.first.value_ == prevTileId) + continue; + resultArray.call("push", tileId.first.value_); + prevTileId = tileId.first.value_; + } + + return resultArray; +} + +uint32_t getNumTileIds(Viewport const& vp, int level) { + Wgs84AABB aabb(Wgs84Point{vp.west, vp.south, .0}, {vp.width, vp.height}); + return aabb.numTileIds(level); +} + +double getTilePriorityById(Viewport const& vp, uint64_t tileId) { + return Wgs84AABB::radialDistancePrioFn({vp.camPosLon, vp.camPosLat}, vp.orientation)(tileId); +} + +/** Get the center position for a mapget tile id in WGS84. */ +mapget::Point getTilePosition(uint64_t tileIdValue) { + return mapget::TileId(tileIdValue).center(); +} + +/** Get the level for a mapget tile id. */ +uint16_t getTileLevel(uint64_t tileIdValue) { + return mapget::TileId(tileIdValue).z(); +} + +/** Get the tile ID for the given level and position. */ +uint64_t getTileIdFromPosition(double longitude, double latitude, uint16_t level) { + return mapget::TileId::fromWgs84(longitude, latitude, level).value_; +} + +/** Get the bounding box for a mapget tile id in WGS84. */ +em::val getTileBox(uint64_t tileIdValue) { + mapget::TileId tid(tileIdValue); + return *JsValue::List({ + JsValue(tid.sw().x), + JsValue(tid.sw().y), + JsValue(tid.ne().x), + JsValue(tid.ne().y) + }); +} + +/** + * Get the bounding box for a mapget corner tile id in WGS84. + * A corner tile box is the original tile box, shifted by half + * the width and height on both axes, so it sits squarely at + * the intersection point of four tiles. + */ +em::val getCornerTileBox(uint64_t tileIdValue) { + mapget::TileId tid(tileIdValue); + auto halfSize = tid.size() * mapget::Point(.5, -.5); + auto sw = tid.sw() + halfSize; + auto ne = tid.ne() + halfSize; + return *JsValue::List({ + JsValue(sw.x), + JsValue(sw.y), + JsValue(ne.x), + JsValue(ne.y) + }); +} + +/** + * Get the neighbor for a mapget tile id. Tile row will be clamped to [0, maxForLevel], + * so a positive/negative wraparound is not possible. The tile id column will wrap at the + * antimeridian. + */ +uint64_t getTileNeighbor(uint64_t tileIdValue, int32_t offsetX, int32_t offsetY) { + return mapget::TileId(tileIdValue).neighbor(offsetX, offsetY).value_; +} + +/** Get the full string key of a map tile feature layer. */ +std::string getTileFeatureLayerKey(std::string const& mapId, std::string const& layerId, uint64_t tileId) { + auto tileKey = mapget::MapTileKey(); + tileKey.layer_ = mapget::LayerType::Features; + tileKey.mapId_ = mapId; + tileKey.layerId_ = layerId; + tileKey.tileId_ = tileId; + return tileKey.toString(); +} + +/** Get mapId, layerId and tileId of a MapTileKey. */ +NativeJsValue parseTileFeatureLayerKey(std::string const& key) { + auto tileKey = mapget::MapTileKey(key); + return *JsValue::List({JsValue(tileKey.mapId_), JsValue(tileKey.layerId_), JsValue(tileKey.tileId_.value_)}); +} + +/** Create a test tile over New York. */ +void generateTestTile(SharedUint8Array& output, TileLayerParser& parser) { + auto tile = TestDataProvider(parser).getTestLayer(-74.0060, 40.7128, 9); + std::stringstream blob; + tile->write(blob); + output.writeToArray(blob.str()); +} + +/** Create a test style. */ +FeatureLayerStyle generateTestStyle() { + return TestDataProvider::style(); +} + + +/** Demangle a C++ type name. */ +std::string demangle(const char* name) { + int status = -4; // some arbitrary value to eliminate the compiler warning + // enable c++11 by passing the flag -std=c++11 to g++ + std::unique_ptr res { + abi::__cxa_demangle(name, NULL, NULL, &status), + std::free + }; + return (status==0) ? res.get() : name ; +} + +/** Create a test style. */ +void setExceptionHandler(em::val handler) { + simfil::ThrowHandler::instance().set([handler](auto&& type, auto&& message){ + handler(demangle(type.c_str()), message); + }); +} + +/** Validate provided SIMFIL query */ +void validateSimfil(const std::string &query) { + auto simfilEnv = std::make_shared(simfil::Environment::WithNewStringCache); + simfil::compile(*simfilEnv, query, false); +} + +} + +EMSCRIPTEN_BINDINGS(erdblick) +{ + // Activate this to see a lot more output from the WASM lib. + // mapget::log().set_level(spdlog::level::debug); + + ////////// LayerType + em::enum_("LayerType") + .value("FEATURES", mapget::LayerType::Features) + .value("HEIGHTMAP", mapget::LayerType::Heightmap) + .value("ORTHOiMAGE", mapget::LayerType::OrthoImage) + .value("GLTF", mapget::LayerType::GLTF) + .value("SOURCEDATA", mapget::LayerType::SourceData); + + ////////// ValueType + em::enum_("ValueType") + .value("NULL", InspectionConverter::ValueType::Null) + .value("NUMBER", InspectionConverter::ValueType::Number) + .value("STRING", InspectionConverter::ValueType::String) + .value("BOOLEAN", InspectionConverter::ValueType::Boolean) + .value("FEATUREID", InspectionConverter::ValueType::FeatureId) + .value("SECTION", InspectionConverter::ValueType::Section) + .value("ARRAY", InspectionConverter::ValueType::ArrayBit); + + ////////// SharedUint8Array + em::class_("SharedUint8Array") + .constructor() + .constructor() + .function("getSize", &SharedUint8Array::getSize) + .function("getPointer", &SharedUint8Array::getPointer); + + ////////// Point + em::value_object("Point") + .field("x", &mapget::Point::x) + .field("y", &mapget::Point::y) + .field("z", &mapget::Point::z); + + ////////// Viewport + em::value_object("Viewport") + .field("south", &Viewport::south) + .field("west", &Viewport::west) + .field("width", &Viewport::width) + .field("height", &Viewport::height) + .field("camPosLon", &Viewport::camPosLon) + .field("camPosLat", &Viewport::camPosLat) + .field("orientation", &Viewport::orientation); + + ////////// FeatureStyleOptionType + em::enum_("FeatureStyleOptionType") + .value("Bool", FeatureStyleOptionType::Bool); + + ////////// FeatureStyleOption + em::value_object("FeatureStyleOption") + .field("label", &FeatureStyleOption::label_) + .field("id", &FeatureStyleOption::id_) + .field("type", &FeatureStyleOption::type_) + .field("defaultValue", &FeatureStyleOption::defaultValue_) // Ensure correct binding/conversion for YAML::Node + .field("description", &FeatureStyleOption::description_); + + ////////// FeatureLayerStyle + em::register_vector("FeatureStyleOptions"); + em::class_("FeatureLayerStyle").constructor() + .function("options", &FeatureLayerStyle::options, em::allow_raw_pointers()) + .function("name", &FeatureLayerStyle::name); + + ////////// SourceDataAddressFormat + em::enum_("SourceDataAddressFormat") + .value("UNKNOWN", mapget::TileSourceDataLayer::SourceDataAddressFormat::Unknown) + .value("BIT_RANGE", mapget::TileSourceDataLayer::SourceDataAddressFormat::BitRange); + + ////////// TileSourceDataLayer + em::class_("TileSourceDataLayer") + .function("addressFormat", &TileSourceDataLayer::addressFormat) + .function("toJson", &TileSourceDataLayer::toJson) + .function("toObject", &TileSourceDataLayer::toObject) + .function("getError", &TileSourceDataLayer::getError); + + ////////// Feature + using FeaturePtr = mapget::model_ptr; + em::class_("Feature") + .function( + "isNull", + std::function( + [](FeaturePtr& self) { return !self; })) + .function( + "id", + std::function( + [](FeaturePtr& self) { return self->id()->toString(); })) + .function( + "geojson", + std::function( + [](FeaturePtr& self) { + return self->toJson().dump(4); })) + .function( + "inspectionModel", + std::function( + [](FeaturePtr& self) { + return *InspectionConverter().convert(self); })) + .function( + "center", + std::function( + [](FeaturePtr& self){ + return geometryCenter(self->firstGeometry()); + })) + .function( + "boundingRadiusEndPoint", + std::function( + [](FeaturePtr& self){ + return boundingRadiusEndPoint(self->firstGeometry()); + })) + .function( + "getGeometryType", + std::function( + [](FeaturePtr& self){ + return getGeometryType(self->firstGeometry()); + })); + + ////////// GeomType + em::enum_("GeomType") + .value("Points", mapget::GeomType::Points) + .value("Line", mapget::GeomType::Line) + .value("Polygon", mapget::GeomType::Polygon) + .value("Mesh", mapget::GeomType::Mesh); + + ////////// TileFeatureLayer + em::class_("TileFeatureLayer") + .function("id", &TileFeatureLayer::id) + .function("tileId", &TileFeatureLayer::tileId) + .function("numFeatures", &TileFeatureLayer::numFeatures) + .function("center", &TileFeatureLayer::center) + .function("find", &TileFeatureLayer::find) + .function("findFeatureIndex", &TileFeatureLayer::findFeatureIndex); + + ////////// Highlight Modes + em::enum_("HighlightMode") + .value("NO_HIGHLIGHT", FeatureStyleRule::NoHighlight) + .value("HOVER_HIGHLIGHT", FeatureStyleRule::HoverHighlight) + .value("SELECTION_HIGHLIGHT", FeatureStyleRule::SelectionHighlight); + + ////////// FeatureLayerVisualization + em::class_("FeatureLayerVisualization") + .constructor() + .function("addTileFeatureLayer", &FeatureLayerVisualization::addTileFeatureLayer) + .function("run", &FeatureLayerVisualization::run) + .function("primitiveCollection", &FeatureLayerVisualization::primitiveCollection) + .function("mergedPointFeatures", &FeatureLayerVisualization::mergedPointFeatures) + .function("externalReferences", &FeatureLayerVisualization::externalReferences) + .function("processResolvedExternalReferences", &FeatureLayerVisualization::processResolvedExternalReferences); + + ////////// FeatureLayerSearch + em::class_("FeatureLayerSearch") + .constructor() + .function("filter", &FeatureLayerSearch::filter) + .function("traceResults", &FeatureLayerSearch::traceResults); + + ////////// TileLayerMetadata + em::value_object("TileLayerMetadata") + .field("id", &TileLayerParser::TileLayerMetadata::id) + .field("nodeId", &TileLayerParser::TileLayerMetadata::nodeId) + .field("mapName", &TileLayerParser::TileLayerMetadata::mapName) + .field("layerName", &TileLayerParser::TileLayerMetadata::layerName) + .field("tileId", &TileLayerParser::TileLayerMetadata::tileId) + .field("numFeatures", &TileLayerParser::TileLayerMetadata::numFeatures); + + ////////// TileLayerParser + em::class_("TileLayerParser") + .constructor<>() + .function("setDataSourceInfo", &TileLayerParser::setDataSourceInfo) + .function("getDataSourceInfo", &TileLayerParser::getDataSourceInfo) + .function("getFieldDictOffsets", &TileLayerParser::getFieldDictOffsets) + .function("getFieldDict", &TileLayerParser::getFieldDict) + .function("addFieldDict", &TileLayerParser::addFieldDict) + .function("readFieldDictUpdate", &TileLayerParser::readFieldDictUpdate) + .function("readTileFeatureLayer", &TileLayerParser::readTileFeatureLayer) + .function("readTileSourceDataLayer", &TileLayerParser::readTileSourceDataLayer) + .function("readTileLayerMetadata", &TileLayerParser::readTileLayerMetadata) + .function( + "filterFeatureJumpTargets", + std::function< + NativeJsValue(TileLayerParser const&, std::string)>( + [](TileLayerParser const& self, std::string input) + { + auto result = self.filterFeatureJumpTargets(input); + auto convertedResult = JsValue::List(); + for (auto const& r : result) + convertedResult.push(r.toJsValue()); + return *convertedResult; + })) + .function("reset", &TileLayerParser::reset); + + ////////// Viewport TileID calculation + em::function("getTileIds", &getTileIds); + em::function("getNumTileIds", &getNumTileIds); + em::function("getTilePriorityById", &getTilePriorityById); + em::function("getTilePosition", &getTilePosition); + em::function("getTileIdFromPosition", &getTileIdFromPosition); + em::function("getTileBox", &getTileBox); + em::function("getCornerTileBox", &getCornerTileBox); + em::function("getTileLevel", &getTileLevel); + + ////////// Get/Parse full id of a TileFeatureLayer + em::function("getTileFeatureLayerKey", &getTileFeatureLayerKey); + em::function("parseTileFeatureLayerKey", &parseTileFeatureLayerKey); + + ////////// Get tile id with vertical/horizontal offset + em::function("getTileNeighbor", &getTileNeighbor); + + ////////// Get a test tile/style + em::function("generateTestTile", &generateTestTile); + em::function("generateTestStyle", &generateTestStyle); + + ////////// Set an exception handler + em::function("setExceptionHandler", &setExceptionHandler); + + ////////// Validate SIMFIL query + em::function("validateSimfilQuery", &validateSimfil); + + ////////// Memory utilities + em::function("getTotalMemory", &getTotalMemory); + em::function("getFreeMemory", &getFreeMemory); + em::function("reportMemoryLeaks", &reportMemoryLeaks); + em::function("enableLeakCheck", &enableLeakCheck); + em::function("disableLeakCheck", &disableLeakCheck); +} diff --git a/libs/core/src/cesium-interface/labels.cpp b/libs/core/src/cesium-interface/labels.cpp index a8f34dd8..216f7c11 100644 --- a/libs/core/src/cesium-interface/labels.cpp +++ b/libs/core/src/cesium-interface/labels.cpp @@ -6,71 +6,91 @@ namespace erdblick { -CesiumPrimitiveLabelsCollection::CesiumPrimitiveLabelsCollection() : +CesiumLabelCollection::CesiumLabelCollection() : labelCollection_(Cesium().LabelCollection.New()) {} -void CesiumPrimitiveLabelsCollection::addLabel( - JsValue const &position, - const std::string &labelText, - FeatureStyleRule const &style, - uint32_t id, - BoundEvalFun const& evalFun) { +JsValue CesiumLabelCollection::labelParams( + const JsValue& position, + const std::string& labelText, + const FeatureStyleRule& style, + const JsValue& id, + const BoundEvalFun& evalFun) +{ auto const &color = style.labelColor(); auto const &outlineColor = style.labelOutlineColor(); auto const &bgColor = style.labelBackgroundColor(); auto const &padding = style.labelBackgroundPadding(); auto labelProperties = JsValue::Dict({ - {"id", JsValue(id)}, - {"position", position}, - {"show", JsValue(true)}, - {"text", JsValue(labelText)}, - {"font", JsValue(style.labelFont())}, - {"disableDepthTestDistance", JsValue(std::numeric_limits::infinity())}, - {"fillColor", Cesium().Color.New(color.r, color.g, color.b, color.a)}, - {"outlineColor", Cesium().Color.New(outlineColor.r, outlineColor.g, outlineColor.b, outlineColor.a)}, - {"outlineWidth", JsValue(style.labelOutlineWidth())}, - {"showBackground", JsValue(style.showBackground())}, - {"backgroundColor", Cesium().Color.New(bgColor.r, bgColor.g, bgColor.b, bgColor.a)}, - {"backgroundPadding", Cesium().Cartesian2.New(padding.first, padding.second)}, - {"style", Cesium().LabelStyle[style.labelStyle()]}, - {"horizontalOrigin", Cesium().HorizontalOrigin[style.labelHorizontalOrigin()]}, - {"verticalOrigin", Cesium().VerticalOrigin[style.labelVerticalOrigin()]}, - {"scale", JsValue(style.labelScale())} + {"id", id}, + {"position", position}, + {"show", JsValue(true)}, + {"text", JsValue(labelText)}, + {"font", JsValue(style.labelFont())}, + {"disableDepthTestDistance", JsValue(std::numeric_limits::infinity())}, + {"fillColor", Cesium().Color.New(color.r, color.g, color.b, color.a)}, + {"outlineColor", Cesium().Color.New(outlineColor.r, outlineColor.g, outlineColor.b, outlineColor.a)}, + {"outlineWidth", JsValue(style.labelOutlineWidth())}, + {"showBackground", JsValue(style.showBackground())}, + {"backgroundColor", Cesium().Color.New(bgColor.r, bgColor.g, bgColor.b, bgColor.a)}, + {"backgroundPadding", Cesium().Cartesian2.New(padding.first, padding.second)}, + {"style", Cesium().LabelStyle[style.labelStyle()]}, + {"horizontalOrigin", Cesium().HorizontalOrigin[style.labelHorizontalOrigin()]}, + {"verticalOrigin", Cesium().VerticalOrigin[style.labelVerticalOrigin()]}, + {"scale", JsValue(style.labelScale())} }); - if (auto const &sbd = style.scaleByDistance()) { - labelProperties.set("scaleByDistance", + if (auto const& sbd = style.scaleByDistance()) { + labelProperties.set( + "scaleByDistance", Cesium().NearFarScalar.New((*sbd)[0], (*sbd)[1], (*sbd)[2], (*sbd)[3])); - } else if (auto const &nfs = style.nearFarScale() ) { - labelProperties.set("scaleByDistance", + } + else if (auto const& nfs = style.nearFarScale()) { + labelProperties.set( + "scaleByDistance", Cesium().NearFarScalar.New((*nfs)[0], (*nfs)[1], (*nfs)[2], (*nfs)[3])); } - if (auto const &osbd = style.offsetScaleByDistance() ) { - labelProperties.set("pixelOffsetScaleByDistance", + if (auto const& osbd = style.offsetScaleByDistance()) { + labelProperties.set( + "pixelOffsetScaleByDistance", Cesium().NearFarScalar.New((*osbd)[0], (*osbd)[1], (*osbd)[2], (*osbd)[3])); } - if (auto const &pixelOffset = style.labelPixelOffset()) { - labelProperties.set("pixelOffset", - Cesium().Cartesian2.New(pixelOffset->first, pixelOffset->second)); + if (auto const& pixelOffset = style.labelPixelOffset()) { + labelProperties + .set("pixelOffset", Cesium().Cartesian2.New(pixelOffset->first, pixelOffset->second)); } - if (auto const &eyeOffset = style.labelEyeOffset()) { - labelProperties.set("eyeOffset", - Cesium().Cartesian3.New(std::get<0>(*eyeOffset),std::get<1>(*eyeOffset),std::get<2>(*eyeOffset))); + if (auto const& eyeOffset = style.labelEyeOffset()) { + labelProperties.set( + "eyeOffset", + Cesium() + .Cartesian3 + .New(std::get<0>(*eyeOffset), std::get<1>(*eyeOffset), std::get<2>(*eyeOffset))); } - if (auto const &tbd = style.translucencyByDistance()) { - labelProperties.set("translucencyByDistance", + if (auto const& tbd = style.translucencyByDistance()) { + labelProperties.set( + "translucencyByDistance", Cesium().NearFarScalar.New((*tbd)[0], (*tbd)[1], (*tbd)[2], (*tbd)[3])); } - labelCollection_.call("add", *labelProperties); + return labelProperties; +} + +void CesiumLabelCollection::addLabel( + JsValue const &position, + const std::string &labelText, + FeatureStyleRule const &style, + JsValue const& id, + BoundEvalFun const& evalFun) +{ + auto params = labelParams(position, labelText, style, id, evalFun); + labelCollection_.call("add", *params); numLabelInstances_++; } -NativeJsValue CesiumPrimitiveLabelsCollection::toJsObject() const { +NativeJsValue CesiumLabelCollection::toJsObject() const { return *labelCollection_; } -bool CesiumPrimitiveLabelsCollection::empty() const { +bool CesiumLabelCollection::empty() const { return numLabelInstances_ == 0; } diff --git a/libs/core/src/cesium-interface/object.cpp b/libs/core/src/cesium-interface/object.cpp index 84f7c70b..ec01d15b 100644 --- a/libs/core/src/cesium-interface/object.cpp +++ b/libs/core/src/cesium-interface/object.cpp @@ -69,6 +69,15 @@ JsValue JsValue::Float64Array(const std::vector& coordinates) #endif } +JsValue JsValue::Undefined() +{ +#ifdef EMSCRIPTEN + return JsValue(emscripten::val::undefined()); +#else + return JsValue(""); +#endif +} + JsValue JsValue::operator[](std::string const& propertyName) { #ifdef EMSCRIPTEN diff --git a/libs/core/src/cesium-interface/points.cpp b/libs/core/src/cesium-interface/points.cpp index a579e372..3a3470a1 100644 --- a/libs/core/src/cesium-interface/points.cpp +++ b/libs/core/src/cesium-interface/points.cpp @@ -12,11 +12,11 @@ CesiumPointPrimitiveCollection::CesiumPointPrimitiveCollection() : pointPrimitiveCollection_(Cesium().PointPrimitiveCollection.New()) {} -void CesiumPointPrimitiveCollection::addPoint( +JsValue CesiumPointPrimitiveCollection::pointParams( const JsValue& position, - FeatureStyleRule const& style, - uint32_t id, - BoundEvalFun const& evalFun) + const FeatureStyleRule& style, + const JsValue& id, + const BoundEvalFun& evalFun) { auto const color = style.color(evalFun); auto const& oColor = style.outlineColor(); @@ -25,7 +25,7 @@ void CesiumPointPrimitiveCollection::addPoint( {"position", position}, {"color", Cesium().Color.New(color.r, color.g, color.b, color.a)}, {"pixelSize", JsValue(style.width())}, - {"id", JsValue(id)}, + {"id", id}, {"outlineColor", Cesium().Color.New(oColor.r, oColor.g, oColor.b, oColor.a)}, {"outlineWidth", JsValue(style.outlineWidth())}, }); @@ -36,7 +36,17 @@ void CesiumPointPrimitiveCollection::addPoint( Cesium().NearFarScalar.New((*nfs)[0], (*nfs)[1], (*nfs)[2], (*nfs)[3])); } - pointPrimitiveCollection_.call("add", *options); + return options; +} + +void CesiumPointPrimitiveCollection::addPoint( + const JsValue& position, + FeatureStyleRule const& style, + JsValue const& id, + BoundEvalFun const& evalFun) +{ + auto params = pointParams(position, style, id, evalFun); + pointPrimitiveCollection_.call("add", *params); ++numGeometryInstances_; } @@ -50,4 +60,4 @@ bool CesiumPointPrimitiveCollection::empty() const return numGeometryInstances_ == 0; } -} \ No newline at end of file +} diff --git a/libs/core/src/cesium-interface/primitive.cpp b/libs/core/src/cesium-interface/primitive.cpp index 1a8f327d..b59dea93 100644 --- a/libs/core/src/cesium-interface/primitive.cpp +++ b/libs/core/src/cesium-interface/primitive.cpp @@ -64,7 +64,7 @@ CesiumPrimitive CesiumPrimitive::withPerInstanceColorAppearance(bool flatAndSync void CesiumPrimitive::addPolyLine( JsValue const &vertices, FeatureStyleRule const &style, - uint32_t id, + JsValue const& id, BoundEvalFun const &evalFun) { JsValue polyline; if (clampToGround_) { @@ -85,7 +85,7 @@ void CesiumPrimitive::addPolyLine( void CesiumPrimitive::addPolygon( const JsValue &vertices, const FeatureStyleRule &style, - uint32_t id, + JsValue const& id, BoundEvalFun const &evalFun) { auto polygon = Cesium().PolygonGeometry.New({ {"polygonHierarchy", Cesium().PolygonHierarchy.New(*vertices)}, @@ -98,7 +98,7 @@ void CesiumPrimitive::addPolygon( void CesiumPrimitive::addTriangles( const JsValue &float64Array, const FeatureStyleRule &style, - uint32_t id, + JsValue const& id, BoundEvalFun const &evalFun) { auto geometry = Cesium().Geometry.New({ {"attributes", JsValue::Dict({ @@ -115,7 +115,7 @@ void CesiumPrimitive::addTriangles( void CesiumPrimitive::addGeometryInstance( const FeatureStyleRule &style, - uint32_t id, + JsValue const& id, const JsValue &geom, BoundEvalFun const &evalFun) { auto attributes = JsValue::Dict(); @@ -125,7 +125,7 @@ void CesiumPrimitive::addGeometryInstance( } auto geometryInstance = Cesium().GeometryInstance.New({ {"geometry", geom}, - {"id", JsValue(id)}, + {"id", id}, {"attributes", attributes} }); ++numGeometryInstances_; diff --git a/libs/core/src/geometry.cpp b/libs/core/src/geometry.cpp index bd7702dd..fb2ff599 100644 --- a/libs/core/src/geometry.cpp +++ b/libs/core/src/geometry.cpp @@ -3,6 +3,8 @@ #include "geometry.h" #include "cesium-interface/point-conversion.h" +using namespace mapget; + Point erdblick::geometryCenter(const model_ptr& g) { if (!g) { @@ -118,6 +120,36 @@ Point erdblick::geometryCenter(const model_ptr& g) return averageVectorPosition(intersectedTrianglePoints); } +Point erdblick::boundingRadiusEndPoint(const model_ptr& g) +{ + const Point center = erdblick::geometryCenter(g); + if (!g) { + std::cerr << "Cannot obtain bounding radius vector end point of null geometry." << std::endl; + return center; + } + + float maxDistanceSquared = 0.0f; + Point farPoint = center; + g->forEachPoint([¢er, &maxDistanceSquared, &farPoint](const auto& p) + { + float dx = p.x - center.x; + float dy = p.y - center.y; + float dz = p.z - center.z; + float distanceSquared = dx * dx + dy * dy + dz * dz; + if (distanceSquared > maxDistanceSquared) { + farPoint = p; + maxDistanceSquared = distanceSquared; + } + return true; + }); + + return farPoint; +} + +GeomType erdblick::getGeometryType(const model_ptr& g) { + return g->geomType(); +} + double erdblick::pointSideOfLine(const Point& lineVector, const Point& lineStart, const Point& p) { return lineVector.x * (p.y - lineStart.y) - lineVector.y * (p.x - lineStart.x); diff --git a/libs/core/src/inspection.cpp b/libs/core/src/inspection.cpp index 0bf82571..6d7878c2 100644 --- a/libs/core/src/inspection.cpp +++ b/libs/core/src/inspection.cpp @@ -46,6 +46,11 @@ JsValue InspectionConverter::convert(model_ptr const& featurePtr) stringPool_ = featurePtr->model().strings(); featureId_ = featurePtr->id()->toString(); + // Top-Level Feature Item + auto featureScope = push("Feature", "", ValueType::Section); + featureScope->value_ = JsValue(featurePtr->id()->toString()); + convertSourceDataReferences(featurePtr->sourceDataReferences(), *featureScope); + // Identifiers section. { auto scope = push(convertStringView("Identifiers"), "", ValueType::Section); @@ -56,15 +61,15 @@ JsValue InspectionConverter::convert(model_ptr const& featurePtr) push("layerId", "layerId", ValueType::String)->value_ = convertStringView(featurePtr->model().layerInfo()->layerId_); // TODO: Investigate and fix the issue for "index out of bounds" error. - // Affects boundaries and lane connectors -// if (auto prefix = featurePtr->model().getIdPrefix()) { -// for (auto const& [k, v] : prefix->fields()) { -// convertField(k, v); -// } -// } -// for (auto const& [k, v] : featurePtr->id()->fields()) { -// convertField(k, v); -// } + // Affects boundaries and lane connectors + // if (auto prefix = featurePtr->model().getIdPrefix()) { + // for (auto const& [k, v] : prefix->fields()) { + // convertField(k, v); + // } + // } + // for (auto const& [k, v] : featurePtr->id()->fields()) { + // convertField(k, v); + // } for (auto const& [key, value]: featurePtr->id()->keyValuePairs()) { auto &field = current_->children_.emplace_back(); @@ -237,6 +242,7 @@ void InspectionConverter::convertRelation(const model_ptr& r) auto relGroupScope = push(relGroup); auto relScope = push(JsValue(relGroup->children_.size()), nextRelationIndex_, ValueType::FeatureId); relScope->value_ = JsValue(r->target()->toString()); + relScope->mapId_ = JsValue(r->model().mapId()); relScope->hoverId_ = featureId_+":relation#"+std::to_string(nextRelationIndex_); convertSourceDataReferences(r->sourceDataReferences(), *relScope); if (r->hasSourceValidity()) { @@ -378,6 +384,8 @@ JsValue InspectionConverter::InspectionNode::toJsValue() const newDict.set("children", childrenToJsValue()); if (!geoJsonPath_.empty()) newDict.set("geoJsonPath", JsValue(geoJsonPath_)); + if (mapId_) + newDict.set("mapId", *mapId_); if (!sourceDataRefs_.empty()) { auto list = JsValue::List(); for (const auto& ref : sourceDataRefs_) { diff --git a/libs/core/src/layer.cpp b/libs/core/src/layer.cpp new file mode 100644 index 00000000..5ce931e3 --- /dev/null +++ b/libs/core/src/layer.cpp @@ -0,0 +1,247 @@ +#include "layer.h" + +#include "mapget/model/feature.h" +#include + +namespace erdblick +{ + +/** + * Constructor accepting a shared pointer to the original `TileFeatureLayer` class. + * @param self Shared pointer to `mapget::TileFeatureLayer`. + */ +TileFeatureLayer::TileFeatureLayer(std::shared_ptr self) + : model_(std::move(self)) {} + +/** + * Retrieves the ID of the tile feature layer as a string. + * @return The ID string. + */ +std::string TileFeatureLayer::id() +{ + return model_->id().toString(); +} + +/** + * Retrieves the tile ID as a 64-bit unsigned integer. + * @return The tile ID. + */ +uint64_t TileFeatureLayer::tileId() const +{ + return model_->tileId().value_; +} + +/** + * Gets the number of features in the tile. + * @return The number of features. + */ +uint32_t TileFeatureLayer::numFeatures() const +{ + return model_->numRoots(); +} + +/** + * Retrieves the center point of the tile, including the zoom level as the Z coordinate. + * @return The center point of the tile. + */ +mapget::Point TileFeatureLayer::center() const +{ + auto result = model_->tileId().center(); + result.z = model_->tileId().z(); + return result; +} + +/** + * Finds a feature within the tile by its ID. + * @param id The ID of the feature to find. + * @return A pointer to the found feature, or `nullptr` if not found. + */ +mapget::model_ptr TileFeatureLayer::find(const std::string& id) const +{ + return model_->find(id); +} + +/** + * Finds the index of a feature based on its type and ID parts. + * @param type The type of the feature. + * @param idParts The parts of the feature's ID. + * @return The index of the feature, or `-1` if not found. + */ +int32_t TileFeatureLayer::findFeatureIndex(std::string type, NativeJsValue idParts) const +{ + auto idPartsKvp = JsValue(idParts).toKeyValuePairs(); + if (auto result = model_->find(type, idPartsKvp)) + return result->addr().index(); + return -1; +} + +TileFeatureLayer::~TileFeatureLayer() = default; + +/** + * Constructor accepting a shared pointer to the original `TileSourceDataLayer` class. + * @param self Shared pointer to `mapget::TileSourceDataLayer`. + */ +TileSourceDataLayer::TileSourceDataLayer(std::shared_ptr self) + : model_(std::move(self)) {} + +/** + * Retrieves the source data address format of the layer. + * @return The address format. + */ +mapget::TileSourceDataLayer::SourceDataAddressFormat TileSourceDataLayer::addressFormat() const +{ + return model_->sourceDataAddressFormat(); +} + +/** + * Converts the layer's data to a JSON string with indentation. + * @return The JSON representation of the layer. + */ +std::string TileSourceDataLayer::toJson() const +{ + return model_->toJson().dump(2); +} + +/** + * Converts the `SourceDataLayer` hierarchy to a tree model compatible structure. + * + * **Layout:** + * ```json + * [ + * { + * "data": {"key": "...", "value": ...}, + * "children": [{ ... }] + * }, + * ... + * ] + * ``` + * @return A `NativeJsValue` representing the hierarchical data structure. + */ +NativeJsValue TileSourceDataLayer::toObject() const +{ + using namespace erdblick; + using namespace mapget; + using namespace simfil; + + const auto& strings = *model_->strings(); + + std::function visit; + + // Function to handle atomic (non-complex) nodes + auto visitAtomic = [&](JsValue&& key, const simfil::ModelNode& node) { + auto value = [&node]() -> JsValue { + switch (node.type()) { + case simfil::ValueType::Null: + return JsValue(); + case simfil::ValueType::Bool: + return JsValue(std::get(node.value())); + case simfil::ValueType::Int: + return JsValue(std::get(node.value())); + case simfil::ValueType::Float: + return JsValue(std::get(node.value())); + case simfil::ValueType::String: { + auto v = node.value(); + if (auto vv = std::get_if(&v)) + return JsValue(*vv); + if (auto vv = std::get_if(&v)) + return JsValue(std::string(*vv)); + } + default: + return JsValue(); + } + }(); + + auto res = JsValue::Dict(); + auto data = JsValue::Dict(); + data.set("key", std::move(key)); + data.set("value", std::move(value)); + res.set("data", std::move(data)); + + return res; + }; + + // Function to handle array nodes + auto visitArray = [&](JsValue&& key, const simfil::ModelNode& node) -> JsValue { + auto res = JsValue::Dict(); + + auto data = JsValue::Dict(); + data.set("key", std::move(key)); + res.set("data", std::move(data)); + + auto children = JsValue::List(); + int i = 0; + for (const auto& item : node) { + children.call("push", visit(JsValue(i++), *item)); + } + + if (i > 0) + res.set("children", std::move(children)); + + return res; + }; + + // Function to handle source data addresses + auto visitAddress = [&](const SourceDataAddress& addr) { + if (model_->sourceDataAddressFormat() == mapget::TileSourceDataLayer::SourceDataAddressFormat::BitRange) { + auto res = JsValue::Dict(); + res.set("offset", JsValue(addr.bitOffset())); + res.set("size", JsValue(addr.bitSize())); + return res; + } else { + return JsValue(addr.u64()); + } + }; + + // Function to handle object nodes + auto visitObject = [&](JsValue&& key, const simfil::ModelNode& node) -> JsValue { + auto res = JsValue::Dict(); + + auto data = JsValue::Dict(); + data.set("key", std::move(key)); + + if (node.addr().column() == mapget::TileSourceDataLayer::Compound) { + auto compound = model_->resolveCompound(*ModelNode::Ptr::make(model_->shared_from_this(), node.addr())); + + data.set("address", visitAddress(compound->sourceDataAddress())); + data.set("type", JsValue(std::string(compound->schemaName()))); + } + + res.set("data", std::move(data)); + + auto children = JsValue::List(); + for (const auto& [field, v] : node.fields()) { + if (auto k = strings.resolve(field); k && v) { + children.call("push", visit(JsValue(k->data()), *v)); + } + } + + if (node.size() > 0) + res.set("children", std::move(children)); + + return res; + }; + + // Main recursive visit function + visit = [&](JsValue&& key, const simfil::ModelNode& node) -> JsValue { + switch (node.type()) { + case simfil::ValueType::Array: + return visitArray(std::move(key), node); + case simfil::ValueType::Object: + return visitObject(std::move(key), node); + default: + return visitAtomic(std::move(key), node); + } + }; + + if (model_->numRoots() == 0) + return *JsValue::Dict(); + + return *visit(JsValue("root"), *model_->root(0)); +} + +std::string TileSourceDataLayer::getError() const +{ + return model_->error() ? *model_->error() : ""; +} + +} // namespace erdblick diff --git a/libs/core/src/parser.cpp b/libs/core/src/parser.cpp index ab458b35..64e732b9 100644 --- a/libs/core/src/parser.cpp +++ b/libs/core/src/parser.cpp @@ -84,47 +84,39 @@ NativeJsValue TileLayerParser::getFieldDictOffsets() void TileLayerParser::reset() { + // Note: The reader is only ever used to read field dict updates. + // For this, it does not need a layer info provider or onParsedLayer callback. reader_ = std::make_unique( - [this](auto&& mapId, auto&& layerId) - { - return resolveMapLayerInfo(std::string(mapId), std::string(layerId)); - }, - [this](auto&& layer){ - const auto type = layer->layerInfo()->type_; - if (type != LayerType::Features) - return; - - if (tileParsedFun_) - tileParsedFun_(std::static_pointer_cast(layer)); - }, + [](auto&& mapId, auto&& layerId){return nullptr;}, + [](auto&& layer){}, cachedStrings_); } -mapget::TileFeatureLayer::Ptr TileLayerParser::readTileFeatureLayer(const SharedUint8Array& buffer) +TileFeatureLayer TileLayerParser::readTileFeatureLayer(const SharedUint8Array& buffer) { std::stringstream inputStream; inputStream << buffer.toString(); - auto result = std::make_shared( + auto result = TileFeatureLayer(std::make_shared( inputStream, [this](auto&& mapId, auto&& layerId) { return resolveMapLayerInfo(std::string(mapId), std::string(layerId)); }, - [this](auto&& nodeId) { return cachedStrings_->getStringPool(nodeId); }); + [this](auto&& nodeId) { return cachedStrings_->getStringPool(nodeId); })); return result; } -mapget::TileSourceDataLayer::Ptr TileLayerParser::readTileSourceDataLayer(SharedUint8Array const& buffer) +TileSourceDataLayer TileLayerParser::readTileSourceDataLayer(SharedUint8Array const& buffer) { std::stringstream inputStream; inputStream << buffer.toString(); - auto result = std::make_shared( + auto result = TileSourceDataLayer(std::make_shared( inputStream, [this](auto&& mapId, auto&& layerId) { return resolveMapLayerInfo(std::string(mapId), std::string(layerId)); }, - [this](auto&& nodeId) { return cachedStrings_->getStringPool(nodeId); }); + [this](auto&& nodeId) { return cachedStrings_->getStringPool(nodeId); })); return result; } diff --git a/libs/core/src/rule.cpp b/libs/core/src/rule.cpp index 6113594a..d5e6c00c 100644 --- a/libs/core/src/rule.cpp +++ b/libs/core/src/rule.cpp @@ -12,13 +12,13 @@ std::optional parseArrowMode(std::string const& arrowSt if (arrowStr == "none") { return FeatureStyleRule::NoArrow; } - else if (arrowStr == "forward") { + if (arrowStr == "forward") { return FeatureStyleRule::ForwardArrow; } - else if (arrowStr == "backward") { + if (arrowStr == "backward") { return FeatureStyleRule::BackwardArrow; } - else if (arrowStr == "double") { + if (arrowStr == "double") { return FeatureStyleRule::DoubleArrow; } @@ -30,13 +30,13 @@ std::optional parseGeometryEnum(std::string const& enumStr) { if (enumStr == "point") { return mapget::GeomType::Points; } - else if (enumStr == "mesh") { + if (enumStr == "mesh") { return mapget::GeomType::Mesh; } - else if (enumStr == "line") { + if (enumStr == "line") { return mapget::GeomType::Line; } - else if (enumStr == "polygon") { + if (enumStr == "polygon") { return mapget::GeomType::Polygon; } @@ -45,7 +45,7 @@ std::optional parseGeometryEnum(std::string const& enumStr) { } } -FeatureStyleRule::FeatureStyleRule(YAML::Node const& yaml) +FeatureStyleRule::FeatureStyleRule(YAML::Node const& yaml, uint32_t index) : index_(index) { parse(yaml); } @@ -100,11 +100,14 @@ void FeatureStyleRule::parse(const YAML::Node& yaml) if (yaml["mode"].IsDefined()) { // Parse the feature aspect that is covered by this rule. auto modeStr = yaml["mode"].as(); - if (modeStr == "normal") { - mode_ = Normal; + if (modeStr == "none") { + mode_ = NoHighlight; } - else if (modeStr == "highlight") { - mode_ = Highlight; + else if (modeStr == "hover") { + mode_ = HoverHighlight; + } + else if (modeStr == "selection") { + mode_ = SelectionHighlight; } else { std::cout << "Unsupported mode: " << modeStr << std::endl; @@ -168,6 +171,12 @@ void FeatureStyleRule::parse(const YAML::Node& yaml) offset_.y = yaml["offset"][1].as(); offset_.z = yaml["offset"][2].as(); } + if (yaml["point-merge-grid-cell"].IsDefined() && yaml["point-merge-grid-cell"].size() >= 3) { + pointMergeGridCellSize_ = glm::dvec3(); + pointMergeGridCellSize_->x = yaml["point-merge-grid-cell"][0].as(); + pointMergeGridCellSize_->y = yaml["point-merge-grid-cell"][1].as(); + pointMergeGridCellSize_->z = yaml["point-merge-grid-cell"][2].as(); + } ///////////////////////////////////// /// Line Style Fields @@ -364,7 +373,7 @@ void FeatureStyleRule::parse(const YAML::Node& yaml) } } -FeatureStyleRule const* FeatureStyleRule::match(mapget::Feature& feature) const +FeatureStyleRule const* FeatureStyleRule::match(mapget::Feature& feature, BoundEvalFun const& evalFun) const { // Filter by feature type regular expression. if (type_) { @@ -376,7 +385,7 @@ FeatureStyleRule const* FeatureStyleRule::match(mapget::Feature& feature) const // Filter by simfil expression. if (!filter_.empty()) { - if (!feature.evaluate(filter_).as()) { + if (!evalFun.eval_(filter_).as()) { return nullptr; } } @@ -384,7 +393,7 @@ FeatureStyleRule const* FeatureStyleRule::match(mapget::Feature& feature) const // Return matching sub-rule or this. if (!firstOfRules_.empty()) { for (auto const& rule : firstOfRules_) { - if (auto matchingRule = rule.match(feature)) { + if (auto matchingRule = rule.match(feature, evalFun)) { return matchingRule; } } @@ -402,7 +411,7 @@ bool FeatureStyleRule::supports(const mapget::GeomType& g) const glm::fvec4 FeatureStyleRule::color(BoundEvalFun const& evalFun) const { if (!colorExpression_.empty()) { - auto colorVal = evalFun(colorExpression_); + auto colorVal = evalFun.eval_(colorExpression_); if (colorVal.isa(simfil::ValueType::Int)) { auto colorInt = colorVal.as(); auto a = static_cast(colorInt & 0xff) / 255.; @@ -458,7 +467,7 @@ int FeatureStyleRule::dashPattern() const FeatureStyleRule::Arrow FeatureStyleRule::arrow(BoundEvalFun const& evalFun) const { if (!arrowExpression_.empty()) { - auto arrowVal = evalFun(arrowExpression_); + auto arrowVal = evalFun.eval_(arrowExpression_); if (arrowVal.isa(simfil::ValueType::String)) { auto arrowStr = arrowVal.as(); if (auto arrowMode = parseArrowMode(arrowStr)) @@ -526,7 +535,7 @@ bool FeatureStyleRule::selectable() const return selectable_; } -FeatureStyleRule::Mode FeatureStyleRule::mode() const +FeatureStyleRule::HighlightMode FeatureStyleRule::mode() const { return mode_; } @@ -599,13 +608,11 @@ std::string const& FeatureStyleRule::labelTextExpression() const std::string FeatureStyleRule::labelText(BoundEvalFun const& evalFun) const { if (!labelTextExpression_.empty()) { - auto resultVal = evalFun(labelTextExpression_); + auto resultVal = evalFun.eval_(labelTextExpression_); auto resultText = resultVal.toString(); if (!resultText.empty()) { return resultText; } - std::cout << "Empty return value for the label text expression: " << labelTextExpression_ - << ": " << resultVal.toString() << std::endl; return labelText_; } return labelText_; @@ -650,6 +657,11 @@ glm::dvec3 const& FeatureStyleRule::offset() const return offset_; } +std::optional const& FeatureStyleRule::pointMergeGridCellSize() const +{ + return pointMergeGridCellSize_; +} + std::optional const& FeatureStyleRule::attributeType() const { return attributeType_; @@ -665,4 +677,9 @@ std::optional const& FeatureStyleRule::attributeValidityGeometry() const return attributeValidityGeometry_; } +uint32_t const& FeatureStyleRule::index() const +{ + return index_; +} + } \ No newline at end of file diff --git a/libs/core/src/search.cpp b/libs/core/src/search.cpp index be8477fd..dffdecdc 100644 --- a/libs/core/src/search.cpp +++ b/libs/core/src/search.cpp @@ -2,16 +2,16 @@ #include "geometry.h" #include "cesium-interface/point-conversion.h" -erdblick::FeatureLayerSearch::FeatureLayerSearch(mapget::TileFeatureLayer& tfl) : tfl_(tfl) +erdblick::FeatureLayerSearch::FeatureLayerSearch(TileFeatureLayer& tfl) : tfl_(tfl) {} erdblick::NativeJsValue erdblick::FeatureLayerSearch::filter(const std::string& q) { auto results = JsValue::List(); - auto mapTileKey = tfl_.id().toString(); + auto mapTileKey = tfl_.id(); - for (const auto& feature : tfl_) { - auto evalResult = tfl_.evaluate(anyWrap(q), *feature); + for (const auto& feature : *tfl_.model_) { + auto evalResult = tfl_.model_->evaluate(anyWrap(q), *feature); if (evalResult.empty()) continue; auto& firstEvalResult = evalResult[0]; @@ -42,3 +42,4 @@ std::string erdblick::anyWrap(const std::string_view& q) { return fmt::format("any({})", q); } + diff --git a/libs/core/src/sourcedata.cpp b/libs/core/src/sourcedata.cpp deleted file mode 100644 index 24132b92..00000000 --- a/libs/core/src/sourcedata.cpp +++ /dev/null @@ -1,125 +0,0 @@ -#include "sourcedata.hpp" - -#include "mapget/model/sourcedatalayer.h" -#include "mapget/model/sourcedata.h" - -namespace erdblick -{ - -erdblick::JsValue tileSourceDataLayerToObject(const mapget::TileSourceDataLayer& layer) { - using namespace erdblick; - using namespace mapget; - using namespace simfil; - - const auto& strings = *layer.strings(); - - std::function visit; - auto visitAtomic = [&](JsValue&& key, const simfil::ModelNode& node) { - auto value = [&node]() -> JsValue { - switch (node.type()) { - case simfil::ValueType::Null: - return JsValue(); - case simfil::ValueType::Bool: - return JsValue(std::get(node.value())); - case simfil::ValueType::Int: - return JsValue(std::get(node.value())); - case simfil::ValueType::Float: - return JsValue(std::get(node.value())); - case simfil::ValueType::String: { - auto v = node.value(); - if (auto vv = std::get_if(&v)) - return JsValue(*vv); - if (auto vv = std::get_if(&v)) - return JsValue(std::string(*vv)); - } - default: - return JsValue(); - } - }(); - - auto res = JsValue::Dict(); - auto data = JsValue::Dict(); - data.set("key", std::move(key)); - data.set("value", std::move(value)); - res.set("data", std::move(data)); - - return res; - }; - - auto visitArray = [&](JsValue&& key, const simfil::ModelNode& node) -> JsValue { - auto res = JsValue::Dict(); - - auto data = JsValue::Dict(); - data.set("key", std::move(key)); - res.set("data", std::move(data)); - - auto children = JsValue::List(); - auto i = 0; - for (const auto& item : node) { - children.call("push", visit(JsValue(i++), *item)); - } - - if (i > 0) - res.set("children", std::move(children)); - - return res; - }; - - auto visitAddress = [&](const SourceDataAddress& addr) { - if (layer.sourceDataAddressFormat() == mapget::TileSourceDataLayer::SourceDataAddressFormat::BitRange) { - auto res = JsValue::Dict(); - res.set("offset", JsValue(addr.bitOffset())); - res.set("size", JsValue(addr.bitSize())); - - return res; - } else { - return JsValue(addr.u64()); - } - }; - - auto visitObject = [&](JsValue&& key, const simfil::ModelNode& node) -> JsValue { - auto res = JsValue::Dict(); - - auto data = JsValue::Dict(); - data.set("key", std::move(key)); - - if (node.addr().column() == mapget::TileSourceDataLayer::Compound) { - auto compound = layer.resolveCompound(*ModelNode::Ptr::make(layer.shared_from_this(), node.addr())); - - data.set("address", visitAddress(compound->sourceDataAddress())); - data.set("type", JsValue(std::string(compound->schemaName()))); - } - - res.set("data", std::move(data)); - - auto children = JsValue::List(); - for (const auto& [field, v] : node.fields()) { - if (auto k = strings.resolve(field); k && v) { - children.call("push", visit(JsValue(k->data()), *v)); - } - } - - if (node.size() > 0) - res.set("children", std::move(children)); - - return res; - }; - - visit = [&](JsValue&& key, const simfil::ModelNode& node) -> JsValue { - switch (node.type()) { - case simfil::ValueType::Array: - return visitArray(std::move(key), node); - case simfil::ValueType::Object: - return visitObject(std::move(key), node); - default: - return visitAtomic(std::move(key), node); - } - }; - - if (layer.numRoots() == 0) - return JsValue::Dict(); - - return visit(JsValue("root"), *layer.root(0)); -} - -} diff --git a/libs/core/src/style.cpp b/libs/core/src/style.cpp index 42ab0f44..8a227972 100644 --- a/libs/core/src/style.cpp +++ b/libs/core/src/style.cpp @@ -17,14 +17,20 @@ FeatureLayerStyle::FeatureLayerStyle(SharedUint8Array const& yamlArray) // Convert char vector to YAML node. auto styleYaml = YAML::Load(styleSpec); + if (auto name = styleYaml["name"]) { + if (name.IsScalar()) + name_ = name.Scalar(); + } + if (!styleYaml["rules"] || !(styleYaml["rules"].IsSequence())) { std::cout << "YAML stylesheet error: Spec does not contain any rules?" << std::endl; return; } + uint32_t ruleIndex = 0; for (auto const& rule : styleYaml["rules"]) { // Create FeatureStyleRule object. - rules_.emplace_back(rule); + rules_.emplace_back(rule, ruleIndex++); } for (auto const& option : styleYaml["options"]) { @@ -33,8 +39,6 @@ FeatureLayerStyle::FeatureLayerStyle(SharedUint8Array const& yamlArray) } valid_ = true; - - std::cout << "Parsed a style YAML!" << std::endl; } bool FeatureLayerStyle::isValid() const @@ -52,6 +56,10 @@ const std::vector& FeatureLayerStyle::options() const return options_; } +std::string const& FeatureLayerStyle::name() const { + return name_; +} + FeatureStyleOption::FeatureStyleOption(const YAML::Node& yaml) { if (auto node = yaml["label"]) { diff --git a/libs/core/src/visualization.cpp b/libs/core/src/visualization.cpp index a4e1d1be..9644e26e 100644 --- a/libs/core/src/visualization.cpp +++ b/libs/core/src/visualization.cpp @@ -19,16 +19,21 @@ uint32_t fvec4ToInt(glm::fvec4 const& v) { } FeatureLayerVisualization::FeatureLayerVisualization( + std::string const& mapTileKey, const FeatureLayerStyle& style, NativeJsValue const& rawOptionValues, - std::string highlightFeatureId) - : coloredLines_(CesiumPrimitive::withPolylineColorAppearance(false)), + NativeJsValue const& rawFeatureMergeService, + FeatureStyleRule::HighlightMode const& highlightMode, + NativeJsValue const& rawFeatureIdSubset) + : mapTileKey_(mapTileKey), + coloredLines_(CesiumPrimitive::withPolylineColorAppearance(false)), coloredNontrivialMeshes_(CesiumPrimitive::withPerInstanceColorAppearance(false, false)), coloredTrivialMeshes_(CesiumPrimitive::withPerInstanceColorAppearance(true)), coloredGroundLines_(CesiumPrimitive::withPolylineColorAppearance(true)), coloredGroundMeshes_(CesiumPrimitive::withPerInstanceColorAppearance(true, true)), + featureMergeService_(rawFeatureMergeService), style_(style), - highlightFeatureId_(std::move(highlightFeatureId)), + highlightMode_(highlightMode), externalRelationReferences_(JsValue::List()) { // Convert option values dict to simfil values. @@ -44,54 +49,80 @@ FeatureLayerVisualization::FeatureLayerVisualization( }); optionValues_.emplace(option.id_, std::move(simfilValue)); } + + // Convert feature ID subset. + auto featureIdSubset = JsValue(rawFeatureIdSubset); + for (auto i = 0; i < featureIdSubset.size(); ++i) { + featureIdSubset_.insert(featureIdSubset.at(i).as()); + } } -void FeatureLayerVisualization::addTileFeatureLayer( - std::shared_ptr tile) +FeatureLayerVisualization::~FeatureLayerVisualization() = default; + +void FeatureLayerVisualization::addTileFeatureLayer(TileFeatureLayer const& tile) { if (!tile_) { - tile_ = tile; - internalStringPoolCopy_ = std::make_shared(*tile->strings()); + tile_ = tile.model_; + internalStringPoolCopy_ = std::make_shared(*tile.model_->strings()); + + // Pre-create empty merged point feature visualization lists. + for (auto&& rule : style_.rules()) { + if (rule.mode() != highlightMode_ || !rule.pointMergeGridCellSize()) { + continue; + } + mergedPointsPerStyleRuleId_.emplace( + getMapLayerStyleRuleId(rule.index()), + std::map, std::optional>>()); + } } + // Ensure that the added aux tile and the primary tile use the same // field name encoding. So we transcode the aux tile into the same dict. // However, the transcoding process changes the dictionary, as it might // add unknown field names. This would fork the dict state from the remote // node dict, which leads to undefined behavior. So we work on a copy of it. - tile->setStrings(internalStringPoolCopy_); - allTiles_.emplace_back(tile); + tile.model_->setStrings(internalStringPoolCopy_); + allTiles_.emplace_back(tile.model_); } void FeatureLayerVisualization::run() { - uint32_t featureId = 0; - for (auto&& feature : *tile_) { - if (!highlightFeatureId_.empty()) { - if (feature->id()->toString() != highlightFeatureId_) { - ++featureId; - continue; - } - } + auto const& constFeature = static_cast(*feature); + simfil::OverlayNode evaluationContext(simfil::Value::field(constFeature)); + addOptionsToSimfilContext(evaluationContext); + auto boundEvalFun = BoundEvalFun{ + evaluationContext, + [this, &evaluationContext](auto&& str) + { + return evaluateExpression(str, evaluationContext); + }}; for (auto&& rule : style_.rules()) { - if (!highlightFeatureId_.empty()) { - if (rule.mode() != FeatureStyleRule::Highlight) - continue; - } - else if (rule.mode() != FeatureStyleRule::Normal) + if (rule.mode() != highlightMode_) { continue; - - if (auto* matchingSubRule = rule.match(*feature)) { - addFeature(feature, featureId, *matchingSubRule); + } + auto mapLayerStyleRuleId = getMapLayerStyleRuleId(rule.index()); + if (auto* matchingSubRule = rule.match(*feature, boundEvalFun)) { + addFeature(feature, boundEvalFun, *matchingSubRule, mapLayerStyleRuleId); featuresAdded_ = true; } } - ++featureId; } } +std::string FeatureLayerVisualization::getMapLayerStyleRuleId(uint32_t ruleIndex) const +{ + return fmt::format( + "{}:{}:{}:{}:{}", + tile_->mapId(), + tile_->layerInfo()->layerId_, + style_.name(), + static_cast(highlightMode_), + ruleIndex); +} + NativeJsValue FeatureLayerVisualization::primitiveCollection() const { if (!featuresAdded_) @@ -130,6 +161,21 @@ NativeJsValue FeatureLayerVisualization::primitiveCollection() const return *collection; } +NativeJsValue FeatureLayerVisualization::mergedPointFeatures() const +{ + auto result = JsValue::Dict(); + for (auto const& [mapLayerStyleRuleId, primitives] : mergedPointsPerStyleRuleId_) { + auto pointList = JsValue::List(); + for (auto const& [_, featureIdsAndPoint] : primitives) { + if (auto const& pt = featureIdsAndPoint.second) { + pointList.push(*pt); + } + } + result.set(mapLayerStyleRuleId, pointList); + } + return *result; +} + NativeJsValue FeatureLayerVisualization::externalReferences() { return *externalRelationReferences_; @@ -187,23 +233,26 @@ void FeatureLayerVisualization::processResolvedExternalReferences( void FeatureLayerVisualization::addFeature( model_ptr& feature, - uint32_t id, - FeatureStyleRule const& rule) + BoundEvalFun& evalFun, + FeatureStyleRule const& rule, + std::string const& mapLayerStyleRuleId) { + auto featureId = feature->id()->toString(); + if (!featureIdSubset_.empty()) { + if (featureIdSubset_.find(featureId) == featureIdSubset_.end()) { + return; + } + } + auto offset = localWgs84UnitCoordinateSystem(feature->firstGeometry()) * rule.offset(); switch(rule.aspect()) { case FeatureStyleRule::Feature: { - auto const& constFeature = static_cast(*feature); - simfil::OverlayNode evaluationContext(simfil::Value::field(constFeature)); - addOptionsToSimfilContext(evaluationContext); - - auto boundEvalFun = [this, &evaluationContext](auto&& str){return evaluateExpression(str, evaluationContext);}; feature->geom()->forEachGeometry( - [this, id, &rule, &boundEvalFun, &offset](auto&& geom) + [this, featureId, &rule, &mapLayerStyleRuleId, &evalFun, &offset](auto&& geom) { if (rule.supports(geom->geomType())) - addGeometry(geom, id, rule, boundEvalFun, offset); + addGeometry(geom, featureId, rule, mapLayerStyleRuleId, evalFun, offset); return true; }); break; @@ -232,8 +281,9 @@ void FeatureLayerVisualization::addFeature( feature, layerName, attr, - id, // TODO: Rethink, how an attribute link may be encoded in the id. + featureId, // TODO: Rethink, how an attribute link may be encoded in the id. rule, + mapLayerStyleRuleId, offsetFactor, offset); return true; @@ -247,20 +297,35 @@ void FeatureLayerVisualization::addFeature( void FeatureLayerVisualization::addGeometry( model_ptr const& geom, - uint32_t id, + std::string_view id, FeatureStyleRule const& rule, - BoundEvalFun const& evalFun, + std::string const& mapLayerStyleRuleId, + BoundEvalFun& evalFun, glm::dvec3 const& offset) { - if (!rule.selectable()) - id = UnselectableId; + // Combine the ID with the mapTileKey to create an + // easy link from the geometry back to the feature. + auto tileFeatureId = JsValue::Undefined(); + if (rule.selectable()) { + switch (highlightMode_) { + case FeatureStyleRule::NoHighlight: + tileFeatureId = makeTileFeatureId(id); + break; + case FeatureStyleRule::HoverHighlight: + tileFeatureId = JsValue("hover-highlight"); + break; + case FeatureStyleRule::SelectionHighlight: + tileFeatureId = JsValue("selection-highlight"); + break; + } + } std::vector vertsCartesian; vertsCartesian.reserve(geom->numPoints()); geom->forEachPoint( [&vertsCartesian, &offset](auto&& vertex) { - vertsCartesian.emplace_back(wgsToCartesian(vertex, offset)); + vertsCartesian.emplace_back(wgsToCartesian(vertex, offset)); return true; }); @@ -269,23 +334,43 @@ void FeatureLayerVisualization::addGeometry( if (vertsCartesian.size() >= 3) { auto jsVerts = encodeVerticesAsList(vertsCartesian); if (rule.flat()) - coloredGroundMeshes_.addPolygon(jsVerts, rule, id, evalFun); + coloredGroundMeshes_.addPolygon(jsVerts, rule, tileFeatureId, evalFun); else - coloredNontrivialMeshes_.addPolygon(jsVerts, rule, id, evalFun); + coloredNontrivialMeshes_.addPolygon(jsVerts, rule, tileFeatureId, evalFun); } break; case GeomType::Line: - addPolyLine(vertsCartesian, rule, id, evalFun); + addPolyLine(vertsCartesian, rule, tileFeatureId, evalFun); break; case GeomType::Mesh: if (vertsCartesian.size() >= 3) { auto jsVerts = encodeVerticesAsFloat64Array(vertsCartesian); - coloredTrivialMeshes_.addTriangles(jsVerts, rule, id, evalFun); + coloredTrivialMeshes_.addTriangles(jsVerts, rule, tileFeatureId, evalFun); } break; case GeomType::Points: + auto pointIndex = 0; for (auto const& pt : vertsCartesian) { - coloredPoints_.addPoint(JsValue(pt), rule, id, evalFun); + + // If a merge-grid cell size is set, then a merged feature representation was requested. + if (auto const& gridCellSize = rule.pointMergeGridCellSize()) { + addMergedPointGeometry( + id, + mapLayerStyleRuleId, + gridCellSize, + geom->pointAt(pointIndex), + "pointParameters", + evalFun, + [&](auto& augmentedEvalFun) + { + return coloredPoints_ + .pointParams(JsValue(pt), rule, tileFeatureId, augmentedEvalFun); + }); + } + else + coloredPoints_.addPoint(JsValue(pt), rule, tileFeatureId, evalFun); + + ++pointIndex; } break; } @@ -293,12 +378,87 @@ void FeatureLayerVisualization::addGeometry( if (rule.hasLabel()) { auto text = rule.labelText(evalFun); if (!text.empty()) { - labelCollection_.addLabel( - JsValue(wgsToCartesian(geometryCenter(geom), offset)), - text, - rule, - id, - evalFun); + auto wgsPos = geometryCenter(geom); + auto xyzPos = JsValue(wgsToCartesian(wgsPos, offset)); + + // If a merge-grid cell size is set, then a merged feature representation was requested. + if (auto const& gridCellSize = rule.pointMergeGridCellSize()) { + addMergedPointGeometry( + id, + mapLayerStyleRuleId, + gridCellSize, + wgsPos, + "labelParameters", + evalFun, + [&](auto& augmentedEvalFun) + { + return labelCollection_.labelParams( + xyzPos, + text, + rule, + tileFeatureId, + augmentedEvalFun); + }); + } + else + labelCollection_.addLabel( + xyzPos, + text, + rule, + tileFeatureId, + evalFun); + } + } +} + +void FeatureLayerVisualization::addMergedPointGeometry( + const std::string_view& id, + const std::string& mapLayerStyleRuleId, + const std::optional& gridCellSize, + mapget::Point const& pointCartographic, + const char* geomField, + BoundEvalFun& evalFun, + std::function const& makeGeomParams) +{ + // Convert the cartographic point to an integer representation, based + // on the grid cell size set in the style sheet. + auto gridPosition = pointCartographic / *gridCellSize; + auto gridPositionHash = fmt::format( + "{}:{}:{}", + static_cast(glm::floor(gridPosition.x)), + static_cast(glm::floor(gridPosition.y)), + static_cast(glm::floor(gridPosition.z))); + + // Add the $mergeCount variable to the evaluation context. + // This variable indicates, how many features from other tiles have already been added + // for the given grid position. We must sum both existing points in the point merge service + // from other tiles, and existing points from this tile. + auto& [mergedPointFeatureSet, mergedPointVisu] = + mergedPointsPerStyleRuleId_[mapLayerStyleRuleId][gridPositionHash]; + auto [_, featureIdIsNew] = mergedPointFeatureSet.emplace(id); + auto mergedPointCount = featureMergeService_.call( + "count", + pointCartographic, + gridPositionHash, + tile_->tileId().z(), + mapLayerStyleRuleId) + static_cast(mergedPointFeatureSet.size()); + evalFun.context_.set( + internalStringPoolCopy_->emplace("$mergeCount"), + simfil::Value(mergedPointCount)); + + // Add a MergedPointVisualization to the list. + if (!mergedPointVisu) { + mergedPointVisu = JsValue::Dict({ + {"position", JsValue(pointCartographic)}, + {"positionHash", JsValue(gridPositionHash)}, + {geomField, JsValue(makeGeomParams(evalFun))}, + {"featureIds", JsValue::List({makeTileFeatureId(id)})}, + }); + } + else { + mergedPointVisu->set(geomField, JsValue(makeGeomParams(evalFun))); + if (featureIdIsNew) { + (*mergedPointVisu)["featureIds"].push(makeTileFeatureId(id)); } } } @@ -359,7 +519,7 @@ FeatureLayerVisualization::encodeVerticesAsFloat64Array(std::vector(wgsA, offset); auto cartB = wgsToCartesian(wgsB, offset); + // Combine the ID with the mapTileKey to create an + // easy link from the geometry back to the feature. + auto tileFeatureId = makeTileFeatureId(id); + addPolyLine( {cartA, cartB}, rule, - id, + tileFeatureId, evalFun); if (rule.hasLabel()) { @@ -420,7 +584,7 @@ void erdblick::FeatureLayerVisualization::addLine( JsValue(mapget::Point(cartA + (cartB - cartA) * labelPositionHint)), text, rule, - id, + tileFeatureId, evalFun); } } @@ -429,8 +593,8 @@ void erdblick::FeatureLayerVisualization::addLine( void FeatureLayerVisualization::addPolyLine( std::vector const& vertsCartesian, const FeatureStyleRule& rule, - uint32_t id, - BoundEvalFun const& evalFun) + JsValue const& tileFeatureId, + BoundEvalFun& evalFun) { if (vertsCartesian.size() < 2) return; @@ -440,27 +604,27 @@ void FeatureLayerVisualization::addPolyLine( if (arrowType == FeatureStyleRule::DoubleArrow) { auto jsVertsPair = encodeVerticesAsReversedSplitList(vertsCartesian); auto& primitive = getPrimitiveForArrowMaterial(rule, evalFun); - primitive.addPolyLine(jsVertsPair.first, rule, id, evalFun); - primitive.addPolyLine(jsVertsPair.second, rule, id, evalFun); + primitive.addPolyLine(jsVertsPair.first, rule, tileFeatureId, evalFun); + primitive.addPolyLine(jsVertsPair.second, rule, tileFeatureId, evalFun); return; } auto jsVerts = encodeVerticesAsList(vertsCartesian); if (arrowType == FeatureStyleRule::ForwardArrow) { - getPrimitiveForArrowMaterial(rule, evalFun).addPolyLine(jsVerts, rule, id, evalFun); + getPrimitiveForArrowMaterial(rule, evalFun).addPolyLine(jsVerts, rule, tileFeatureId, evalFun); } else if (arrowType == FeatureStyleRule::BackwardArrow) { jsVerts.call("reverse"); - getPrimitiveForArrowMaterial(rule, evalFun).addPolyLine(jsVerts, rule, id, evalFun); + getPrimitiveForArrowMaterial(rule, evalFun).addPolyLine(jsVerts, rule, tileFeatureId, evalFun); } else if (rule.isDashed()) { - getPrimitiveForDashMaterial(rule, evalFun).addPolyLine(jsVerts, rule, id, evalFun); + getPrimitiveForDashMaterial(rule, evalFun).addPolyLine(jsVerts, rule, tileFeatureId, evalFun); } else if (rule.flat()) { - coloredGroundLines_.addPolyLine(jsVerts, rule, id, evalFun); + coloredGroundLines_.addPolyLine(jsVerts, rule, tileFeatureId, evalFun); } else { - coloredLines_.addPolyLine(jsVerts, rule, id, evalFun); + coloredLines_.addPolyLine(jsVerts, rule, tileFeatureId, evalFun); } } @@ -488,8 +652,9 @@ void FeatureLayerVisualization::addAttribute( model_ptr const& feature, std::string_view const& layer, model_ptr const& attr, - uint32_t id, + std::string_view const& id, const FeatureStyleRule& rule, + std::string const& mapLayerStyleRuleId, uint32_t& offsetFactor, glm::dvec3 const& offset) { @@ -530,10 +695,12 @@ void FeatureLayerVisualization::addAttribute( simfil::Value(layer)); // Function which can evaluate a simfil expression in the attribute context. - auto boundEvalFun = [this, &attrEvaluationContext](auto&& str) - { - return evaluateExpression(str, attrEvaluationContext); - }; + auto boundEvalFun = BoundEvalFun{ + attrEvaluationContext, + [this, &attrEvaluationContext](auto&& str) + { + return evaluateExpression(str, attrEvaluationContext); + }}; // Bump visual offset factor for next visualized attribute. ++offsetFactor; @@ -544,6 +711,7 @@ void FeatureLayerVisualization::addAttribute( geom, id, rule, + mapLayerStyleRuleId, boundEvalFun, offset * static_cast(offsetFactor)); } @@ -555,6 +723,14 @@ void FeatureLayerVisualization::addOptionsToSimfilContext(simfil::OverlayNode& c } } +JsValue FeatureLayerVisualization::makeTileFeatureId(const std::string_view& featureId) const +{ + return JsValue::Dict({ + {"mapTileKey", mapTileKey_}, + {"featureId", JsValue(std::string(featureId))} + }); +} + RecursiveRelationVisualizationState::RecursiveRelationVisualizationState( const FeatureStyleRule& rule, mapget::model_ptr f, @@ -683,10 +859,12 @@ void RecursiveRelationVisualizationState::render( simfil::Value(r.twoway_)); // Function which can evaluate a simfil expression in the relation context. - auto boundEvalFun = [this, &relationEvaluationContext](auto&& str) - { - return visu_.evaluateExpression(str, relationEvaluationContext); - }; + auto boundEvalFun = BoundEvalFun{ + relationEvaluationContext, + [this, &relationEvaluationContext](auto&& str) + { + return visu_.evaluateExpression(str, relationEvaluationContext); + }}; // Obtain source/target geometries. auto sourceGeom = r.relation_->hasSourceValidity() ? @@ -739,14 +917,14 @@ void RecursiveRelationVisualizationState::render( // Run source geometry visualization. if (sourceGeom && visualizedFeatures_.emplace(sourceId).second) { if (auto sourceRule = rule_.relationSourceStyle()) { - visu_.addGeometry(sourceGeom, UnselectableId, *sourceRule, boundEvalFun, offsetBase * sourceRule->offset()); + visu_.addGeometry(sourceGeom, UnselectableId, *sourceRule, "", boundEvalFun, offsetBase * sourceRule->offset()); } } // Run target geometry visualization. if (targetGeom && visualizedFeatures_.emplace(targetId).second) { if (auto targetRule = rule_.relationTargetStyle()) { - visu_.addGeometry(targetGeom, UnselectableId, *targetRule, boundEvalFun, offsetBase * targetRule->offset()); + visu_.addGeometry(targetGeom, UnselectableId, *targetRule, "", boundEvalFun, offsetBase * targetRule->offset()); } } diff --git a/package-lock.json b/package-lock.json index 809673c0..19b14849 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "erdblick", - "version": "2024.3.2", + "version": "2024.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "erdblick", - "version": "2024.3.2", + "version": "2024.4.0", "dependencies": { "@angular/animations": "^18.2.0", "@angular/common": "^18.2.0", @@ -16,13 +16,15 @@ "@angular/platform-browser": "^18.2.0", "@angular/platform-browser-dynamic": "^18.2.0", "@angular/router": "^18.2.0", - "@codemirror/autocomplete": "^6.14.0", - "@codemirror/lang-yaml": "^6.0.0", - "@codemirror/lint": "^6.5.0", - "@codemirror/view": "^6.25.1", + "@codemirror/autocomplete": "^6.18.0", + "@codemirror/lang-yaml": "^6.1.1", + "@codemirror/lint": "^6.8.1", + "@codemirror/view": "^6.32.0", + "@ngx-formly/core": "^6.3.6", + "@ngx-formly/primeng": "^6.3.6", "assert": "^2.1.0", "browserify-zlib": "^0.2.0", - "cesium": "1.115.0", + "cesium": "1.120.0", "codemirror": "^6.0.1", "https-browserify": "^1.0.0", "js-yaml": "^4.1.0", @@ -32,8 +34,8 @@ "rxjs": "~7.8.1", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", - "tslib": "^2.3.0", - "url": "^0.11.3", + "tslib": "^2.6.3", + "url": "^0.11.4", "util": "^0.12.5", "zone.js": "~0.14.10" }, @@ -42,92 +44,23 @@ "@angular-devkit/build-angular": "^18.2.1", "@angular/cli": "^18.2.1", "@angular/compiler-cli": "^18.2.0", - "@types/jasmine": "~4.3.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.9.0", + "@types/jasmine": "~5.1.4", "@types/js-yaml": "^4.0.9", - "@typescript-eslint/eslint-plugin": "^7.4.0", - "@typescript-eslint/parser": "^7.4.0", - "eslint": "^8.57.0", - "jasmine-core": "~4.6.0", - "karma": "~6.4.0", + "@typescript-eslint/eslint-plugin": "^8.2.0", + "@typescript-eslint/parser": "^8.2.0", + "eslint": "^9.9.0", + "globals": "^15.9.0", + "jasmine-core": "~5.2.0", + "karma": "~6.4.4", "karma-chrome-launcher": "~3.2.0", - "karma-coverage": "~2.2.0", + "karma-coverage": "~2.2.1", "karma-jasmine": "~5.1.0", - "karma-jasmine-html-reporter": "~2.0.0", + "karma-jasmine-html-reporter": "~2.1.0", "typescript": "~5.5.4" } }, - "../../klebert/primeng": { - "version": "17.18.9", - "extraneous": true, - "license": "SEE LICENSE IN LICENSE.md", - "devDependencies": { - "@angular-devkit/build-angular": "^18.0.2", - "@angular-eslint/eslint-plugin": "18.0.0", - "@angular-eslint/eslint-plugin-template": "18.0.0", - "@angular-eslint/schematics": "18.0.0", - "@angular-eslint/template-parser": "18.0.0", - "@angular/animations": "^18.0.1", - "@angular/cdk": "^18.0.1", - "@angular/cli": "^18.0.2", - "@angular/common": "^18.0.1", - "@angular/compiler": "^18.0.1", - "@angular/compiler-cli": "^18.0.1", - "@angular/core": "^18.0.1", - "@angular/forms": "^18.0.1", - "@angular/platform-browser": "^18.0.1", - "@angular/platform-browser-dynamic": "^18.0.1", - "@angular/platform-server": "^18.0.1", - "@angular/router": "^18.0.1", - "@angular/ssr": "^18.0.1", - "@docsearch/js": "^3.3.4", - "@stackblitz/sdk": "1.9.0", - "@types/express": "^4.17.17", - "@types/jasmine": "~4.3.1", - "@types/jest": "^29.5.1", - "@types/node": "^16.18.67", - "@types/react": "^18.2.41", - "@typescript-eslint/eslint-plugin": "^7.11.0", - "chart.js": "4.4.2", - "codelyzer": "^0.0.28", - "del": "^7.1.0", - "domino": "^2.1.6", - "esbuild": "^0.19.8", - "eslint": "^8.57.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-import": "latest", - "eslint-plugin-jsdoc": "latest", - "eslint-plugin-prefer-arrow": "latest", - "express": "^4.19.2", - "file-saver": "^2.0.5", - "gulp": "^5.0.0", - "gulp-concat": "^2.6.1", - "gulp-flatten": "^0.4.0", - "gulp-rename": "^2.0.0", - "gulp-uglify": "^3.0.2", - "gulp-uglifycss": "^1.1.0", - "jasmine-core": "~4.6.0", - "jspdf": "^2.5.1", - "jspdf-autotable": "^3.5.28", - "karma": "~6.4.2", - "karma-chrome-launcher": "~3.2.0", - "karma-coverage": "~2.2.0", - "karma-jasmine": "~5.1.0", - "karma-jasmine-html-reporter": "~2.0.0", - "ng-packagr": "^18.0.0", - "prettier": "^3.0.0", - "primeflex": "^3.3.1", - "primeicons": "^7.0.0", - "prismjs": "^1.29.0", - "quill": "2.0.2", - "rxjs": "~7.8.1", - "ts-node": "~10.9.1", - "tslib": "^2.5.0", - "typedoc": "0.25.13", - "typescript": "5.4.5", - "xlsx": "^0.18.5", - "zone.js": "~0.14.0" - } - }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -176,12 +109,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1802.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.1.tgz", - "integrity": "sha512-XTnJfCBMDQl3xF4w/eNrq821gbj2Ig1cqbzpRflhz4pqrANTAfHfPoIC7piWEZ60FNlHapzb6fvh6tJUGXG9og==", + "version": "0.1802.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.5.tgz", + "integrity": "sha512-c7sVoW85Yqj7IYvNKxtNSGS5I7gWpORorg/xxLZX3OkHWXDrwYbb5LN/2p5/Aytxyb0aXl4o5fFOu6CUwcaLUw==", "dev": true, "dependencies": { - "@angular-devkit/core": "18.2.1", + "@angular-devkit/core": "18.2.5", "rxjs": "7.8.1" }, "engines": { @@ -191,16 +124,16 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.1.tgz", - "integrity": "sha512-ANsTWKjIlEvJ6s276TbwnDhkoHhQDfsNiRFUDRGBZu94UNR78ImQZSyKYGHJOeQQH6jpBtraA1rvW5WKozAtlw==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.5.tgz", + "integrity": "sha512-dIvb0AHoRIMM6tLuG4t6lDDslSAYP77wqytodsN317UzFOuuCPernXbO8NJs+QHxj09nPsem1T5vnvpO2E/PVQ==", "dev": true, "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.1", - "@angular-devkit/build-webpack": "0.1802.1", - "@angular-devkit/core": "18.2.1", - "@angular/build": "18.2.1", + "@angular-devkit/architect": "0.1802.5", + "@angular-devkit/build-webpack": "0.1802.5", + "@angular-devkit/core": "18.2.5", + "@angular/build": "18.2.5", "@babel/core": "7.25.2", "@babel/generator": "7.25.0", "@babel/helper-annotate-as-pure": "7.24.7", @@ -211,7 +144,7 @@ "@babel/preset-env": "7.25.3", "@babel/runtime": "7.25.0", "@discoveryjs/json-ext": "0.6.1", - "@ngtools/webpack": "18.2.1", + "@ngtools/webpack": "18.2.5", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", @@ -251,10 +184,10 @@ "terser": "5.31.6", "tree-kill": "1.2.2", "tslib": "2.6.3", - "vite": "5.4.0", + "vite": "5.4.6", "watchpack": "2.4.1", - "webpack": "5.93.0", - "webpack-dev-middleware": "7.3.0", + "webpack": "5.94.0", + "webpack-dev-middleware": "7.4.2", "webpack-dev-server": "5.0.4", "webpack-merge": "6.0.1", "webpack-subresource-integrity": "5.1.0" @@ -318,6 +251,12 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, "node_modules/@angular-devkit/build-angular/node_modules/webpack-merge": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", @@ -333,12 +272,12 @@ } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1802.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.1.tgz", - "integrity": "sha512-xOP9Hxkj/mWYdMTa/8uNxFTv7z+3UiGdt4VAO7vetV5qkU/S9rRq8FEKviCc2llXfwkhInSgeeHpWKdATa+YIQ==", + "version": "0.1802.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.5.tgz", + "integrity": "sha512-6qkcrWBdkxojCVHGWcdJaz4G+7QTjFvmc+3g8xvLc9sYvJq1I059gfXhDnC0FxiA0MT4cY/26ECYWUHTD5CJLQ==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1802.1", + "@angular-devkit/architect": "0.1802.5", "rxjs": "7.8.1" }, "engines": { @@ -352,9 +291,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.1.tgz", - "integrity": "sha512-fSuGj6CxiTFR+yjuVcaWqaVb5Wts39CSBYRO1BlsOlbuWFZ2NKC/BAb5bdxpB31heCBJi7e3XbPvcMMJIcnKlA==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.5.tgz", + "integrity": "sha512-r9TumPlJ8PvA2+yz4sp+bUHgtznaVKzhvXTN5qL1k4YP8LJ7iZWMR2FOP+HjukHZOTsenzmV9pszbogabqwoZQ==", "dev": true, "dependencies": { "ajv": "8.17.1", @@ -379,12 +318,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.1.tgz", - "integrity": "sha512-2t/q0Jcv7yqhAzEdNgsxoGSCmPgD4qfnVOJ7EJw3LNIA+kX1CmtN4FESUS0i49kN4AyNJFAI5O2pV8iJiliKaw==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.5.tgz", + "integrity": "sha512-NUmz2UQ1Xl4cf4j1AgkwIfsCjBzAPgfeC3IBrD29hSOBE1Y3j6auqjBkvw50v6mbSPxESND995Xy13HpK1Xflw==", "dev": true, "dependencies": { - "@angular-devkit/core": "18.2.1", + "@angular-devkit/core": "18.2.5", "jsonc-parser": "3.3.1", "magic-string": "0.30.11", "ora": "5.4.1", @@ -397,9 +336,9 @@ } }, "node_modules/@angular/animations": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.0.tgz", - "integrity": "sha512-BFAfqDDjsa0Q91F4s33pFcBG+ydFDurEmwYGG1gmO7UXZJI6ZbRVdULaZHz75M+Bf3hJkzVB05pesvfbK+Fg/g==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.5.tgz", + "integrity": "sha512-IlXtW/Nj48ZzjHUzH1TykZcSR64ScJx39T3IHnjV2z/bVATzZ36JGoadQHdqpJNKBodYJNgtJCGLCbgAvGWY2g==", "dependencies": { "tslib": "^2.3.0" }, @@ -407,17 +346,17 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.0" + "@angular/core": "18.2.5" } }, "node_modules/@angular/build": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.1.tgz", - "integrity": "sha512-HwzjB+I31cAtjTTbbS2NbayzfcWthaKaofJlSmZIst3PN+GwLZ8DU0DRpd/xu5AXkk+DoAIWd+lzUIaqngz6ow==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.5.tgz", + "integrity": "sha512-XWkmjzgeUga0SJ0lYSYcTuYOWTyqcln2mNfBp7Ae/GZ+/7+APbedsIZEiZGZwveOIyOpTM5wguNSoe9khDl5Ig==", "dev": true, "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.1", + "@angular-devkit/architect": "0.1802.5", "@babel/core": "7.25.2", "@babel/helper-annotate-as-pure": "7.24.7", "@babel/helper-split-export-declaration": "7.24.7", @@ -439,7 +378,7 @@ "rollup": "4.20.0", "sass": "1.77.6", "semver": "7.6.3", - "vite": "5.4.0", + "vite": "5.4.6", "watchpack": "2.4.1" }, "engines": { @@ -479,17 +418,17 @@ } }, "node_modules/@angular/cli": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.1.tgz", - "integrity": "sha512-SomUFDHanY4o7k3XBGf1eFt4z1h05IGJHfcbl2vxoc0lY59VN13m/pZsD2AtpqtJTzLQT02XQOUP4rmBbGoQ+Q==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.5.tgz", + "integrity": "sha512-97uNs0HsOdnMaTlNJKFjIBUXw0wz43uYvSSKmIpBt7eq1LaPLju1G/qpDIHx2YwhMClPrXXrW2H/xdvqZiIw+w==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1802.1", - "@angular-devkit/core": "18.2.1", - "@angular-devkit/schematics": "18.2.1", + "@angular-devkit/architect": "0.1802.5", + "@angular-devkit/core": "18.2.5", + "@angular-devkit/schematics": "18.2.5", "@inquirer/prompts": "5.3.8", "@listr2/prompt-adapter-inquirer": "2.0.15", - "@schematics/angular": "18.2.1", + "@schematics/angular": "18.2.5", "@yarnpkg/lockfile": "1.1.0", "ini": "4.1.3", "jsonc-parser": "3.3.1", @@ -512,9 +451,9 @@ } }, "node_modules/@angular/common": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.0.tgz", - "integrity": "sha512-DELx/QYNqqjmiM+kE5PoVmyG4gPw5WB1bDDeg3hEuBCK3eS2KosgQH0/MQo3OSBZHOcAMFjfHMGqKgxndmYixQ==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.5.tgz", + "integrity": "sha512-m+KJrtbFXTE36jP/po6UAMeUR/enQxRHpVGLCRcIcE7VWVH1ZcOvoW1yqh2A6k+KxWXeajlq/Z04nnMhcoxMRw==", "dependencies": { "tslib": "^2.3.0" }, @@ -522,14 +461,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.0", + "@angular/core": "18.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.0.tgz", - "integrity": "sha512-RmGwQ7jRzotUKKMk0CwxTcIXFr5mRxSbJf9o5S3ujuIOo1lYop8SQZvjq67a5BuoYyD+1pX6iMmxZqlbKoihBQ==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.5.tgz", + "integrity": "sha512-vcqe9x4dGGAnMfPhEpcZyiSVgAiqJeK80LqP1vWoAmBR+HeOqAilSv6SflcLAtuTzwgzMMAvD2T+SMCgUvaqww==", "dependencies": { "tslib": "^2.3.0" }, @@ -537,7 +476,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.0" + "@angular/core": "18.2.5" }, "peerDependenciesMeta": { "@angular/core": { @@ -546,9 +485,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.0.tgz", - "integrity": "sha512-pPBFjMqNTNettrleLtEc6a/ysOZjG3F0Z5lyKYePcyNQmn2rpa9XmD2y6PhmzTmIhxeXrogWA84Xgg/vK5wBNw==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.5.tgz", + "integrity": "sha512-CCCtZobUTUfId/RTYtuDCw5R1oK0w65hdAUMRP1MdGmd8bb8DKJA86u1QCWwozL3rbXlIIX4ognQ6urQ43k/Gw==", "dev": true, "dependencies": { "@babel/core": "7.25.2", @@ -569,14 +508,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.0", + "@angular/compiler": "18.2.5", "typescript": ">=5.4 <5.6" } }, "node_modules/@angular/core": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.0.tgz", - "integrity": "sha512-7+4wXfeAk1TnG3MGho2gpBI5XHxeSRWzLK2rC5qyyRbmMV+GrIgf1HqFjQ4S02rydkQvGpjqQHtO1PYJnyn4bg==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.5.tgz", + "integrity": "sha512-5BLVc5gXxzanQkADNS9WPsor3vNF5nQcyIHBi5VScErwM5vVZ7ATH1iZwaOg1ykDEVTFVhKDwD0X1aaqGDbhmQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -589,9 +528,9 @@ } }, "node_modules/@angular/forms": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.0.tgz", - "integrity": "sha512-G+4BjNCUo4cRwg9NwisGLbtG/1AbIJNOO3RUejPJJbCcAkYMSzmDWSQ+LQEGW4vC/1xaDKO8AT71DI/I09bOxA==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.5.tgz", + "integrity": "sha512-ohKeH+EZCCIyGSiFYlraWLzssGAZc13P92cuYpXB62322PkcA5u0IT72mML9JWGKRqF2zteVsw4koWHVxXM5mA==", "dependencies": { "tslib": "^2.3.0" }, @@ -599,16 +538,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.0", - "@angular/core": "18.2.0", - "@angular/platform-browser": "18.2.0", + "@angular/common": "18.2.5", + "@angular/core": "18.2.5", + "@angular/platform-browser": "18.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/platform-browser": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.0.tgz", - "integrity": "sha512-yhj281TuPz5a8CehwucwIVl29Oqte9KS4X/VQkMV++GpYQE2KKKcoff4FXSdF5RUcUYkK2li4IvawIqPmUSagg==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.5.tgz", + "integrity": "sha512-PoX9idwnOpTJBlujzZ2nFGOsmCnZzOH7uNSWIR7trdoq0b1AFXfrxlCQ36qWamk7bbhJI4H28L8YTmKew/nXDA==", "dependencies": { "tslib": "^2.3.0" }, @@ -616,9 +555,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "18.2.0", - "@angular/common": "18.2.0", - "@angular/core": "18.2.0" + "@angular/animations": "18.2.5", + "@angular/common": "18.2.5", + "@angular/core": "18.2.5" }, "peerDependenciesMeta": { "@angular/animations": { @@ -627,9 +566,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.0.tgz", - "integrity": "sha512-izfaXKNC/kqOEzJG8eTnFu39VLI3vv3dTKoYOdEKRB7MTI0t0x66oZmABnHcihtkTSvXs/is+7lA5HmH2ZuIEQ==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.5.tgz", + "integrity": "sha512-5u0IuAt1r5e2u2vSKhp3phnaf6hH89B/q7GErfPse1sdDfNI6wHVppxai28PAfAj9gwooJun6MjFWhJFLzS44A==", "dependencies": { "tslib": "^2.3.0" }, @@ -637,16 +576,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.0", - "@angular/compiler": "18.2.0", - "@angular/core": "18.2.0", - "@angular/platform-browser": "18.2.0" + "@angular/common": "18.2.5", + "@angular/compiler": "18.2.5", + "@angular/core": "18.2.5", + "@angular/platform-browser": "18.2.5" } }, "node_modules/@angular/router": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.0.tgz", - "integrity": "sha512-6/462hvC3HSwbps8VItECHpkdkiFqRmTKdn1Trik+FjnvdupYrKB6kBsveM3eP+gZD4zyMBMKzBWB9N/xA1clw==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.5.tgz", + "integrity": "sha512-OjZV1PTiSwT0ytmR0ykveLYzs4uQWf0EuIclZmWqM/bb8Q4P+gJl7/sya05nGnZsj6nHGOL0e/LhSZ3N+5p6qg==", "dependencies": { "tslib": "^2.3.0" }, @@ -654,9 +593,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.0", - "@angular/core": "18.2.0", - "@angular/platform-browser": "18.2.0", + "@angular/common": "18.2.5", + "@angular/core": "18.2.5", + "@angular/platform-browser": "18.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -674,9 +613,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", - "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -793,9 +732,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.0.tgz", - "integrity": "sha512-GYM6BxeQsETc9mnct+nIIpf63SAyzvyYN7UB/IlTyd+MBg06afFGp0mIeUqGyWgS2mxad6vqbMrHVlaL3m70sQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz", + "integrity": "sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", @@ -803,7 +742,7 @@ "@babel/helper-optimise-call-expression": "^7.24.7", "@babel/helper-replace-supers": "^7.25.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/traverse": "^7.25.0", + "@babel/traverse": "^7.25.4", "semver": "^6.3.1" }, "engines": { @@ -1043,13 +982,13 @@ } }, "node_modules/@babel/helpers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", - "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", + "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", "dev": true, "dependencies": { "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/types": "^7.25.6" }, "engines": { "node": ">=6.9.0" @@ -1071,12 +1010,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", - "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", "dev": true, "dependencies": { - "@babel/types": "^7.25.2" + "@babel/types": "^7.25.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -1240,12 +1179,12 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", - "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.6.tgz", + "integrity": "sha512-aABl0jHw9bZ2karQ/uUD6XP4u0SG22SJrOHFoL6XB1R7dTovOP4TzTlsxOYC5yQ1pdscVK2JTUnF6QL3ARoAiQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1492,13 +1431,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", - "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz", + "integrity": "sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-class-features-plugin": "^7.25.4", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1525,16 +1464,16 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.0.tgz", - "integrity": "sha512-xyi6qjr/fYU304fiRwFbekzkqVJZ6A7hOjWZd+89FVcBqPV3S9Wuozz82xdpLspckeaafntbzglaW4pqpzvtSw==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.4.tgz", + "integrity": "sha512-oexUfaQle2pF/b6E0dwsxQtAol9TLSO88kQvym6HHBWFliV2lGdrPieX+WgMRLSJDVzdYywk7jXbLPuO2KLTLg==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-compilation-targets": "^7.25.2", "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-replace-supers": "^7.25.0", - "@babel/traverse": "^7.25.0", + "@babel/traverse": "^7.25.4", "globals": "^11.1.0" }, "engines": { @@ -1544,6 +1483,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", @@ -1978,13 +1926,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", - "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.4.tgz", + "integrity": "sha512-ao8BG7E2b/URaUQGqN3Tlsg+M3KlHY6rJ1O1gXAEUnZoyNQnvKyH87Kfg+FoxSeyWUB8ISZZsC91C44ZuBFytw==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-class-features-plugin": "^7.25.4", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -2210,13 +2158,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", - "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz", + "integrity": "sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.2", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -2378,16 +2326,16 @@ } }, "node_modules/@babel/traverse": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", - "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", "@babel/template": "^7.25.0", - "@babel/types": "^7.25.2", + "@babel/types": "^7.25.6", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2395,10 +2343,34 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.6", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/types": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", - "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.24.8", @@ -2410,9 +2382,9 @@ } }, "node_modules/@cesium/engine": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@cesium/engine/-/engine-8.0.0.tgz", - "integrity": "sha512-0HvvpoKPrb1Go6MnKsTk0Vn2HV4wv+UAlZGbzG3sD/QjEm4FOYj/sLFrwQqmCn6nFN1aRbhAH1xXJnA9bF+1Wg==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@cesium/engine/-/engine-10.1.0.tgz", + "integrity": "sha512-xwdJEhGYgf6481vhrb80N5DgQZMwWvn08TWE6NXEgOhkZ7WnTCykYoCDNBMj9WQBqTfREk7/e+/RI4Gx2/TlUA==", "dependencies": { "@tweenjs/tween.js": "^23.1.1", "@zip.js/zip.js": "^2.7.34", @@ -2420,17 +2392,17 @@ "bitmap-sdf": "^1.0.3", "dompurify": "^3.0.2", "draco3d": "^1.5.1", - "earcut": "^2.2.4", + "earcut": "^3.0.0", "grapheme-splitter": "^1.0.4", "jsep": "^1.3.8", "kdbush": "^4.0.1", - "ktx-parse": "^0.6.0", + "ktx-parse": "^0.7.0", "lerc": "^2.0.0", "mersenne-twister": "^1.1.0", - "meshoptimizer": "^0.20.0", + "meshoptimizer": "^0.21.0", "pako": "^2.0.4", "protobufjs": "^7.1.0", - "rbush": "^3.0.1", + "rbush": "^4.0.0", "topojson-client": "^3.1.0", "urijs": "^1.19.7" }, @@ -2444,11 +2416,11 @@ "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" }, "node_modules/@cesium/widgets": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@cesium/widgets/-/widgets-5.0.0.tgz", - "integrity": "sha512-004x7F5F8CHFnhWkuRbOrgduOug8q36/fqegs4UMdzgxOG9zNXfJoZInD6jwytCZJHH7aVcZvIoTKAdsS8bNKQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@cesium/widgets/-/widgets-7.1.0.tgz", + "integrity": "sha512-SZCtaByBrBTssyUpg0Nir34B4wvvu8bKOMOOevv0AzYxfMeYRBX8CH/Ck/5fUJcTcsVmcYHVOqBF339wwKtcag==", "dependencies": { - "@cesium/engine": "^8.0.0", + "@cesium/engine": "^10.1.0", "nosleep.js": "^0.12.0" }, "engines": { @@ -2456,9 +2428,9 @@ } }, "node_modules/@codemirror/autocomplete": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.0.tgz", - "integrity": "sha512-5DbOvBbY4qW5l57cjDsmmpDh3/TeK1vXfTHa+BUMrRzdWdcxKZ4U4V7vQaTtOpApNU4kLS4FQ6cINtLg245LXA==", + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.1.tgz", + "integrity": "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA==", "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -2473,9 +2445,9 @@ } }, "node_modules/@codemirror/commands": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz", - "integrity": "sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.2.tgz", + "integrity": "sha512-Fq7eWOl1Rcbrfn6jD8FPCj9Auaxdm5nIK5RYOeW7ughnd/rY5AmPg6b+CfsG39ZHdwiwe8lde3q8uR7CF5S0yQ==", "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", @@ -2535,9 +2507,9 @@ "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" }, "node_modules/@codemirror/view": { - "version": "6.32.0", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.32.0.tgz", - "integrity": "sha512-AgVNvED2QTsZp5e3syoHLsrWtwJFYWdx1Vr/m3f4h1ATQz0ax60CfXF3Htdmk69k2MlYZw8gXesnQdHtzyVmAw==", + "version": "6.33.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.33.0.tgz", + "integrity": "sha512-AroaR3BvnjRW8fiZBalAaK+ZzB5usGgI014YKElYZvQdNH5ZIidHlO+cyf/2rWzyBFRkvG6VhiXeAEbC53P2YQ==", "dependencies": { "@codemirror/state": "^6.4.0", "style-mod": "^4.1.0", @@ -2984,24 +2956,38 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -3009,7 +2995,7 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3031,26 +3017,13 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3062,74 +3035,34 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz", + "integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "deprecated": "Use @eslint/config-array instead", + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, "engines": { - "node": ">=10.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/@eslint/plugin-kit": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz", + "integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "levn": "^0.4.1" }, "engines": { - "node": "*" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -3145,22 +3078,28 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@inquirer/checkbox": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.4.7.tgz", - "integrity": "sha512-5YwCySyV1UEgqzz34gNsC38eKxRBtlRDpJLlKcRtTjlYA/yDKuc1rfw+hjw+2WJxbAZtaDPsRl5Zk7J14SBoBw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.5.0.tgz", + "integrity": "sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==", "dev": true, "dependencies": { - "@inquirer/core": "^9.0.10", + "@inquirer/core": "^9.1.0", "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", + "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -3182,18 +3121,17 @@ } }, "node_modules/@inquirer/core": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.0.10.tgz", - "integrity": "sha512-TdESOKSVwf6+YWDz8GhS6nKscwzkIyakEzCLJ5Vh6O3Co2ClhCJ0A4MG909MUWfaWdpJm7DE45ii51/2Kat9tA==", + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", "dev": true, "dependencies": { - "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", "@types/mute-stream": "^0.0.4", - "@types/node": "^22.1.0", + "@types/node": "^22.5.5", "@types/wrap-ansi": "^3.0.0", "ansi-escapes": "^4.3.2", - "cli-spinners": "^2.9.2", "cli-width": "^4.1.0", "mute-stream": "^1.0.0", "signal-exit": "^4.1.0", @@ -3205,14 +3143,26 @@ "node": ">=18" } }, + "node_modules/@inquirer/core/node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "dev": true, + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@inquirer/editor": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-2.1.22.tgz", - "integrity": "sha512-K1QwTu7GCK+nKOVRBp5HY9jt3DXOfPGPr6WRDrPImkcJRelG9UTx2cAtK1liXmibRrzJlTWOwqgWT3k2XnS62w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-2.2.0.tgz", + "integrity": "sha512-9KHOpJ+dIL5SZli8lJ6xdaYLPPzB8xB9GZItg39MBybzhxA16vxmszmQFrRwbOA918WA2rvu8xhDEg/p6LXKbw==", "dev": true, "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", "external-editor": "^3.1.0" }, "engines": { @@ -3220,13 +3170,13 @@ } }, "node_modules/@inquirer/expand": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-2.1.22.tgz", - "integrity": "sha512-wTZOBkzH+ItPuZ3ZPa9lynBsdMp6kQ9zbjVPYEtSBG7UulGjg2kQiAnUjgyG4SlntpTce5bOmXAPvE4sguXjpA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-2.3.0.tgz", + "integrity": "sha512-qnJsUcOGCSG1e5DTOErmv2BPQqrtT6uzqn1vI/aYGiPKq+FgslGZmtdnXbhuI7IlT7OByDoEEqdnhUnVR2hhLw==", "dev": true, "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -3234,48 +3184,48 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.5.tgz", - "integrity": "sha512-79hP/VWdZ2UVc9bFGJnoQ/lQMpL74mGgzSYX1xUqCVk7/v73vJCMw1VuyWN1jGkZ9B3z7THAbySqGbCNefcjfA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.6.tgz", + "integrity": "sha512-yfZzps3Cso2UbM7WlxKwZQh2Hs6plrbjs1QnzQDZhK2DgyCo6D8AaHps9olkNcUFlcYERMqU3uJSp1gmy3s/qQ==", "dev": true, "engines": { "node": ">=18" } }, "node_modules/@inquirer/input": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.2.9.tgz", - "integrity": "sha512-7Z6N+uzkWM7+xsE+3rJdhdG/+mQgejOVqspoW+w0AbSZnL6nq5tGMEVASaYVWbkoSzecABWwmludO2evU3d31g==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.3.0.tgz", + "integrity": "sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw==", "dev": true, "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2" + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" }, "engines": { "node": ">=18" } }, "node_modules/@inquirer/number": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-1.0.10.tgz", - "integrity": "sha512-kWTxRF8zHjQOn2TJs+XttLioBih6bdc5CcosXIzZsrTY383PXI35DuhIllZKu7CdXFi2rz2BWPN9l0dPsvrQOA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-1.1.0.tgz", + "integrity": "sha512-ilUnia/GZUtfSZy3YEErXLJ2Sljo/mf9fiKc08n18DdwdmDbOzRcTv65H1jjDvlsAuvdFXf4Sa/aL7iw/NanVA==", "dev": true, "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2" + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" }, "engines": { "node": ">=18" } }, "node_modules/@inquirer/password": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-2.1.22.tgz", - "integrity": "sha512-5Fxt1L9vh3rAKqjYwqsjU4DZsEvY/2Gll+QkqR4yEpy6wvzLxdSgFhUcxfDAOtO4BEoTreWoznC0phagwLU5Kw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-2.2.0.tgz", + "integrity": "sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg==", "dev": true, "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2" }, "engines": { @@ -3304,13 +3254,13 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-2.2.4.tgz", - "integrity": "sha512-pb6w9pWrm7EfnYDgQObOurh2d2YH07+eDo3xQBsNAM2GRhliz6wFXGi1thKQ4bN6B0xDd6C3tBsjdr3obsCl3Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-2.3.0.tgz", + "integrity": "sha512-zzfNuINhFF7OLAtGHfhwOW2TlYJyli7lOUoJUXw/uyklcwalV6WRXBXtFIicN8rTRK1XTiPWB4UY+YuW8dsnLQ==", "dev": true, "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -3318,14 +3268,14 @@ } }, "node_modules/@inquirer/search": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-1.0.7.tgz", - "integrity": "sha512-p1wpV+3gd1eST/o5N3yQpYEdFNCzSP0Klrl+5bfD3cTTz8BGG6nf4Z07aBW0xjlKIj1Rp0y3x/X4cZYi6TfcLw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-1.1.0.tgz", + "integrity": "sha512-h+/5LSj51dx7hp5xOn4QFnUaKeARwUCLs6mIhtkJ0JYPBLmEYjdHSYh7I6GrLg9LwpJ3xeX0FZgAG1q0QdCpVQ==", "dev": true, "dependencies": { - "@inquirer/core": "^9.0.10", + "@inquirer/core": "^9.1.0", "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", + "@inquirer/type": "^1.5.3", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -3333,14 +3283,14 @@ } }, "node_modules/@inquirer/select": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.4.7.tgz", - "integrity": "sha512-JH7XqPEkBpNWp3gPCqWqY8ECbyMoFcCZANlL6pV9hf59qK6dGmkOlx1ydyhY+KZ0c5X74+W6Mtp+nm2QX0/MAQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.5.0.tgz", + "integrity": "sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA==", "dev": true, "dependencies": { - "@inquirer/core": "^9.0.10", + "@inquirer/core": "^9.1.0", "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", + "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -3349,9 +3299,9 @@ } }, "node_modules/@inquirer/type": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.2.tgz", - "integrity": "sha512-w9qFkumYDCNyDZmNQjf/n6qQuvQ4dMC3BJesY4oF+yr0CxR5vxujflAVeIcS6U336uzi9GM0kAfZlLrZ9UTkpA==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", "dev": true, "dependencies": { "mute-stream": "^1.0.0" @@ -3378,9 +3328,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "engines": { "node": ">=12" @@ -3786,9 +3736,9 @@ ] }, "node_modules/@ngtools/webpack": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.1.tgz", - "integrity": "sha512-v86U3jOoy5R9ZWe9Q0LbHRx/IBw1lbn0ldBU+gIIepREyVvb9CcH/vAyIb2Fw1zaYvvfG1OyzdrHyW8iGXjdnQ==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.5.tgz", + "integrity": "sha512-L0n4eHObeqEOYRfSP+e4SeF/dmwxOIFy9xYvYCOUwOLrW4b3+a1+kkT30pqyfL72LFtpf0cmUwaWEFIcWl5PCg==", "dev": true, "engines": { "node": "^18.19.1 || ^20.11.1 || >=22.0.0", @@ -3801,6 +3751,30 @@ "webpack": "^5.54.0" } }, + "node_modules/@ngx-formly/core": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-6.3.7.tgz", + "integrity": "sha512-To2mH09YSm3nyThABNHIameIJCPA9C+x3/JFxFtBWek+UbYeW9DYOqNHRCc7P1ToqLqNEuwrmzjB2YSA8pO9Pw==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/forms": ">=13.2.0", + "rxjs": "^6.5.3 || ^7.0.0" + } + }, + "node_modules/@ngx-formly/primeng": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/@ngx-formly/primeng/-/primeng-6.3.7.tgz", + "integrity": "sha512-1eQUoZLVAo3Jo8Xk5iPWxAyKCmF/zK6BFdm13K1DvJDaGTFjkEQtZnhJup0dET6lB6inNbmMCNRoKFCjnD9HdA==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@ngx-formly/core": "6.3.7", + "primeng": ">=13.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3946,9 +3920,9 @@ } }, "node_modules/@npmcli/package-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.2.0.tgz", - "integrity": "sha512-qe/kiqqkW0AGtvBjL8TJKZk/eBBSpnJkUWvHdQ9jM2lKHXRYYJuyNpJPlJw3c8QjC2ow6NZYiLExhUaeJelbxQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.2.1.tgz", + "integrity": "sha512-f7zYC6kQautXHvNbLEWgD/uGu1+xCn9izgqBfgItWSx22U0ZDekxN08A1vM8cTxj/cRVe0Q94Ode+tdoYmIOOQ==", "dev": true, "dependencies": { "@npmcli/git": "^5.0.0", @@ -3963,6 +3937,15 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/@npmcli/package-json/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/@npmcli/package-json/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -3983,6 +3966,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@npmcli/package-json/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@npmcli/promise-spawn": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", @@ -4342,13 +4340,13 @@ ] }, "node_modules/@schematics/angular": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.1.tgz", - "integrity": "sha512-bBV7I+MCbdQmBPUFF4ECg37VReM0+AdQsxgwkjBBSYExmkErkDoDgKquwL/tH7stDCc5IfTd0g9BMeosRgDMug==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.5.tgz", + "integrity": "sha512-tBXhk9OGT4U6VsBNbuCNl2ITDOF3NYdGrEieIHU+lHSkpJNGZUIGxCgXCETXkmXDq1pe4wFZSKelWjeqYDfX0g==", "dev": true, "dependencies": { - "@angular-devkit/core": "18.2.1", - "@angular-devkit/schematics": "18.2.1", + "@angular-devkit/core": "18.2.5", + "@angular-devkit/schematics": "18.2.5", "jsonc-parser": "3.3.1" }, "engines": { @@ -4495,6 +4493,30 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@tweenjs/tween.js": { "version": "23.1.3", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", @@ -4553,26 +4575,6 @@ "@types/node": "*" } }, - "node_modules/@types/eslint": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.0.tgz", - "integrity": "sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -4619,9 +4621,9 @@ } }, "node_modules/@types/jasmine": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.6.tgz", - "integrity": "sha512-3N0FpQTeiWjm+Oo1WUYWguUS7E6JLceiGTriFrG8k5PU7zRLJCzLcWURU3wjMbZGS//a2/LgjsnO3QxIlwxt9g==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.4.tgz", + "integrity": "sha512-px7OMFO/ncXxixDe1zR13V1iycqWae0MxTaw62RpFlksUi5QuNWgQJFkTQjIOvrmutJbI7Fp2Y2N1F6D2R4G6w==", "dev": true }, "node_modules/@types/js-yaml": { @@ -4652,9 +4654,9 @@ } }, "node_modules/@types/node": { - "version": "22.4.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.2.tgz", - "integrity": "sha512-nAvM3Ey230/XzxtyDcJ+VjvlzpzoHwLsF7JaDRfoI0ytO0mVheerNmM45CtA0yOILXwXXxOrcUWH3wltX+7PSw==", + "version": "22.5.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", + "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==", "dependencies": { "undici-types": "~6.19.2" } @@ -4669,9 +4671,9 @@ } }, "node_modules/@types/qs": { - "version": "6.9.15", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", "dev": true }, "node_modules/@types/range-parser": { @@ -4741,31 +4743,31 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", - "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz", + "integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/type-utils": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/type-utils": "8.6.0", + "@typescript-eslint/utils": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -4774,26 +4776,26 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", - "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz", + "integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/typescript-estree": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -4802,16 +4804,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", - "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz", + "integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0" + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -4819,26 +4821,23 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", - "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz", + "integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/typescript-estree": "8.6.0", + "@typescript-eslint/utils": "8.6.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "eslint": "^8.56.0" - }, "peerDependenciesMeta": { "typescript": { "optional": true @@ -4846,12 +4845,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", - "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz", + "integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==", "dev": true, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -4859,22 +4858,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", - "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz", + "integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -4886,51 +4885,69 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", - "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz", + "integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0" + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/typescript-estree": "8.6.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", - "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz", + "integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/types": "8.6.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", @@ -5108,9 +5125,9 @@ "dev": true }, "node_modules/@zip.js/zip.js": { - "version": "2.7.48", - "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.48.tgz", - "integrity": "sha512-J7cliimZ2snAbr0IhLx2U8BwfA1pKucahKzTpFtYq4hEgKxwvFJcIjCIVNPwQpfVab7iVP+AKmoH1gidBlyhiQ==", + "version": "2.7.52", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.52.tgz", + "integrity": "sha512-+5g7FQswvrCHwYKNMd/KFxZSObctLSsQOgqBSi0LzwHo3li9Eh1w5cF5ndjQw9Zbr3ajVnd2+XyiX85gAetx1Q==", "engines": { "bun": ">=0.7.0", "deno": ">=1.0.0", @@ -5170,9 +5187,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", - "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "dev": true, "dependencies": { "acorn": "^8.11.0" @@ -5377,15 +5394,6 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "dev": true }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/assert": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", @@ -5601,9 +5609,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, "dependencies": { "bytes": "3.1.2", @@ -5614,7 +5622,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -5656,12 +5664,13 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { @@ -5798,6 +5807,15 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/cacache/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -5824,6 +5842,21 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -5852,9 +5885,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001651", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", - "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "version": "1.0.30001660", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", + "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==", "dev": true, "funding": [ { @@ -5872,19 +5905,15 @@ ] }, "node_modules/cesium": { - "version": "1.115.0", - "resolved": "https://registry.npmjs.org/cesium/-/cesium-1.115.0.tgz", - "integrity": "sha512-aIC+JWO+0W/WDOAR1KIN+hEAUCZ98qUTQba7cPhW0fvMKiRVblDT1IJmZlb4dGBDtUGc0ZhwMXk2KpfM8BSztQ==", - "workspaces": [ - "packages/engine", - "packages/widgets" - ], + "version": "1.120.0", + "resolved": "https://registry.npmjs.org/cesium/-/cesium-1.120.0.tgz", + "integrity": "sha512-1TkuCgWhhZ+TlNM4Hps08xb+TyNwChkR8MiYNtFior8XIglEvdh+JgPqdI+yfd9M02bjV13HDj5D6muIYsW4uw==", "dependencies": { - "@cesium/engine": "^8.0.0", - "@cesium/widgets": "^5.0.0" + "@cesium/engine": "^10.1.0", + "@cesium/widgets": "^7.1.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.18.0" } }, "node_modules/chalk": { @@ -6367,50 +6396,6 @@ "node": ">=10.13.0" } }, - "node_modules/copy-webpack-plugin/node_modules/globby": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", - "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", - "dev": true, - "dependencies": { - "@sindresorhus/merge-streams": "^2.1.0", - "fast-glob": "^3.3.2", - "ignore": "^5.2.4", - "path-type": "^5.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/copy-webpack-plugin/node_modules/path-type": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", - "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/copy-webpack-plugin/node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/core-js-compat": { "version": "3.38.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", @@ -6670,12 +6655,12 @@ } }, "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -6837,18 +6822,6 @@ "node": ">=0.3.1" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -6861,18 +6834,6 @@ "node": ">=6" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dom-serialize": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", @@ -6951,9 +6912,9 @@ "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==" }, "node_modules/earcut": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", - "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.0.tgz", + "integrity": "sha512-41Fs7Q/PLq1SDbqjsgcY7GA42T0jvaCNGXgGtsNdvg+Yv8eIu06bxv4/PoREkZ9nMDNwnUSG9OFB9+yv8eKhDg==" }, "node_modules/eastasianwidth": { "version": "0.2.0", @@ -6968,15 +6929,15 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.5.13", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", - "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==", + "version": "1.5.25", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.25.tgz", + "integrity": "sha512-kMb204zvK3PsSlgvvwzI3wBIcAw15tRkYk+NQdsjdDtcQWTp2RABbMQ9rUBy8KNEOM+/E6ep+XC3AykiWZld4g==", "dev": true }, "node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true }, "node_modules/emojis-list": { @@ -7213,9 +7174,9 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" @@ -7237,43 +7198,39 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz", + "integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.10.0", + "@eslint/plugin-kit": "^0.1.0", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", @@ -7285,23 +7242,31 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -7350,16 +7315,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -7406,6 +7361,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -7418,21 +7385,6 @@ "node": ">=10.13.0" } }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -7448,18 +7400,6 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -7472,30 +7412,30 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", "dev": true, + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -7618,37 +7558,37 @@ "dev": true }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -7677,14 +7617,23 @@ "ms": "2.0.0" } }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/express/node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -7792,15 +7741,15 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { @@ -7902,17 +7851,16 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { @@ -7922,9 +7870,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "dev": true, "funding": [ { @@ -8149,52 +8097,33 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", "dev": true, "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", "dev": true, "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -8590,6 +8519,30 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/image-size": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", @@ -8735,9 +8688,9 @@ } }, "node_modules/is-core-module": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", - "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "dependencies": { "hasown": "^2.0.2" @@ -9117,9 +9070,9 @@ } }, "node_modules/jasmine-core": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", - "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.2.0.tgz", + "integrity": "sha512-tSAtdrvWybZkQmmaIoDgnvHG8ORUNw5kEVlO5CvrXj02Jjr9TZrmjFq7FUiOUzJiOP2wLGYT6PgrQgQF4R1xiw==", "dev": true }, "node_modules/jest-worker": { @@ -9351,16 +9304,6 @@ "node": ">=10.0.0" } }, - "node_modules/karma-coverage/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", @@ -9377,18 +9320,6 @@ "node": ">=8" } }, - "node_modules/karma-coverage/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/karma-coverage/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -9414,16 +9345,22 @@ } }, "node_modules/karma-jasmine-html-reporter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.0.0.tgz", - "integrity": "sha512-SB8HNNiazAHXM1vGEzf8/tSyEhkfxuDdhYdPBX2Mwgzt0OuF2gicApQ+uvXLID/gXyJQgvrM9+1/2SxZFUUDIA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz", + "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==", "dev": true, "peerDependencies": { - "jasmine-core": "^4.0.0", + "jasmine-core": "^4.0.0 || ^5.0.0", "karma": "^6.0.0", "karma-jasmine": "^5.0.0" } }, + "node_modules/karma-jasmine/node_modules/jasmine-core": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", + "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", + "dev": true + }, "node_modules/karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -9448,16 +9385,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/karma/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/karma/node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -9502,18 +9429,6 @@ "node": ">=8" } }, - "node_modules/karma/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/karma/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -9614,14 +9529,14 @@ } }, "node_modules/ktx-parse": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-0.6.0.tgz", - "integrity": "sha512-hYOJUI86N9+YPm0M3t8hVzW9t5FnFFibRalZCrqHs/qM2eNziqQzBtAaF0ErgkXm8F+5uE8CjPUYr32vWlXLkQ==" + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-0.7.1.tgz", + "integrity": "sha512-FeA3g56ksdFNwjXJJsc1CCc7co+AJYDp6ipIp878zZ2bU8kWROatLYf39TQEd4/XRSUvBXovQ8gaVKWPXsCLEQ==" }, "node_modules/launch-editor": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.8.1.tgz", - "integrity": "sha512-elBx2l/tp9z99X5H/qev8uyDywVh0VXAwEbjk8kJhnc5grOFkGh7aW6q55me9xnYbss261XtnUrysZ+XvGbhQA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", "dev": true, "dependencies": { "picocolors": "^1.0.0", @@ -9786,9 +9701,9 @@ } }, "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "engines": { "node": ">=12" @@ -10044,9 +9959,9 @@ } }, "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "engines": { "node": ">=12" @@ -10228,9 +10143,9 @@ } }, "node_modules/memfs": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.11.1.tgz", - "integrity": "sha512-LZcMTBAgqUUKNXZagcZxvXXfgF1bHX7Y7nQ0QyEiNbRJgE29GhgPd8Yna1VQcLlPiHt/5RFJMWYN9Uv/VPNvjQ==", + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.11.2.tgz", + "integrity": "sha512-VcR7lEtgQgv7AxGkrNNeUAimFLT+Ov8uGu1LuOfbe/iF/dKoh/QgpoaMZlhfejvLtMxtXYyeoT7Ar1jEbWdbPA==", "dev": true, "dependencies": { "@jsonjoy.com/json-pack": "^1.0.3", @@ -10247,10 +10162,13 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -10273,9 +10191,9 @@ "integrity": "sha512-mUYWsMKNrm4lfygPkL3OfGzOPTR2DBlTkBNHM//F6hGp8cLThY897crAlk3/Jo17LEOOjQUrNAx6DvgO77QJkA==" }, "node_modules/meshoptimizer": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.20.0.tgz", - "integrity": "sha512-olcJ1q+YVnjroRJpCL1Dj5aZxr2JMr2hRutMUwhuHZvpAL7SIZgOT6eMlFF4TbBGSR89tawE/gqB79J/LrW/Nw==" + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.21.0.tgz", + "integrity": "sha512-WabtlpnK/GgD0GMwYd1fBTfYHf4MIcQPEg6dt7y4GuDcY51RzLSkSNE8ZogD7U3Vs2/fIf4z89TOLpA80EOnhg==" }, "node_modules/methods": { "version": "1.1.2", @@ -10287,9 +10205,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { "braces": "^3.0.3", @@ -10392,18 +10310,15 @@ "dev": true }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "*" } }, "node_modules/minimist": { @@ -10596,9 +10511,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "node_modules/msgpackr": { @@ -10785,9 +10700,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", - "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", + "integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==", "dev": true, "optional": true, "bin": { @@ -10810,6 +10725,15 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/node-gyp/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -10839,6 +10763,21 @@ "node": ">=16" } }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/node-gyp/node_modules/which": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", @@ -11591,24 +11530,27 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "dev": true }, "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", "dev": true, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true }, "node_modules/picomatch": { @@ -11908,6 +11850,7 @@ "version": "17.18.9", "resolved": "https://github.com/Klebert-Engineering/primeng/releases/download/17.18.9-patched/primeng-17.18.9-patched.tgz", "integrity": "sha512-79e8NZvMjf+2QqgMpbGuh9G9OB7qxI/9j2b4qMSfdDceYfIMFzSWhxD8yr0omdSFwKrvjgggqtTjuKML5wrYqg==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, @@ -11954,9 +11897,9 @@ } }, "node_modules/protobufjs": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz", - "integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -12020,12 +11963,11 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -12055,9 +11997,9 @@ ] }, "node_modules/quickselect": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", - "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==" }, "node_modules/randombytes": { "version": "2.1.0", @@ -12093,11 +12035,11 @@ } }, "node_modules/rbush": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", - "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.1.tgz", + "integrity": "sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==", "dependencies": { - "quickselect": "^2.0.0" + "quickselect": "^3.0.0" } }, "node_modules/readable-stream": { @@ -12150,9 +12092,9 @@ "dev": true }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", - "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", "dev": true, "dependencies": { "regenerate": "^1.4.2" @@ -12601,9 +12543,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "dependencies": { "debug": "2.6.9", @@ -12651,12 +12593,6 @@ "node": ">=4" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/send/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -12745,20 +12681,29 @@ "dev": true }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -12870,12 +12815,15 @@ } }, "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, "engines": { - "node": ">=8" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/slice-ansi": { @@ -13006,9 +12954,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -13244,9 +13192,9 @@ } }, "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "engines": { "node": ">=12" @@ -13715,9 +13663,9 @@ } }, "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, "node_modules/tuf-js": { "version": "2.2.1", @@ -13790,9 +13738,9 @@ } }, "node_modules/ua-parser-js": { - "version": "0.7.38", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.38.tgz", - "integrity": "sha512-fYmIy7fKTSFAhG3fuPlubeGaMoAd6r0rSnfEsO5nEY55i26KSLt9EH7PLQiiqPUhNqYIJvSkTy1oArIcXAbPbA==", + "version": "0.7.39", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.39.tgz", + "integrity": "sha512-IZ6acm6RhQHNibSt7+c09hhvsKy9WUr4DVbeq9U8o71qxyYtJpQeDxQnMrVqnIFMLcQjHO0I9wgfO2vIahht4w==", "dev": true, "funding": [ { @@ -13808,6 +13756,9 @@ "url": "https://github.com/sponsors/faisalman" } ], + "bin": { + "ua-parser-js": "script/cli.js" + }, "engines": { "node": "*" } @@ -13818,9 +13769,9 @@ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "dev": true, "engines": { "node": ">=4" @@ -13840,9 +13791,9 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", "dev": true, "engines": { "node": ">=4" @@ -13976,20 +13927,6 @@ "node": ">= 0.4" } }, - "node_modules/url/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -14060,14 +13997,14 @@ } }, "node_modules/vite": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz", - "integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", "dev": true, "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.40", - "rollup": "^4.13.0" + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -14524,6 +14461,34 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/vite/node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/void-elements": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", @@ -14576,12 +14541,11 @@ "dev": true }, "node_modules/webpack": { - "version": "5.93.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", - "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, "dependencies": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", @@ -14590,7 +14554,7 @@ "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -14623,9 +14587,9 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.3.0.tgz", - "integrity": "sha512-xD2qnNew+F6KwOGZR7kWdbIou/ud7cVqLEXeK1q0nHcNsX/u7ul/fSdlOTX4ntSL5FNFy7ZJJXbf0piF591JYw==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", "dev": true, "dependencies": { "colorette": "^2.0.10", @@ -14710,6 +14674,15 @@ } } }, + "node_modules/webpack-dev-server/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/webpack-dev-server/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -14754,6 +14727,21 @@ } } }, + "node_modules/webpack-dev-server/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/webpack-dev-server/node_modules/rimraf": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", diff --git a/package.json b/package.json index 4fd5cdb3..e71e519e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "erdblick", - "version": "2024.3.2", + "version": "2024.4.0", "scripts": { "ng": "ng", "start": "ng serve", @@ -19,13 +19,15 @@ "@angular/platform-browser": "^18.2.0", "@angular/platform-browser-dynamic": "^18.2.0", "@angular/router": "^18.2.0", - "@codemirror/autocomplete": "^6.14.0", - "@codemirror/lang-yaml": "^6.0.0", - "@codemirror/lint": "^6.5.0", - "@codemirror/view": "^6.25.1", + "@codemirror/autocomplete": "^6.18.0", + "@codemirror/lang-yaml": "^6.1.1", + "@codemirror/lint": "^6.8.1", + "@codemirror/view": "^6.32.0", + "@ngx-formly/core": "^6.3.6", + "@ngx-formly/primeng": "^6.3.6", "assert": "^2.1.0", "browserify-zlib": "^0.2.0", - "cesium": "1.115.0", + "cesium": "1.120.0", "codemirror": "^6.0.1", "https-browserify": "^1.0.0", "js-yaml": "^4.1.0", @@ -35,8 +37,8 @@ "rxjs": "~7.8.1", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", - "tslib": "^2.3.0", - "url": "^0.11.3", + "tslib": "^2.6.3", + "url": "^0.11.4", "util": "^0.12.5", "zone.js": "~0.14.10" }, @@ -45,17 +47,20 @@ "@angular-devkit/build-angular": "^18.2.1", "@angular/cli": "^18.2.1", "@angular/compiler-cli": "^18.2.0", - "@types/jasmine": "~4.3.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.9.0", + "@types/jasmine": "~5.1.4", "@types/js-yaml": "^4.0.9", - "@typescript-eslint/eslint-plugin": "^7.4.0", - "@typescript-eslint/parser": "^7.4.0", - "eslint": "^8.57.0", - "jasmine-core": "~4.6.0", - "karma": "~6.4.0", + "@typescript-eslint/eslint-plugin": "^8.2.0", + "@typescript-eslint/parser": "^8.2.0", + "eslint": "^9.9.0", + "globals": "^15.9.0", + "jasmine-core": "~5.2.0", + "karma": "~6.4.4", "karma-chrome-launcher": "~3.2.0", - "karma-coverage": "~2.2.0", + "karma-coverage": "~2.2.1", "karma-jasmine": "~5.1.0", - "karma-jasmine-html-reporter": "~2.0.0", + "karma-jasmine-html-reporter": "~2.1.0", "typescript": "~5.5.4" } } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d2f7e752..aa8d5d9a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,4 +1,5 @@ project(test.erdblick) +enable_testing() if (NOT TARGET Catch2) FetchContent_Declare(Catch2 diff --git a/test/test-visualization.cpp b/test/test-visualization.cpp index f13a469e..36a3c35a 100644 --- a/test/test-visualization.cpp +++ b/test/test-visualization.cpp @@ -14,8 +14,8 @@ TEST_CASE("FeatureLayerVisualization", "[erdblick.renderer]") TileLayerParser tlp; auto testLayer = TestDataProvider(tlp).getTestLayer(42., 11., 13); auto style = TestDataProvider::style(); - FeatureLayerVisualization visualization(style, {}); - visualization.addTileFeatureLayer(testLayer); + FeatureLayerVisualization visualization("Features:Test:Test:0", style, {}, {}); + visualization.addTileFeatureLayer(TileFeatureLayer(testLayer)); visualization.run(); auto result = visualization.primitiveCollection(); std::cout << result << std::endl; diff --git a/webpack.config.js b/webpack.config.js index 22561886..b1180ca8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -16,4 +16,7 @@ module.exports = { externals: { 'cesium': 'Cesium' }, + output: { + publicPath: 'auto' + } };