From 838f94f8b257fd74b191a87861cc05aead3aa26a Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Fri, 29 Nov 2024 12:49:47 +0100 Subject: [PATCH 01/60] Add GIS component for map-querying + align icons in Query dropdown correctly, if the icons have different widths --- .../default-layout.component.html | 25 ++++++++++--- .../default-layout.component.scss | 7 ++++ src/app/views/querying/gis/gis.component.html | 4 +++ src/app/views/querying/gis/gis.component.scss | 0 src/app/views/querying/gis/gis.component.ts | 36 +++++++++++++++++++ .../views/querying/querying.component.html | 1 + src/app/views/views.module.ts | 2 ++ 7 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 src/app/views/querying/gis/gis.component.html create mode 100644 src/app/views/querying/gis/gis.component.scss create mode 100644 src/app/views/querying/gis/gis.component.ts diff --git a/src/app/containers/default-layout/default-layout.component.html b/src/app/containers/default-layout/default-layout.component.html index e0bd30c0..03c72284 100644 --- a/src/app/containers/default-layout/default-layout.component.html +++ b/src/app/containers/default-layout/default-layout.component.html @@ -40,31 +40,48 @@
  • - +
    + +
    Console
  • - +
    + +
    Graphical Querying
  • - +
    + +
    Plan Builder
  • - +
    + +
    Explore by Example
  • +
  • + +
    + +
    + Map-based Query +
    +
  • diff --git a/src/app/containers/default-layout/default-layout.component.scss b/src/app/containers/default-layout/default-layout.component.scss index b7a38342..e5759dcc 100644 --- a/src/app/containers/default-layout/default-layout.component.scss +++ b/src/app/containers/default-layout/default-layout.component.scss @@ -38,4 +38,11 @@ padding: 0 0.1rem; } +.icon-fixed-width { + width: 25px; + margin-right: 2px; + display: inline-flex; + justify-content: center; +} + diff --git a/src/app/views/querying/gis/gis.component.html b/src/app/views/querying/gis/gis.component.html new file mode 100644 index 00000000..f7e4d269 --- /dev/null +++ b/src/app/views/querying/gis/gis.component.html @@ -0,0 +1,4 @@ + + Query by Map! + + diff --git a/src/app/views/querying/gis/gis.component.scss b/src/app/views/querying/gis/gis.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/views/querying/gis/gis.component.ts b/src/app/views/querying/gis/gis.component.ts new file mode 100644 index 00000000..732d0318 --- /dev/null +++ b/src/app/views/querying/gis/gis.component.ts @@ -0,0 +1,36 @@ +import { + Component, + inject, + OnDestroy, + OnInit +} from '@angular/core'; +import {CrudService} from '../../../services/crud.service'; +import {LeftSidebarService} from '../../../components/left-sidebar/left-sidebar.service'; +import {BreadcrumbService} from '../../../components/breadcrumb/breadcrumb.service'; +import {WebuiSettingsService} from '../../../services/webui-settings.service'; +import {UtilService} from '../../../services/util.service'; +import {ToasterService} from '../../../components/toast-exposer/toaster.service'; +import {CatalogService} from '../../../services/catalog.service'; + +@Component({ + selector: 'app-gis', + templateUrl: './gis.component.html', + styleUrls: ['./gis.component.scss'] +}) +export class GisComponent implements OnInit, OnDestroy { + private readonly _crud = inject(CrudService); + private readonly _leftSidebar = inject(LeftSidebarService); + private readonly _breadcrumb = inject(BreadcrumbService); + private readonly _settings = inject(WebuiSettingsService); + public readonly _util = inject(UtilService); + public readonly _toast = inject(ToasterService); + public readonly _catalog = inject(CatalogService); + private readonly _sidebar = inject(LeftSidebarService); + + ngOnDestroy(): void { + console.log("GisComponent.ngOnDestroy") + } + ngOnInit(): void { + console.log("GisComponent.ngOnInit") + } +} diff --git a/src/app/views/querying/querying.component.html b/src/app/views/querying/querying.component.html index a84c68f2..889b143d 100644 --- a/src/app/views/querying/querying.component.html +++ b/src/app/views/querying/querying.component.html @@ -2,5 +2,6 @@ + diff --git a/src/app/views/views.module.ts b/src/app/views/views.module.ts index f7830668..86b2cd38 100644 --- a/src/app/views/views.module.ts +++ b/src/app/views/views.module.ts @@ -95,6 +95,7 @@ import { } from '@coreui/angular'; import {EditEntityComponent} from './schema-editing/edit-entity/edit-entity.component'; import {TreeModule} from '@ali-hm/angular-tree-component'; +import {GisComponent} from "./querying/gis/gis.component"; @NgModule({ @@ -169,6 +170,7 @@ import {TreeModule} from '@ali-hm/angular-tree-component'; EditColumnsComponent, FormGeneratorComponent, GraphicalQueryingComponent, + GisComponent, ConsoleComponent, TableViewComponent, UmlComponent, From e21bdb9b6996352b6686e74d128441db25555484 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Fri, 29 Nov 2024 12:53:13 +0100 Subject: [PATCH 02/60] Fixed typo in class name --- src/app/containers/default-layout/default-layout.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/containers/default-layout/default-layout.component.html b/src/app/containers/default-layout/default-layout.component.html index 03c72284..564ad5f0 100644 --- a/src/app/containers/default-layout/default-layout.component.html +++ b/src/app/containers/default-layout/default-layout.component.html @@ -76,7 +76,7 @@
  • -
    +
    Map-based Query From 5754874c10ce41693cf3257f0c5c7906e295673c Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Fri, 29 Nov 2024 14:04:37 +0100 Subject: [PATCH 03/60] Show empty leaflet map correctly inside results view --- angular.json | 3 +- package-lock.json | 6 + package.json | 1 + src/app/components/components.module.ts | 2 + .../data-map/data-map.component.html | 28 + .../data-map/data-map.component.scss | 48 ++ .../data-view/data-map/data-map.component.ts | 94 +++ .../components/data-view/data-map/leaflet.css | 775 ++++++++++++++++++ .../data-view/data-view.component.html | 10 + .../data-view/models/result-set.model.ts | 3 +- 10 files changed, 968 insertions(+), 2 deletions(-) create mode 100644 src/app/components/data-view/data-map/data-map.component.html create mode 100644 src/app/components/data-view/data-map/data-map.component.scss create mode 100644 src/app/components/data-view/data-map/data-map.component.ts create mode 100644 src/app/components/data-view/data-map/leaflet.css diff --git a/angular.json b/angular.json index 43844f06..8c513849 100644 --- a/angular.json +++ b/angular.json @@ -51,7 +51,8 @@ "node_modules/plyr/dist/plyr.css", "node_modules/@ali-hm/angular-tree-component/css/angular-tree-component.css", "node_modules/katex/dist/katex.min.css", - "node_modules/prismjs/themes/prism-okaidia.css" + "node_modules/prismjs/themes/prism-okaidia.css", + "src/app/components/data-view/data-map/leaflet.css" ], "stylePreprocessorOptions": { "includePaths": [ diff --git a/package-lock.json b/package-lock.json index 640a0a45..4d598a4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "jquery": "3.7.1", "jquery-ui": "^1.13.0", "katex": "^0.16.0", + "leaflet": "^1.9.4", "lodash": "^4.17.20", "marked": "^9.1.6", "moment": "^2.30.1", @@ -10970,6 +10971,11 @@ "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", "optional": true }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" + }, "node_modules/less": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", diff --git a/package.json b/package.json index 432c6856..804dd31e 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "jquery": "3.7.1", "jquery-ui": "^1.13.0", "katex": "^0.16.0", + "leaflet": "^1.9.4", "lodash": "^4.17.20", "marked": "^9.1.6", "moment": "^2.30.1", diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index 0be0561c..65f2f191 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -99,6 +99,7 @@ import {ToastComponent as Toast} from './toast-exposer/toast/toast.component'; import {ReloadButtonComponent} from '../views/util/reload-button/reload-button.component'; import {ViewComponent} from './data-view/view/view.component'; import {DockerInstanceComponent} from './docker/dockerinstance/dockerinstance.component'; +import {DataMapComponent} from "./data-view/data-map/data-map.component"; //import 'hammerjs'; @@ -185,6 +186,7 @@ import {DockerInstanceComponent} from './docker/dockerinstance/dockerinstance.co DataCardComponent, DataTableComponent, DataGraphComponent, + DataMapComponent, MediaComponent, DeleteConfirmComponent, ExpandableTextComponent, diff --git a/src/app/components/data-view/data-map/data-map.component.html b/src/app/components/data-view/data-map/data-map.component.html new file mode 100644 index 00000000..a958774e --- /dev/null +++ b/src/app/components/data-view/data-map/data-map.component.html @@ -0,0 +1,28 @@ +
    +
    +
    +
    + + + Loading... + + + Loading... +
    +
    + + + +
    + + + +
    + + diff --git a/src/app/components/data-view/data-map/data-map.component.scss b/src/app/components/data-view/data-map/data-map.component.scss new file mode 100644 index 00000000..e4b8ce95 --- /dev/null +++ b/src/app/components/data-view/data-map/data-map.component.scss @@ -0,0 +1,48 @@ +.map-container { + position: relative; + min-height: 600px; + border: 1px solid lightgray; +} + +#map { + position: absolute; + inset: 0; + z-index: 1; +} + +.map-overlay { + position: absolute; + inset: 0; + z-index: 2; + pointer-events: none; + display: grid; + background: rgba(0, 0, 0, 0.1); + place-items: center; +} + +.spinner-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + background: rgba(0, 0, 0, 0.25); + padding: 2rem; +} + +.render-button { + position: absolute; + z-index: 3; + bottom: 2rem; + right: 2rem; +} + +.spinner-container > span { + font-size: 1.5rem; + font-weight: 400; + color: white; +} + + +.leaflet-overlay-pane > svg { + transition: opacity 50ms ease-in-out; +} \ No newline at end of file diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts new file mode 100644 index 00000000..0b32d24d --- /dev/null +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -0,0 +1,94 @@ +import {AfterViewInit, Component, effect} from '@angular/core'; +import {DataTemplateComponent} from '../data-template/data-template.component'; +import * as d3 from 'd3'; +import { GeoPath, GeoPermissibleObjects } from 'd3'; +import * as d3Geo from 'd3-geo'; +import * as L from 'leaflet'; + +@Component({ + selector: 'app-data-map', + templateUrl: './data-map.component.html', + styleUrls: ['./data-map.component.scss'] +}) +export class DataMapComponent extends DataTemplateComponent implements AfterViewInit { + constructor() { + super(); + } + + // Leaflet + readonly MIN_ZOOM = 0; + readonly MAX_ZOOM = 19; + readonly INITIAL_ZOOM = 6; + private map!: L.Map; + private currentBaseLayer: L.TileLayer | undefined; + + // D3 + private svg: + | d3.Selection + | undefined; + private g: d3.Selection | undefined; + // private circles: + // | d3.Selection + // | undefined; + // private paths: + // | d3.Selection + // | undefined; + private pathGenerator!: GeoPath; + private tooltip!: d3.Selection; + + ngAfterViewInit(): void { + const leafletMap = L.map('map').setView([52, 10], this.INITIAL_ZOOM); + this.map = leafletMap; + this.svg = d3.select(this.map.getPanes().overlayPane).append('svg'); + this.g = this.svg.append('g').attr('class', 'leaflet-zoom-hide'); + this.tooltip = d3 + .select('body') + .append('div') + .style('position', 'absolute') + .style('font-size', '0.75rem') + .style('font-family', 'monospace') + .style('background', 'white') + .style('border', '1px solid #ccc') + .style('padding', '5px') + .style('display', 'none') + .style('z-index', '9999'); + + // function projectPoint(this: any, x: number, y: number) { + // const point = leafletMap.latLngToLayerPoint(new L.LatLng(y, x)); + // this.stream.point(point.x, point.y); + // } + // + // const transform = d3Geo.geoTransform({ point: projectPoint }); + // this.pathGenerator = d3Geo.geoPath().projection(transform); + // + // this.map.on('zoomend', () => { + // this.updateSvgPosition(); + // }); + // this.map.on('moveend', () => { + // if (!this.svg || !this.g) { + // return; + // } + // const bounds = this.map.getBounds(); + // const topLeft = this.map.latLngToLayerPoint(bounds.getNorthWest()); + // const bottomRight = this.map.latLngToLayerPoint( + // bounds.getSouthEast(), + // ); + // this.svg + // .style('width', '999999px') + // .style('height', '999999px') + // .style('left', topLeft.x + 'px') + // .style('top', topLeft.y + 'px'); + // this.g.attr('transform', `translate(${-topLeft.x}, ${-topLeft.y})`); + // }); + + this.currentBaseLayer = L.tileLayer( + 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + { + maxZoom: this.MAX_ZOOM, + attribution: + '© OpenStreetMap contributors', + }, + ).addTo(this.map); + } +} + diff --git a/src/app/components/data-view/data-map/leaflet.css b/src/app/components/data-view/data-map/leaflet.css new file mode 100644 index 00000000..cbe009ce --- /dev/null +++ b/src/app/components/data-view/data-map/leaflet.css @@ -0,0 +1,775 @@ +/* required styles */ + +.leaflet-pane, +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-tile-container, +.leaflet-pane > svg, +.leaflet-pane > canvas, +.leaflet-zoom-box, +.leaflet-image-layer, +.leaflet-layer { + position: absolute; + left: 0; + top: 0; +} + +.leaflet-container { + overflow: hidden; +} + +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + -webkit-user-drag: none; +} + +/* Prevents IE11 from highlighting tiles in blue */ +.leaflet-tile::selection { + background: transparent; +} + +/* Safari renders non-retina tile on retina better with this, but Chrome is worse */ +.leaflet-safari .leaflet-tile { + image-rendering: -webkit-optimize-contrast; +} + +/* hack that prevents hw layers "stretching" when loading new tiles */ +.leaflet-safari .leaflet-tile-container { + width: 1600px; + height: 1600px; + -webkit-transform-origin: 0 0; +} + +.leaflet-marker-icon, +.leaflet-marker-shadow { + display: block; +} + +/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ +/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ +.leaflet-container .leaflet-overlay-pane svg { + max-width: none !important; + max-height: none !important; +} + +.leaflet-container .leaflet-marker-pane img, +.leaflet-container .leaflet-shadow-pane img, +.leaflet-container .leaflet-tile-pane img, +.leaflet-container img.leaflet-image-layer, +.leaflet-container .leaflet-tile { + max-width: none !important; + max-height: none !important; + width: auto; + padding: 0; +} + +.leaflet-container img.leaflet-tile { + /* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */ + mix-blend-mode: plus-lighter; +} + +.leaflet-container.leaflet-touch-zoom { + -ms-touch-action: pan-x pan-y; + touch-action: pan-x pan-y; +} + +.leaflet-container.leaflet-touch-drag { + -ms-touch-action: pinch-zoom; + /* Fallback for FF which doesn't support pinch-zoom */ + touch-action: none; + touch-action: pinch-zoom; +} + +.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { + -ms-touch-action: none; + touch-action: none; +} + +.leaflet-container { + -webkit-tap-highlight-color: transparent; +} + +.leaflet-container a { + -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); +} + +.leaflet-tile { + filter: inherit; + visibility: hidden; +} + +.leaflet-tile-loaded { + visibility: inherit; +} + +.leaflet-zoom-box { + width: 0; + height: 0; + -moz-box-sizing: border-box; + box-sizing: border-box; + z-index: 800; +} + +/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ +.leaflet-overlay-pane svg { + -moz-user-select: none; +} + +.leaflet-pane { + z-index: 400; +} + +.leaflet-tile-pane { + z-index: 200; +} + +.leaflet-overlay-pane { + z-index: 400; +} + +.leaflet-shadow-pane { + z-index: 500; +} + +.leaflet-marker-pane { + z-index: 600; +} + +.leaflet-tooltip-pane { + z-index: 650; +} + +.leaflet-popup-pane { + z-index: 700; +} + +.leaflet-map-pane canvas { + z-index: 100; +} + +.leaflet-map-pane svg { + z-index: 200; +} + +.leaflet-vml-shape { + width: 1px; + height: 1px; +} + +.lvml { + behavior: url(#default#VML); + display: inline-block; + position: absolute; +} + + +/* control positioning */ + +.leaflet-control { + position: relative; + z-index: 800; + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; +} + +.leaflet-top, +.leaflet-bottom { + position: absolute; + z-index: 1000; + pointer-events: none; +} + +.leaflet-top { + top: 0; +} + +.leaflet-right { + right: 0; +} + +.leaflet-bottom { + bottom: 0; +} + +.leaflet-left { + left: 0; +} + +.leaflet-control { + float: left; + clear: both; +} + +.leaflet-right .leaflet-control { + float: right; +} + +.leaflet-top .leaflet-control { + margin-top: 10px; +} + +.leaflet-bottom .leaflet-control { + margin-bottom: 10px; +} + +.leaflet-left .leaflet-control { + margin-left: 10px; +} + +.leaflet-right .leaflet-control { + margin-right: 10px; +} + + +/* zoom and fade animations */ + +.leaflet-fade-anim .leaflet-popup { + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; +} + +.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { + opacity: 1; +} + +.leaflet-zoom-animated { + -webkit-transform-origin: 0 0; + -ms-transform-origin: 0 0; + transform-origin: 0 0; +} + +svg.leaflet-zoom-animated { + will-change: transform; +} + +.leaflet-zoom-anim .leaflet-zoom-animated { + -webkit-transition: -webkit-transform 0.25s cubic-bezier(0, 0, 0.25, 1); + -moz-transition: -moz-transform 0.25s cubic-bezier(0, 0, 0.25, 1); + transition: transform 0.25s cubic-bezier(0, 0, 0.25, 1); +} + +.leaflet-zoom-anim .leaflet-tile, +.leaflet-pan-anim .leaflet-tile { + -webkit-transition: none; + -moz-transition: none; + transition: none; +} + +.leaflet-zoom-anim .leaflet-zoom-hide { + visibility: hidden; +} + + +/* cursors */ + +.leaflet-interactive { + cursor: pointer; +} + +.leaflet-grab { + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; +} + +.leaflet-crosshair, +.leaflet-crosshair .leaflet-interactive { + cursor: crosshair; +} + +.leaflet-popup-pane, +.leaflet-control { + cursor: auto; +} + +.leaflet-dragging .leaflet-grab, +.leaflet-dragging .leaflet-grab .leaflet-interactive, +.leaflet-dragging .leaflet-marker-draggable { + cursor: move; + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + cursor: grabbing; +} + +/* marker & overlays interactivity */ +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-image-layer, +.leaflet-pane > svg path, +.leaflet-tile-container { + pointer-events: none; +} + +.leaflet-marker-icon.leaflet-interactive, +.leaflet-image-layer.leaflet-interactive, +.leaflet-pane > svg path.leaflet-interactive, +svg.leaflet-image-layer.leaflet-interactive path { + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; +} + +/* visual tweaks */ + +.leaflet-container { + background: #ddd; + outline-offset: 1px; +} + +.leaflet-container a { + color: #0078A8; +} + +.leaflet-zoom-box { + border: 2px dotted #38f; + background: rgba(255, 255, 255, 0.5); +} + + +/* general typography */ +.leaflet-container { + font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; + font-size: 12px; + font-size: 0.75rem; + line-height: 1.5; +} + + +/* general toolbar styles */ + +.leaflet-bar { + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65); + border-radius: 4px; +} + +.leaflet-bar a { + background-color: #fff; + border-bottom: 1px solid #ccc; + width: 26px; + height: 26px; + line-height: 26px; + display: block; + text-align: center; + text-decoration: none; + color: black; +} + +.leaflet-bar a, +.leaflet-control-layers-toggle { + background-position: 50% 50%; + background-repeat: no-repeat; + display: block; +} + +.leaflet-bar a:hover, +.leaflet-bar a:focus { + background-color: #f4f4f4; +} + +.leaflet-bar a:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.leaflet-bar a:last-child { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom: none; +} + +.leaflet-bar a.leaflet-disabled { + cursor: default; + background-color: #f4f4f4; + color: #bbb; +} + +.leaflet-touch .leaflet-bar a { + width: 30px; + height: 30px; + line-height: 30px; +} + +.leaflet-touch .leaflet-bar a:first-child { + border-top-left-radius: 2px; + border-top-right-radius: 2px; +} + +.leaflet-touch .leaflet-bar a:last-child { + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; +} + +/* zoom control */ + +.leaflet-control-zoom-in, +.leaflet-control-zoom-out { + font: bold 18px 'Lucida Console', Monaco, monospace; + text-indent: 1px; +} + +.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { + font-size: 22px; +} + + +/* layers control */ + +.leaflet-control-layers { + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4); + background: #fff; + border-radius: 5px; +} + +.leaflet-control-layers-toggle { + /*Note: commented out, because we don't need them, and I don't want to change the WabPack config*/ + /*background-image: url(images/layers.png);*/ + width: 36px; + height: 36px; +} + +.leaflet-retina .leaflet-control-layers-toggle { + /*Note: commented out, because we don't need them, and I don't want to change the WabPack config*/ + /*background-image: url(images/layers-2x.png);*/ + background-size: 26px 26px; +} + +.leaflet-touch .leaflet-control-layers-toggle { + width: 44px; + height: 44px; +} + +.leaflet-control-layers .leaflet-control-layers-list, +.leaflet-control-layers-expanded .leaflet-control-layers-toggle { + display: none; +} + +.leaflet-control-layers-expanded .leaflet-control-layers-list { + display: block; + position: relative; +} + +.leaflet-control-layers-expanded { + padding: 6px 10px 6px 6px; + color: #333; + background: #fff; +} + +.leaflet-control-layers-scrollbar { + overflow-y: scroll; + overflow-x: hidden; + padding-right: 5px; +} + +.leaflet-control-layers-selector { + margin-top: 2px; + position: relative; + top: 1px; +} + +.leaflet-control-layers label { + display: block; + font-size: 13px; + font-size: 1.08333em; +} + +.leaflet-control-layers-separator { + height: 0; + border-top: 1px solid #ddd; + margin: 5px -10px 5px -6px; +} + +/* Default icon URLs */ +.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */ + /*Note: commented out, because we don't need them, and I don't want to change the WabPack config*/ + /*background-image: url(images/marker-icon.png);*/ +} + + +/* attribution and scale controls */ + +.leaflet-container .leaflet-control-attribution { + background: #fff; + background: rgba(255, 255, 255, 0.8); + margin: 0; +} + +.leaflet-control-attribution, +.leaflet-control-scale-line { + padding: 0 5px; + color: #333; + line-height: 1.4; +} + +.leaflet-control-attribution a { + text-decoration: none; +} + +.leaflet-control-attribution a:hover, +.leaflet-control-attribution a:focus { + text-decoration: underline; +} + +.leaflet-attribution-flag { + display: inline !important; + vertical-align: baseline !important; + width: 1em; + height: 0.6669em; +} + +.leaflet-left .leaflet-control-scale { + margin-left: 5px; +} + +.leaflet-bottom .leaflet-control-scale { + margin-bottom: 5px; +} + +.leaflet-control-scale-line { + border: 2px solid #777; + border-top: none; + line-height: 1.1; + padding: 2px 5px 1px; + white-space: nowrap; + -moz-box-sizing: border-box; + box-sizing: border-box; + background: rgba(255, 255, 255, 0.8); + text-shadow: 1px 1px #fff; +} + +.leaflet-control-scale-line:not(:first-child) { + border-top: 2px solid #777; + border-bottom: none; + margin-top: -2px; +} + +.leaflet-control-scale-line:not(:first-child):not(:last-child) { + border-bottom: 2px solid #777; +} + +.leaflet-touch .leaflet-control-attribution, +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + box-shadow: none; +} + +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + border: 2px solid rgba(0, 0, 0, 0.2); + background-clip: padding-box; +} + + +/* popup */ + +.leaflet-popup { + position: absolute; + text-align: center; + margin-bottom: 20px; +} + +.leaflet-popup-content-wrapper { + padding: 1px; + text-align: left; + border-radius: 12px; +} + +.leaflet-popup-content { + margin: 13px 24px 13px 20px; + line-height: 1.3; + font-size: 13px; + font-size: 1.08333em; + min-height: 1px; +} + +.leaflet-popup-content p { + margin: 17px 0; + margin: 1.3em 0; +} + +.leaflet-popup-tip-container { + width: 40px; + height: 20px; + position: absolute; + left: 50%; + margin-top: -1px; + margin-left: -20px; + overflow: hidden; + pointer-events: none; +} + +.leaflet-popup-tip { + width: 17px; + height: 17px; + padding: 1px; + + margin: -10px auto 0; + pointer-events: auto; + + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} + +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { + background: white; + color: #333; + box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4); +} + +.leaflet-container a.leaflet-popup-close-button { + position: absolute; + top: 0; + right: 0; + border: none; + text-align: center; + width: 24px; + height: 24px; + font: 16px/24px Tahoma, Verdana, sans-serif; + color: #757575; + text-decoration: none; + background: transparent; +} + +.leaflet-container a.leaflet-popup-close-button:hover, +.leaflet-container a.leaflet-popup-close-button:focus { + color: #585858; +} + +.leaflet-popup-scrolled { + overflow: auto; +} + +.leaflet-oldie .leaflet-popup-content-wrapper { + -ms-zoom: 1; +} + +.leaflet-oldie .leaflet-popup-tip { + width: 24px; + margin: 0 auto; + + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; + filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); +} + +.leaflet-oldie .leaflet-control-zoom, +.leaflet-oldie .leaflet-control-layers, +.leaflet-oldie .leaflet-popup-content-wrapper, +.leaflet-oldie .leaflet-popup-tip { + border: 1px solid #999; +} + + +/* div icon */ + +.leaflet-div-icon { + background: #fff; + border: 1px solid #666; +} + + +/* Tooltip */ +/* Base styles for the element that has a tooltip */ +.leaflet-tooltip { + position: absolute; + padding: 6px; + background-color: #fff; + border: 1px solid #fff; + border-radius: 3px; + color: #222; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); +} + +.leaflet-tooltip.leaflet-interactive { + cursor: pointer; + pointer-events: auto; +} + +.leaflet-tooltip-top:before, +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + position: absolute; + pointer-events: none; + border: 6px solid transparent; + background: transparent; + content: ""; +} + +/* Directions */ + +.leaflet-tooltip-bottom { + margin-top: 6px; +} + +.leaflet-tooltip-top { + margin-top: -6px; +} + +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-top:before { + left: 50%; + margin-left: -6px; +} + +.leaflet-tooltip-top:before { + bottom: 0; + margin-bottom: -12px; + border-top-color: #fff; +} + +.leaflet-tooltip-bottom:before { + top: 0; + margin-top: -12px; + margin-left: -6px; + border-bottom-color: #fff; +} + +.leaflet-tooltip-left { + margin-left: -6px; +} + +.leaflet-tooltip-right { + margin-left: 6px; +} + +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + top: 50%; + margin-top: -6px; +} + +.leaflet-tooltip-left:before { + right: 0; + margin-right: -12px; + border-left-color: #fff; +} + +.leaflet-tooltip-right:before { + left: 0; + margin-left: -12px; + border-right-color: #fff; +} + +/* Printing */ + +@media print { + /* Prevent printers from removing background-images of controls. */ + .leaflet-control { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } +} diff --git a/src/app/components/data-view/data-view.component.html b/src/app/components/data-view/data-view.component.html index 6294ebf2..57562487 100644 --- a/src/app/components/data-view/data-view.component.html +++ b/src/app/components/data-view/data-view.component.html @@ -22,6 +22,11 @@ (click)="$presentationType.set(presentationTypes.GRAPH)" [class.active]="$presentationType() === presentationTypes.GRAPH" tooltip="graph" placement="top" delay="200"> +
    +
    + + +
    +
    diff --git a/src/app/views/querying/gis/components/config-section/config-section.component.scss b/src/app/views/querying/gis/components/config-section/config-section.component.scss new file mode 100644 index 00000000..aa0c1ebd --- /dev/null +++ b/src/app/views/querying/gis/components/config-section/config-section.component.scss @@ -0,0 +1,31 @@ +.section-header { + all: unset; + box-sizing: border-box; + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #D3D3D3; + padding: 0.25rem 0; + transition: all 100ms ease-in-out; + + &:hover { + color: #5856D5; + + & > svg { + fill: #5856D5; + } + } + + & > svg { + width: 20px; + height: 20px; + fill: #BBBBBB; + } +} + +.section-body { + flex: 1; + margin: 1rem 0; +} diff --git a/src/app/views/querying/gis/components/config-section/config-section.component.ts b/src/app/views/querying/gis/components/config-section/config-section.component.ts new file mode 100644 index 00000000..3243ca7c --- /dev/null +++ b/src/app/views/querying/gis/components/config-section/config-section.component.ts @@ -0,0 +1,40 @@ +import {Component, Injector, Input, OnInit} from '@angular/core'; +import { ButtonDirective, CollapseDirective } from '@coreui/angular'; +import { Visualization } from '../../models/visualization.interface'; +import { NgComponentOutlet, NgIf } from '@angular/common'; + +@Component({ + selector: 'app-config-section', + standalone: true, + imports: [ButtonDirective, CollapseDirective, NgComponentOutlet, NgIf], + templateUrl: './config-section.component.html', + styleUrl: './config-section.component.scss', +}) +export class ConfigSectionComponent implements OnInit { + @Input() config?: Visualization; + @Input() title: string = ''; + + injector?: Injector; + + constructor() { + } + + ngOnInit(): void { + this.injector = Injector.create({ + providers: [{ provide: 'config', useValue: this.config }], + }); + } + + // Needed? + updateConfigInjector(): void { + this.injector = Injector.create({ + providers: [{ provide: 'config', useValue: this.config }], + }); + } + + isSectionBodyVisible = false; + + toggleSectionBodyVisibility(): void { + this.isSectionBodyVisible = !this.isSectionBodyVisible; + } +} diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.html b/src/app/views/querying/gis/components/layers/map-layers.component.html new file mode 100644 index 00000000..829db7cf --- /dev/null +++ b/src/app/views/querying/gis/components/layers/map-layers.component.html @@ -0,0 +1,173 @@ +
    +
    + + + +
    + @for (layer of layers; track layer.name) { + +

    + Preview data +

    +
    +
    + First data point: + +
    + No data attached to geometries +
    +
    + + + + {{ layer.index }} +
    + +
    +
    + + +
    +
    + +
    + {{ layer.name }}({{ layer.data.length }}) +
    +
    + + + + +
    +
    +
    + } +
    + + + + 0 + + +
    + Base Layer +
    +
    + + + + +
    +
    +
    + + + +
    Add layer
    + +
    + +
    + + + + + + +
    + TODO +
    + +
    + TODO +
    + +
    + Select which dataset will be loaded +
    + +
    +
    + +
    + + +
    + +
    +
    + + + + +
    +
    diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.scss b/src/app/views/querying/gis/components/layers/map-layers.component.scss new file mode 100644 index 00000000..4fc9fb90 --- /dev/null +++ b/src/app/views/querying/gis/components/layers/map-layers.component.scss @@ -0,0 +1,208 @@ +.layers-container { + position: relative; + height: 100%; + display: flex; + flex-direction: column; + padding: 1rem; + grid-gap: 1rem; + + max-height: 100%; + overflow-y: auto; + + & > .spacer-top { + flex: 1; + } + + & > .spacer-bottom { + margin-bottom: 2rem; + } +} + +.drag-drop-layers { + display: flex; + flex-direction: column; + grid-gap: 1rem; +} + +.top-bottom-label { + text-align: center; + color: lightgray; + margin: 0 6rem; + letter-spacing: 0.5px; + user-select: none; +} + +.top-label { + border-bottom: 1px solid lightgray; +} + +.bottom-label { + border-top: 1px solid lightgray; +} + +.base-layers { + display: flex; + justify-content: space-evenly; + padding: 0.5rem 0.25rem; +} + +.base-layers > div { + display: grid; + place-items: center; + height: 50px; + width: 50px; + border-radius: 0.2rem; + transition: all 100ms ease-in-out; +} + +.base-layers > div:hover { + background: #f1f1f1; + cursor: pointer; +} + +.base-layers > div > svg { + width: 30px; + fill: #bfdab3; +} + +.add-layer-button { + all: unset; + border-radius: 100%; + border: 1px solid #1a186c; + background: #4b49b6; + position: absolute; + bottom: -1.75rem; + left: 50%; + transform: translateX(-50%); + padding: 1.5rem; + display: grid; + place-items: center; + cursor: pointer; +} + +.add-layer-button > svg { + width: 1rem; + transform: scale(1.2); + fill: #f3f4f7; +} + +.layer-card { + display: flex; + border: 1px solid lightgray; + flex-direction: column; + background: white; + + & > .card-header { + display: grid; + place-items: center; + grid-gap: 0.5rem; + grid-template-columns: 1fr 1fr 1fr; + color: lightgray; + + & > span { + width: 100%; + user-select: none; + } + + & > .resize-grip { + display: grid; + place-items: center; + width: 2rem; + height: 15px; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); + cursor: grab; + + & > span { + height: 3px; + width: 3px; + background-color: #bbb; + border-radius: 50%; + display: inline-block; + } + } + + & > .icons { + display: flex; + gap: 0.35rem; + width: 100%; + justify-content: flex-end; + + & > .eye, & > .remove { + all: unset; + + & > svg { + width: 1rem; + fill: lightgray; + transition: all 100ms ease-in-out; + cursor: pointer; + + &:hover { + fill: #4b49b6; + } + } + } + } + } + + & > .layer-card-body { + display: flex; + flex-direction: column; + gap: 1rem; + } + + & .title { + word-break: break-all; + + & > .layer-name { + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: bold; + } + + & > .layer-size { + text-transform: uppercase; + letter-spacing: 0.5px; + } + + & > .preview-data { + all: unset; + color: #4645ab; + cursor: pointer; + user-select: none; + margin-left: 0.25rem; + + &:hover { + text-decoration: underline; + } + } + } + + & .config-sections { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + & .config-container { + flex: 1; + } +} + +.layer-card-placeholder { + border: 2px dashed #007bff; + background-color: #e9f5ff; + height: 50px; +} + +.json-viewer-popover-body { + max-height: 40vh; + overflow-y: auto; +} + +.add-layer-modal-body { + display: flex; + flex-direction: column; + gap: 1rem; +} + diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts new file mode 100644 index 00000000..64d10761 --- /dev/null +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -0,0 +1,358 @@ +import {AfterViewInit, Component, ElementRef, HostListener, OnInit, Renderer2} from '@angular/core'; +import {LayerContext} from '../../models/LayerContext.model'; +import {LayerSettingsService} from '../../services/layersettings.service'; +import { + ButtonCloseDirective, + ButtonDirective, + CardBodyComponent, + CardComponent, + CardHeaderComponent, + FormControlDirective, + FormLabelDirective, + FormSelectDirective, + InputGroupComponent, + InputGroupTextDirective, + ListGroupDirective, + ListGroupItemDirective, + ModalBodyComponent, + ModalComponent, + ModalFooterComponent, + ModalHeaderComponent, + ModalTitleDirective, + PopoverDirective, +} from '@coreui/angular'; +import {RowResult} from '../../models/RowResult.model'; +import * as GeoJSON from 'geojson'; +import {FeatureCollection} from 'geojson'; +import {MapLayer} from '../../models/MapLayer.model'; +import {AsyncPipe, NgComponentOutlet, NgForOf, NgIf} from '@angular/common'; +import isEqual from 'lodash/isEqual'; +import { + CdkDrag, + CdkDragDrop, + CdkDragHandle, + CdkDragPlaceholder, + CdkDropList, + moveItemInArray, +} from '@angular/cdk/drag-drop'; +import {FormsModule} from '@angular/forms'; +import {NgxJsonViewerModule} from 'ngx-json-viewer'; +import {ConfigSectionComponent} from '../config-section/config-section.component'; +// noinspection ES6UnusedImports +import {getSampleMapLayers} from '../../models/get-sample-maplayers'; + +type BaseLayer = { name: string; value: string }; + +@Component({ + selector: 'app-map-layers', + standalone: true, + imports: [ + FormLabelDirective, + FormControlDirective, + AsyncPipe, + NgComponentOutlet, + NgIf, + ModalComponent, + ModalHeaderComponent, + ModalBodyComponent, + ModalFooterComponent, + ModalTitleDirective, + ButtonCloseDirective, + ButtonDirective, + CdkDropList, + CdkDrag, + CdkDragPlaceholder, + CdkDragHandle, + InputGroupComponent, + InputGroupTextDirective, + FormSelectDirective, + FormsModule, + NgForOf, + CardComponent, + CardHeaderComponent, + CardBodyComponent, + PopoverDirective, + NgxJsonViewerModule, + ConfigSectionComponent, + ListGroupDirective, + ListGroupItemDirective, + ], + templateUrl: './map-layers.component.html', + styleUrl: './map-layers.component.scss', + // changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MapLayersComponent implements OnInit, AfterViewInit { + protected baseLayers: BaseLayer[] = [ + { + name: 'OSM', + value: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + }, + { + name: 'OSM Hot', + value: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', + }, + { + name: 'Grey Background', + value: 'EMPTY', + }, + ]; + protected selectedBaseLayer: BaseLayer = this.baseLayers[0]; + protected layers: MapLayer[] = []; + protected renderedLayers: MapLayer[] = []; + protected isAddLayerModalVisible = false; + protected loadedGeoJsonFile?: GeoJSON.FeatureCollection = undefined; + protected loadedGeoJsonFileName: string = ''; + protected anyLayersVisible = false; + protected addLayerModes: LayerContext[] = [ + LayerContext.Results, + LayerContext.Query, + LayerContext.DB, + LayerContext.External, + ]; + protected addLayerMode: LayerContext = LayerContext.DB; + protected datasetToUrl = new Map([ + ['Genealogy (100, data)', 'gedcom_coordinates_100_data.geojson'], + ['Genealogy (Full, no data)', 'gedcom_coordinates_full.geojson'], + ['Landkreise (D)', 'landkreise_simplify200.geojson'], + ['Basel Stadt Bevölkerung Quartiere', 'bs-stadt-bevoelkerung.geojson'], + ['Leeds Litter Bins', 'LitterBins20211201.geojson'], + ['Bern Urban Heat', 'bern-urban-heat.json'], + ]); + protected polyphenyDatasets = Array.from(this.datasetToUrl.keys()); + protected selectedPolyphenyDataset = ''; + + // Correctly set height. + private pollingTimer: any; + private pollingDuration: number = 3000; // 3 seconds + private pollInterval: number = 500; // Poll every 500ms + private lastHeight: string = ""; + + constructor( + protected layerSettings: LayerSettingsService, + private el: ElementRef, + private renderer: Renderer2 + // private cdRef: ChangeDetectorRef, + ) { + } + + ngAfterViewInit(): void { + this.startPollingHeight(); + } + + ngOnInit(): void { + this.layerSettings.layers$.subscribe((layers) => { + if (!layers) { + return; + } + + this.layers = layers; + this.renderedLayers = this.deepCopyLayers(layers); + this.layerSettings.setCanRerenderLayers(false); + + // Disable Change Detection and manually trigger to prevent rerenders when mouse moves + // this.cdRef.markForCheck(); + }); + + this.layerSettings.modifiedVisualization$.subscribe((config) => { + if (!config) { + return; + } + + const canRerenderLayers = !isEqual( + this.deepCopyLayers(this.layers), + this.renderedLayers, + ); + console.log('Config changed. Rerender layers?', canRerenderLayers); + this.layerSettings.setCanRerenderLayers(canRerenderLayers); + }); + + this.layerSettings.rerenderButtonClicked$.subscribe(() => { + this.updateLayers(this.layers); + }); + + this.updateLayers(getSampleMapLayers()); + } + + @HostListener('window:resize') + onResize(): void { + this.setMapHeight(this.getMapHeight()); + } + + private startPollingHeight(): void { + // A bit dirty, but it works for now. For some reason, a second after the map is created, the size changes. + // We just poll for the first few seconds after the component is created, and update the size if it changes. + const endTime = Date.now() + this.pollingDuration; + this.pollingTimer = setInterval(() => { + const currentHeight = this.getMapHeight(); + if (currentHeight != this.lastHeight){ + this.setMapHeight(currentHeight); + } else { + if (Date.now() > endTime){ + clearInterval(this.pollingTimer); + } + } + }, this.pollInterval); + } + + private getMapHeight(): string { + return `${(document.querySelector("#map") as HTMLElement).offsetHeight}px` + } + + private setMapHeight(mapHeight: string): void { + this.lastHeight = mapHeight; + this.renderer.setStyle(this.el.nativeElement, 'height', mapHeight); + } + + deepCopyLayers(layers: MapLayer[]) { + return layers.map((layer) => layer.copy()); + } + + async loadGeoJsonFile($event: Event) { + if (!event) { + return; + } + try { + const input = event.target as HTMLInputElement; + if (input.files && input.files.length) { + const file = input.files[0]; + this.loadedGeoJsonFileName = file.name; + this.loadedGeoJsonFile = JSON.parse(await file.text()); + } + } catch (error) { + if (error instanceof Error) { + alert(`Failed to load file: ${error.message}`); + } + this.loadedGeoJsonFile = undefined; + } + } + + removeLayer(layer: MapLayer) { + layer.isRemoved = true; + if (layer.isActive) { + this.toggleLayerVisibility(layer); + this.updateLayerUi(); + } + } + + onBaseLayerChange(selectedLayer: BaseLayer): void { + this.layerSettings.setBaseLayer(selectedLayer.value); + } + + // onLayerVisualizationChange( + // selectedLayer: MapLayer, + // selectedVisualization: Visualization, + // ) { + // selectedLayer.updateConfigInjector(); + // this.layerSettings.visualizationConfigurationChanged( + // selectedLayer.visualization, + // ); + // } + + addLayerInternal(layer: MapLayer) { + const newLayers = [ + layer, + ...this.layers.filter((l) => !l.isRemoved), + ].map((v, i) => { + v.index = i + 1; + return v; + }); + this.updateLayers(newLayers); + } + + async fetchGeoJsonFile(url: string): Promise { + const response = await window.fetch(url, { + method: 'GET', + headers: { + 'content-type': 'application/json;charset=UTF-8', + }, + }); + const geojson: GeoJSON.FeatureCollection = await response.json(); + return geojson; + } + + async addLayer() { + switch (this.addLayerMode) { + case LayerContext.Query: + case LayerContext.Results: + case LayerContext.DB: + if (!this.selectedPolyphenyDataset) { + break; + } + const url = `assets/${this.datasetToUrl.get(this.selectedPolyphenyDataset)}`; + const geojson = await this.fetchGeoJsonFile(url); + const layer = new MapLayer( + this.selectedPolyphenyDataset, + ).addData( + geojson.features.map( + (f, i) => + new RowResult( + i, + f.geometry, + f.properties ? f.properties : {}, + ), + ), + ); + this.addLayerInternal(layer); + break; + case LayerContext.External: + if (this.loadedGeoJsonFile) { + const layer = new MapLayer( + this.loadedGeoJsonFileName, + ).addData( + this.loadedGeoJsonFile.features.map( + (f, i) => + new RowResult( + i, + f.geometry, + f.properties ? f.properties : {}, + ), + ), + ); + console.log('Added GeoJSON layer: ', layer); + this.addLayerInternal(layer); + } else { + alert(`No file selected / File could not be loaded.`); + } + break; + } + this.isAddLayerModalVisible = false; + } + + dropLayer(event: CdkDragDrop) { + // TODO: After dropping a layer, the UI is very buggy + moveItemInArray(this.layers, event.previousIndex, event.currentIndex); + this.updateLayers(this.layers); + } + + updateLayers(newLayers: MapLayer[]) { + this.layerSettings.setLayers(newLayers); + this.updateLayerUi(); + } + + toggleLayerVisibility(layer: MapLayer) { + layer.isActive = !layer.isActive; + this.layerSettings.toggleLayerVisibility(layer); + } + + toggleAddLayerModalVisibility() { + this.isAddLayerModalVisible = !this.isAddLayerModalVisible; + } + + addLayerModalVisibilityChanged(event: any) { + this.isAddLayerModalVisible = event; + } + + updateLayerUi() { + // Layers which are first in the array are rendered first, and will be drawn over by other layers. + // Layers: BOTTOM -> TOP + const visibleLayers = this.layers.filter((d) => !d.isRemoved); + for (let i = 0; i < visibleLayers.length; i++) { + visibleLayers[visibleLayers.length - 1 - i].index = i + 1; + } + this.anyLayersVisible = + this.layers.filter((d) => !d.isRemoved).length > 0; + } + + protected readonly Object = Object; + protected readonly LayerContext = LayerContext; +} diff --git a/src/app/views/querying/gis/components/map/map.component.css b/src/app/views/querying/gis/components/map/map.component.css new file mode 100644 index 00000000..694a796e --- /dev/null +++ b/src/app/views/querying/gis/components/map/map.component.css @@ -0,0 +1,48 @@ +.map-container { + position: relative; + height: 100%; + border: 1px solid lightgray; +} + +#map { + position: absolute; + inset: 0; + z-index: 1; +} + +.map-overlay { + position: absolute; + inset: 0; + z-index: 2; + pointer-events: none; + display: grid; + background: rgba(0, 0, 0, 0.1); + place-items: center; +} + +.spinner-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + background: rgba(0, 0, 0, 0.25); + padding: 2rem; +} + +.render-button { + position: absolute; + z-index: 3; + bottom: 2rem; + right: 2rem; +} + +.spinner-container > span { + font-size: 1.5rem; + font-weight: 400; + color: white; +} + + +.leaflet-overlay-pane > svg { + transition: opacity 50ms ease-in-out; +} diff --git a/src/app/views/querying/gis/components/map/map.component.html b/src/app/views/querying/gis/components/map/map.component.html new file mode 100644 index 00000000..297052cf --- /dev/null +++ b/src/app/views/querying/gis/components/map/map.component.html @@ -0,0 +1,14 @@ +
    +
    +
    +
    + + {{ this.isLoadingMessage }} + + {{ this.isLoadingMessage }} +
    +
    + +
    diff --git a/src/app/views/querying/gis/components/map/map.component.ts b/src/app/views/querying/gis/components/map/map.component.ts new file mode 100644 index 00000000..89af30ff --- /dev/null +++ b/src/app/views/querying/gis/components/map/map.component.ts @@ -0,0 +1,380 @@ +import { AfterViewInit, Component, OnInit } from '@angular/core'; +import * as d3 from 'd3'; +import { GeoPath, GeoPermissibleObjects } from 'd3'; +import * as d3Geo from 'd3-geo'; +import * as L from 'leaflet'; +import { LayerSettingsService } from '../../services/layersettings.service'; +import { MapLayer } from '../../models/MapLayer.model'; +import { RowResult } from '../../models/RowResult.model'; +import { ButtonDirective, SpinnerComponent } from '@coreui/angular'; +import { NgIf } from '@angular/common'; + +@Component({ + selector: 'app-map', + standalone: true, + imports: [SpinnerComponent, NgIf, ButtonDirective], + templateUrl: './map.component.html', + styleUrls: ['./map.component.css'], +}) +export class MapComponent implements OnInit, AfterViewInit { + currentBaseLayer: L.TileLayer | undefined; + layers: MapLayer[] = []; + isLoading: boolean = false; + isLoadingMessage: string = 'TODO isLoadingMessage'; + canRerenderLayers: boolean = false; + + readonly MIN_ZOOM = 0; + readonly MAX_ZOOM = 19; + readonly INITIAL_ZOOM = 6; + private map!: L.Map; + private svg: + | d3.Selection + | undefined; + private g: d3.Selection | undefined; + private circles: + | d3.Selection + | undefined; + private paths: + | d3.Selection + | undefined; + private pathGenerator!: GeoPath; + private tooltip!: d3.Selection; + + constructor(protected layerSettings: LayerSettingsService) {} + + ngOnInit() { + this.layerSettings.selectedBaseLayer$.subscribe((item) => { + if (!item) { + return; + } + + if (this.currentBaseLayer) { + this.map.removeLayer(this.currentBaseLayer); + } + + if (item != 'EMPTY') { + this.currentBaseLayer = L.tileLayer(item, { + maxZoom: this.MAX_ZOOM, + attribution: + '© OpenStreetMap contributors', + }).addTo(this.map); + } + }); + + this.layerSettings.layers$.subscribe((layers) => { + if (!layers || !layers.length) { + return; + } + + this.layers = layers; + this.renderLayersWithD3(); + }); + + this.layerSettings.canRerenderLayers$.subscribe((canRerenderLayers) => { + this.canRerenderLayers = canRerenderLayers; + }); + + this.layerSettings.toggleLayerVisibility$.subscribe((layer) => { + this.toggleLayerVisibility(layer); + }); + } + + ngAfterViewInit(): void { + const leafletMap = L.map('map').setView([52, 10], this.INITIAL_ZOOM); + this.map = leafletMap; + this.svg = d3.select(this.map.getPanes().overlayPane).append('svg'); + this.g = this.svg.append('g').attr('class', 'leaflet-zoom-hide'); + this.tooltip = d3 + .select('body') + .append('div') + .style('position', 'absolute') + .style('font-size', '0.75rem') + .style('font-family', 'monospace') + .style('background', 'white') + .style('border', '1px solid #ccc') + .style('padding', '5px') + .style('display', 'none') + .style('z-index', '9999'); + + function projectPoint(this: any, x: number, y: number) { + const point = leafletMap.latLngToLayerPoint(new L.LatLng(y, x)); + this.stream.point(point.x, point.y); + } + + const transform = d3Geo.geoTransform({ point: projectPoint }); + this.pathGenerator = d3Geo.geoPath().projection(transform); + + this.map.on('zoomend', () => { + this.updateSvgPosition(); + }); + this.map.on('moveend', () => { + if (!this.svg || !this.g) { + return; + } + const bounds = this.map.getBounds(); + const topLeft = this.map.latLngToLayerPoint(bounds.getNorthWest()); + const bottomRight = this.map.latLngToLayerPoint( + bounds.getSouthEast(), + ); + this.svg + .style('width', '999999px') + .style('height', '999999px') + .style('left', topLeft.x + 'px') + .style('top', topLeft.y + 'px'); + this.g.attr('transform', `translate(${-topLeft.x}, ${-topLeft.y})`); + }); + + this.currentBaseLayer = L.tileLayer( + 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + { + maxZoom: this.MAX_ZOOM, + attribution: + '© OpenStreetMap contributors', + }, + ).addTo(this.map); + } + + showLoadingSpinner(message: string) { + this.isLoading = true; + this.isLoadingMessage = message; + } + + updateSvgPosition() { + this.showLoadingSpinner('Reposition shapes on map'); + + setTimeout(() => { + try { + if (!this.g || !this.svg) { + return; + } + + if (this.paths) { + this.paths.attr('d', (d) => this.pathGenerator(d.geometry)); + } + + if (this.circles) { + this.circles + .each((d) => { + const layerPoint = this.map.latLngToLayerPoint([ + d.getPoint()!.coordinates[1], + d.getPoint()!.coordinates[0], + ]); + d.cache['x'] = layerPoint.x; + d.cache['y'] = layerPoint.y; + }) + .attr('cx', (d) => d.cache['x']) + .attr('cy', (d) => d.cache['y']); + } + + const bounds = this.map.getBounds(); + const topLeft = this.map.latLngToLayerPoint( + bounds.getNorthWest(), + ); + const bottomRight = this.map.latLngToLayerPoint( + bounds.getSouthEast(), + ); + this.svg + .style('width', '999999px') + .style('height', '999999px') + .style('left', topLeft.x + 'px') + .style('top', topLeft.y + 'px'); + this.g.attr( + 'transform', + `translate(${-topLeft.x}, ${-topLeft.y})`, + ); + } finally { + this.isLoading = false; + } + }, 0); + } + + renderLayersWithD3() { + this.showLoadingSpinner('Rendering layers'); + + setTimeout(() => { + try { + if (!this.svg || !this.g) { + return; + } + + // Remove all previously added elements + this.g.selectAll('*').remove(); + + const points: RowResult[] = []; + const paths: RowResult[] = []; + + // Add shapes from each layer to array + for (const layer of this.layers.slice().reverse()) { + console.log(`Render layer [${layer.name}]. Initialize...`); + + // Initialize all configs + layer.pointShapeVisualization.init(layer.data); + layer.areaShapeVisualization.init(layer.data); + layer.colorVisualization.init(layer.data); + + points.push( + ...layer.data.filter( + (d) => d.geometry.type === 'Point', + ), + ); + paths.push( + ...layer.data.filter( + (d) => d.geometry.type !== 'Point', + ), + ); + } + + // Render all points + // TODO: Circles are always on the bottom this way... + console.log('Create Points: ', points); + this.circles = this.createPoints(points); + + console.log('Create Paths: ', paths); + this.paths = this.createPaths(paths); + + // Set SVG position correctly + this.updateSvgPosition(); + + // Center the map around the data. + // TODO: Do the same thing for paths. + const latLngs : L.LatLng[] = []; + this.circles!.each(d => { + // d has the property geometry, which is a GeoJSON point of type + latLngs.push(L.latLng(d.getPoint().coordinates[1], d.getPoint().coordinates[0],)); + }); + console.log(latLngs) + // TODO: Currently, only do this, if the dataset is not too big. Otherwise we zoom way out, + // and zooming back in can be very slow. (if the points are all over the world) + if (latLngs.length > 0 && latLngs.length <= 1000) { + const latLngBounds = L.latLngBounds(latLngs); + if (latLngBounds.isValid()){ + this.map.fitBounds(latLngBounds); + } + } + } finally { + this.isLoading = false; + } + }, 0); + } + + createPoints(points: RowResult[]) { + if (!this.g) { + return; + } + + const tt = this.tooltip; + + return this.g + .selectAll('circle') + .data(points) + .enter() + .append('circle') + .attr('layer-name', (d) => d.layer!.name) + .attr('layer-index', (d) => d.layer!.index.toString()) + .attr('r', (d) => + d.layer!.pointShapeVisualization.getValueForAttribute('r', d), + ) + .attr('fill', (d) => + d.layer!.colorVisualization.getValueForAttribute('fill', d), + ) + .each((d) => { + const layerPoint = this.map.latLngToLayerPoint([ + d.getPoint().coordinates[1], + d.getPoint().coordinates[0], + ]); + d.cache['x'] = layerPoint.x; + d.cache['y'] = layerPoint.y; + }) + .attr('cx', (d) => d.cache['x']) + .attr('cy', (d) => d.cache['y']) + .style("pointer-events", "auto") + .style("cursor", "pointer") + .on('mouseover', function (event, d) { + tt.style('display', 'block').html(JSON.stringify(d.data, null, 1)); + }) + .on('mousemove', function (event) { + tt.style('top', event.pageY + 10 + 'px').style( + 'left', + event.pageX + 10 + 'px', + ); + }) + .on('mouseout', function () { + tt.style('display', 'none'); + }); + } + + createPaths(paths: RowResult[]) { + if (!this.g) { + return; + } + + const tt = this.tooltip + + return this.g + .selectAll('.paths') + .data(paths) + .enter() + .append('path') + .attr('layer-name', (d) => d.layer!.name) + .attr('layer-index', (d) => d.layer!.index.toString()) + .attr('d', (d) => this.pathGenerator(d.geometry)) + .attr('stroke-width', (d) => + d.layer!.areaShapeVisualization.getValueForAttribute( + 'stroke-width', + d, + ), + ) + .attr('stroke', (d) => + d.layer!.colorVisualization.getValueForAttribute('stroke', d), + ) + .attr('fill', (d) => + d.layer!.colorVisualization.getValueForAttribute('fill', d), + ) + .attr('fill-opacity', (d) => + d.layer!.colorVisualization.getValueForAttribute( + 'fill-opacity', + d, + ), + ) + .style("pointer-events", "auto") + .style("cursor", "pointer") + .on('mouseover', function (event, d) { + console.log("hover") + tt.style('display', 'block').html(JSON.stringify(d.data, null, 2)); + }) + .on('mousemove', function (event) { + tt.style('top', event.pageY + 10 + 'px').style( + 'left', + event.pageX + 10 + 'px', + ); + }) + .on('mouseout', function () { + tt.style('display', 'none'); + }); + } + + toggleLayerVisibility(layer: MapLayer) { + if (!this.g?.node()) { + throw new Error('SVG g does not exist.'); + } + + const layerElements = this.g + .node()! + .querySelectorAll( + `[layer-name='${layer.name}'][layer-index='${layer.index.toString()}']`, + ); + + if (!layerElements.length) { + // Nothing to do + return; + } + + layerElements.forEach((elem) => { + if (layer.isActive) { + elem.classList.remove('layer-hidden'); + } else { + elem.classList.add('layer-hidden'); + } + }); + } +} diff --git a/src/app/views/querying/gis/components/visualization/area-shape-visualization.model.ts b/src/app/views/querying/gis/components/visualization/area-shape-visualization.model.ts new file mode 100644 index 00000000..be74e39c --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/area-shape-visualization.model.ts @@ -0,0 +1,39 @@ +import { Visualization } from '../../models/visualization.interface'; +import { RowResult } from '../../models/RowResult.model'; +import { EmptyComponent } from './empty/empty.component'; +import {AreaShapeComponent} from "./area-shape/area-shape.component"; + +export class AreaShapeVisualization implements Visualization { + name = 'Area Shape'; + // TODO: Change to own component + configurationComponentType = AreaShapeComponent; + + outlineThickness: number; + modes: string[] = ['Solid']; + selectedMode: string = this.modes[0]; + + constructor(outlineThickness: number) { + this.outlineThickness = outlineThickness; + } + + init(data: RowResult[]): void { + // + } + + copy(): Visualization { + const copy = new AreaShapeVisualization(this.outlineThickness); + copy.selectedMode = this.selectedMode; + return copy; + } + + getValueForAttribute(attr: string, data: RowResult): string | number { + switch (attr) { + case 'stroke-width': + return this.outlineThickness; + } + + throw new Error(`Visualization does not support attribute [${attr}]`); + } + + apply(): void {} +} diff --git a/src/app/views/querying/gis/components/visualization/area-shape/area-shape.component.css b/src/app/views/querying/gis/components/visualization/area-shape/area-shape.component.css new file mode 100644 index 00000000..7c613be9 --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/area-shape/area-shape.component.css @@ -0,0 +1,5 @@ +div { + display: flex; + flex-direction: column; + gap: 1rem; +} diff --git a/src/app/views/querying/gis/components/visualization/area-shape/area-shape.component.html b/src/app/views/querying/gis/components/visualization/area-shape/area-shape.component.html new file mode 100644 index 00000000..901b40b7 --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/area-shape/area-shape.component.html @@ -0,0 +1,27 @@ +
    + + Outline Thickness + + + + + + + + +
    diff --git a/src/app/views/querying/gis/components/visualization/area-shape/area-shape.component.ts b/src/app/views/querying/gis/components/visualization/area-shape/area-shape.component.ts new file mode 100644 index 00000000..5c3a57a8 --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/area-shape/area-shape.component.ts @@ -0,0 +1,46 @@ +import { Component, Inject } from '@angular/core'; +import { VisualizationConfiguration } from '../../../models/visualization-configuration.interface'; +import { FormsModule } from '@angular/forms'; +import { LayerSettingsService } from '../../../services/layersettings.service'; +import { + FormCheckComponent, + FormCheckInputDirective, + FormCheckLabelDirective, + FormControlDirective, + FormSelectDirective, + InputGroupComponent, + InputGroupTextDirective, +} from '@coreui/angular'; +import { NgForOf, NgIf } from '@angular/common'; +import {AreaShapeVisualization} from "../area-shape-visualization.model"; + +@Component({ + selector: 'app-area-shape', + standalone: true, + imports: [ + FormsModule, + InputGroupComponent, + InputGroupTextDirective, + FormControlDirective, + FormSelectDirective, + NgForOf, + NgIf, + FormCheckComponent, + FormCheckInputDirective, + FormCheckLabelDirective, + ], + templateUrl: './area-shape.component.html', + styleUrl: './area-shape.component.css', +}) +export class AreaShapeComponent implements VisualizationConfiguration { + constructor( + @Inject('config') protected config: AreaShapeVisualization, + private layerSettings: LayerSettingsService, + ) { + // + } + + configChanged() { + this.layerSettings.visualizationConfigurationChanged(this.config); + } +} diff --git a/src/app/views/querying/gis/components/visualization/color-visualization-model.ts b/src/app/views/querying/gis/components/visualization/color-visualization-model.ts new file mode 100644 index 00000000..2e320cd4 --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/color-visualization-model.ts @@ -0,0 +1,97 @@ +import { Visualization } from '../../models/visualization.interface'; +import { RowResult } from '../../models/RowResult.model'; +import * as d3 from 'd3'; +import * as turf from '@turf/turf'; +import { ColorComponent } from './color/color.component'; + +export class ColorVisualization implements Visualization { + name = 'Color'; + configurationComponentType = ColorComponent; + + modes: string[] = ['Static', 'Gradient']; + selectedMode: string = this.modes[0]; + + // Static + color: string; + fillOpacity: number = 0.25; + + // Gradient + fieldName: string = ''; + normalizeByArea: boolean = false; + colorScale?: d3.ScaleSequential; + + constructor(color: string) { + this.color = color; + } + + init(data: RowResult[]): void { + if (this.selectedMode === this.modes[0]) { + // Nothing to do + return; + } + + const values = this.normalizeByArea + ? data + .map((d) => [ + d.getNumberValueFromField(this.fieldName), + turf.area(d.geometry), + ]) + .filter( + (v): v is [number, number] => + !isNaN(v[0]) && !isNaN(v[1]), + ) + .map((v) => (v[1] > 0 ? v[0] / v[1] : 0)) + : data + .map((d) => d.getNumberValueFromField(this.fieldName)) + .filter((v): v is number => !isNaN(v)); + + console.log('values', values); + + const minValue = d3.min(values) ?? 0; + const maxValue = d3.max(values) ?? 1; + console.log('minValue', minValue); + console.log('maxValue', maxValue); + + this.colorScale = d3 + .scaleSequential(d3.interpolateHsl('lightblue', 'darkblue')) + .domain([minValue, maxValue]); + console.log('this.colorScale', this.colorScale); + } + + copy(): Visualization { + const copy = new ColorVisualization(this.color); + copy.selectedMode = this.selectedMode; + copy.fillOpacity = this.fillOpacity; + copy.fieldName = this.fieldName; + copy.normalizeByArea = this.normalizeByArea; + return copy; + } + + getValueForAttribute(attr: string, data: RowResult): string | number { + switch (attr) { + case 'stroke': + switch (this.selectedMode) { + case 'Static': + return this.color; + case 'Gradient': + return 'black'; + } + return this.color; + case 'fill-opacity': + return this.fillOpacity; + case 'fill': + switch (this.selectedMode) { + case 'Static': + return this.color; + case 'Gradient': + return this.colorScale!( + data.getNumberValueFromField(this.fieldName), + ); + } + } + + throw new Error(`Visualization does not support attribute [${attr}]`); + } + + apply(): void {} +} diff --git a/src/app/views/querying/gis/components/visualization/color/color.component.css b/src/app/views/querying/gis/components/visualization/color/color.component.css new file mode 100644 index 00000000..7c613be9 --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/color/color.component.css @@ -0,0 +1,5 @@ +div { + display: flex; + flex-direction: column; + gap: 1rem; +} diff --git a/src/app/views/querying/gis/components/visualization/color/color.component.html b/src/app/views/querying/gis/components/visualization/color/color.component.html new file mode 100644 index 00000000..5a4e1174 --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/color/color.component.html @@ -0,0 +1,55 @@ +
    + + + + + + + Color + + + + + Area Fill Opacity + + + + + Variable + + + + + + + + +
    diff --git a/src/app/views/querying/gis/components/visualization/color/color.component.ts b/src/app/views/querying/gis/components/visualization/color/color.component.ts new file mode 100644 index 00000000..41d6ce53 --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/color/color.component.ts @@ -0,0 +1,46 @@ +import { Component, Inject } from '@angular/core'; +import { VisualizationConfiguration } from '../../../models/visualization-configuration.interface'; +import { FormsModule } from '@angular/forms'; +import { LayerSettingsService } from '../../../services/layersettings.service'; +import { + FormCheckComponent, + FormCheckInputDirective, + FormCheckLabelDirective, + FormControlDirective, + FormSelectDirective, + InputGroupComponent, + InputGroupTextDirective, +} from '@coreui/angular'; +import { ColorVisualization } from '../color-visualization-model'; +import { NgForOf, NgIf } from '@angular/common'; + +@Component({ + selector: 'app-color', + standalone: true, + imports: [ + FormsModule, + InputGroupComponent, + InputGroupTextDirective, + FormControlDirective, + FormSelectDirective, + NgForOf, + NgIf, + FormCheckComponent, + FormCheckInputDirective, + FormCheckLabelDirective, + ], + templateUrl: './color.component.html', + styleUrl: './color.component.css', +}) +export class ColorComponent implements VisualizationConfiguration { + constructor( + @Inject('config') protected config: ColorVisualization, + private layerSettings: LayerSettingsService, + ) { + // + } + + configChanged() { + this.layerSettings.visualizationConfigurationChanged(this.config); + } +} diff --git a/src/app/views/querying/gis/components/visualization/empty-visualization.model.ts b/src/app/views/querying/gis/components/visualization/empty-visualization.model.ts new file mode 100644 index 00000000..7a60595d --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/empty-visualization.model.ts @@ -0,0 +1,23 @@ +import { Type } from '@angular/core'; +import { RowResult } from '../../models/RowResult.model'; +import { VisualizationConfiguration } from '../../models/visualization-configuration.interface'; +import { Visualization } from '../../models/visualization.interface'; +import {EmptyComponent} from "./empty/empty.component"; + +export class EmptyVisualization implements Visualization { + name: string = "Empty"; + configurationComponentType: Type = EmptyComponent; + + apply(): void { + throw new Error('Method not implemented.'); + } + copy(): Visualization { + throw new Error('Method not implemented.'); + } + init(data: RowResult[]): void { + throw new Error('Method not implemented.'); + } + getValueForAttribute(attr: string, data: RowResult): string | number { + throw new Error('Method not implemented.'); + } +} diff --git a/src/app/views/querying/gis/components/visualization/empty/empty.component.html b/src/app/views/querying/gis/components/visualization/empty/empty.component.html new file mode 100644 index 00000000..8811d3d6 --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/empty/empty.component.html @@ -0,0 +1 @@ +Empty diff --git a/src/app/views/querying/gis/components/visualization/empty/empty.component.ts b/src/app/views/querying/gis/components/visualization/empty/empty.component.ts new file mode 100644 index 00000000..85c9421d --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/empty/empty.component.ts @@ -0,0 +1,17 @@ +import {Component, Inject} from '@angular/core'; +import {VisualizationConfiguration} from "../../../models/visualization-configuration.interface"; +import {ColorVisualization} from "../color-visualization-model"; +import {LayerSettingsService} from "../../../services/layersettings.service"; + +@Component({ + selector: 'app-empty', + standalone: true, + imports: [], + templateUrl: './empty.component.html', +}) +export class EmptyComponent implements VisualizationConfiguration { + constructor( + @Inject('config') protected config: ColorVisualization, + private layerSettings: LayerSettingsService, + ) {} +} diff --git a/src/app/views/querying/gis/components/visualization/label-visualization-model.ts b/src/app/views/querying/gis/components/visualization/label-visualization-model.ts new file mode 100644 index 00000000..5ac4f2b5 --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/label-visualization-model.ts @@ -0,0 +1,29 @@ +import { Visualization } from '../../models/visualization.interface'; +import { RowResult } from '../../models/RowResult.model'; +import {LabelComponent} from "./label/label.component"; + +export class LabelVisualization implements Visualization { + name = 'Label'; + configurationComponentType = LabelComponent; + + fieldName: string = ''; + + constructor() {} + + init(data: RowResult[]): void { + // + } + + copy(): Visualization { + const copy = new LabelVisualization(); + copy.fieldName = this.fieldName; + return copy; + } + + getValueForAttribute(attr: string, data: RowResult): string | number { + return 'TODO'; + // throw new Error(`Visualization does not support attribute [${attr}]`); + } + + apply(): void {} +} diff --git a/src/app/views/querying/gis/components/visualization/label/label.component.css b/src/app/views/querying/gis/components/visualization/label/label.component.css new file mode 100644 index 00000000..7c613be9 --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/label/label.component.css @@ -0,0 +1,5 @@ +div { + display: flex; + flex-direction: column; + gap: 1rem; +} diff --git a/src/app/views/querying/gis/components/visualization/label/label.component.html b/src/app/views/querying/gis/components/visualization/label/label.component.html new file mode 100644 index 00000000..14ba5d14 --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/label/label.component.html @@ -0,0 +1,11 @@ +
    + + Text + + +
    diff --git a/src/app/views/querying/gis/components/visualization/label/label.component.ts b/src/app/views/querying/gis/components/visualization/label/label.component.ts new file mode 100644 index 00000000..195a1a65 --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/label/label.component.ts @@ -0,0 +1,47 @@ +import { Component, Inject } from '@angular/core'; +import { VisualizationConfiguration } from '../../../models/visualization-configuration.interface'; +import { FormsModule } from '@angular/forms'; +import { LayerSettingsService } from '../../../services/layersettings.service'; +import { + FormCheckComponent, + FormCheckInputDirective, + FormCheckLabelDirective, + FormControlDirective, + FormSelectDirective, + InputGroupComponent, + InputGroupTextDirective, +} from '@coreui/angular'; +import { NgForOf, NgIf } from '@angular/common'; +import {AreaShapeVisualization} from "../area-shape-visualization.model"; +import {LabelVisualization} from "../label-visualization-model"; + +@Component({ + selector: 'app-area-shape', + standalone: true, + imports: [ + FormsModule, + InputGroupComponent, + InputGroupTextDirective, + FormControlDirective, + FormSelectDirective, + NgForOf, + NgIf, + FormCheckComponent, + FormCheckInputDirective, + FormCheckLabelDirective, + ], + templateUrl: './label.component.html', + styleUrl: './label.component.css', +}) +export class LabelComponent implements VisualizationConfiguration { + constructor( + @Inject('config') protected config: LabelVisualization, + private layerSettings: LayerSettingsService, + ) { + // + } + + configChanged() { + this.layerSettings.visualizationConfigurationChanged(this.config); + } +} diff --git a/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts b/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts new file mode 100644 index 00000000..ff937041 --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts @@ -0,0 +1,41 @@ +import { Visualization } from '../../models/visualization.interface'; +import { RowResult } from '../../models/RowResult.model'; +import { PointShapeComponent } from './point-shape/point-shape.component'; + +export class PointShapeVisualization implements Visualization { + name = 'Point Shape'; + configurationComponentType = PointShapeComponent; + + modes: string[] = ['Circle', 'Icon']; + selectedMode: string = this.modes[0]; + size: number; + fieldName: string = ''; + + constructor(size: number) { + this.size = size; + } + + init(data: RowResult[]): void { + // + } + + copy(): Visualization { + const copy = new PointShapeVisualization(this.size); + copy.selectedMode = this.selectedMode; + copy.fieldName = this.fieldName; + return copy; + } + + getValueForAttribute(attr: string, data: RowResult): string | number { + if (data.isPoint()) { + switch (attr) { + case 'r': + return this.size; + } + } + + throw new Error(`Visualization does not support attribute [${attr}]`); + } + + apply(): void {} +} diff --git a/src/app/views/querying/gis/components/visualization/point-shape/point-shape.component.css b/src/app/views/querying/gis/components/visualization/point-shape/point-shape.component.css new file mode 100644 index 00000000..7c613be9 --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/point-shape/point-shape.component.css @@ -0,0 +1,5 @@ +div { + display: flex; + flex-direction: column; + gap: 1rem; +} diff --git a/src/app/views/querying/gis/components/visualization/point-shape/point-shape.component.html b/src/app/views/querying/gis/components/visualization/point-shape/point-shape.component.html new file mode 100644 index 00000000..63842066 --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/point-shape/point-shape.component.html @@ -0,0 +1,37 @@ +
    + + Size + + + + + + + + + + Variable + + + +
    diff --git a/src/app/views/querying/gis/components/visualization/point-shape/point-shape.component.ts b/src/app/views/querying/gis/components/visualization/point-shape/point-shape.component.ts new file mode 100644 index 00000000..254556d4 --- /dev/null +++ b/src/app/views/querying/gis/components/visualization/point-shape/point-shape.component.ts @@ -0,0 +1,47 @@ +import { Component, Inject } from '@angular/core'; +import { VisualizationConfiguration } from '../../../models/visualization-configuration.interface'; +import { FormsModule } from '@angular/forms'; +import { LayerSettingsService } from '../../../services/layersettings.service'; +import { + FormCheckComponent, + FormCheckInputDirective, + FormCheckLabelDirective, + FormControlDirective, + FormSelectDirective, + InputGroupComponent, + InputGroupTextDirective, +} from '@coreui/angular'; +import { ColorVisualization } from '../color-visualization-model'; +import { NgForOf, NgIf } from '@angular/common'; +import {PointShapeVisualization} from "../point-shape-visualization.model"; + +@Component({ + selector: 'app-point-shape', + standalone: true, + imports: [ + FormsModule, + InputGroupComponent, + InputGroupTextDirective, + FormControlDirective, + FormSelectDirective, + NgForOf, + NgIf, + FormCheckComponent, + FormCheckInputDirective, + FormCheckLabelDirective, + ], + templateUrl: './point-shape.component.html', + styleUrl: './point-shape.component.css', +}) +export class PointShapeComponent implements VisualizationConfiguration { + constructor( + @Inject('config') protected config: PointShapeVisualization, + private layerSettings: LayerSettingsService, + ) { + // + } + + configChanged() { + this.layerSettings.visualizationConfigurationChanged(this.config); + } +} diff --git a/src/app/views/querying/gis/gis.component.html b/src/app/views/querying/gis/gis.component.html index f7e4d269..84536ac3 100644 --- a/src/app/views/querying/gis/gis.component.html +++ b/src/app/views/querying/gis/gis.component.html @@ -1,4 +1,5 @@ - - Query by Map! + + + diff --git a/src/app/views/querying/gis/gis.component.scss b/src/app/views/querying/gis/gis.component.scss index e69de29b..1c91e928 100644 --- a/src/app/views/querying/gis/gis.component.scss +++ b/src/app/views/querying/gis/gis.component.scss @@ -0,0 +1,7 @@ +.master-detail { + display: grid; + grid-template-columns: auto 350px; + height: 100%; + max-height: 100%; + overflow: hidden; +} \ No newline at end of file diff --git a/src/app/views/querying/gis/models/LayerContext.model.ts b/src/app/views/querying/gis/models/LayerContext.model.ts new file mode 100644 index 00000000..86ad1a6f --- /dev/null +++ b/src/app/views/querying/gis/models/LayerContext.model.ts @@ -0,0 +1,6 @@ +export enum LayerContext { + Results = "Results", + Query = "Query", + DB = "Polypheny", + External = "External file (GeoJSON)" +} diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts new file mode 100644 index 00000000..24de17a8 --- /dev/null +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -0,0 +1,45 @@ +import { Visualization } from './visualization.interface'; +import { RowResult } from './RowResult.model'; +import { ColorVisualization } from '../components/visualization/color-visualization-model'; +import {AreaShapeVisualization} from "../components/visualization/area-shape-visualization.model"; +import {LabelVisualization} from "../components/visualization/label-visualization-model"; +import {PointShapeVisualization} from "../components/visualization/point-shape-visualization.model"; + +export class MapLayer { + name: string; + data: RowResult[] = []; + + pointShapeVisualization: Visualization = new PointShapeVisualization(3); + areaShapeVisualization: Visualization = new AreaShapeVisualization(1); + colorVisualization: Visualization = new ColorVisualization('red'); + labelVisualization: Visualization = new LabelVisualization(); + + // Computed (Not used in copy) + isActive: boolean = true; + isRemoved: boolean = false; + index: number = -1; + + constructor(name: string) { + this.name = name; + } + + copy() { + // Do not copy isActive and isRemoved, because we use the copy to check if + // anything changes so we need to rerender, but in these cases we do not need + // to rerender. + const copy = new MapLayer(this.name).addData( + this.data.map((d) => d.copy()), + ); + copy.pointShapeVisualization = this.pointShapeVisualization.copy(); + copy.areaShapeVisualization = this.areaShapeVisualization.copy(); + copy.colorVisualization = this.colorVisualization.copy(); + copy.labelVisualization = this.labelVisualization.copy(); + return copy; + } + + addData(data: RowResult[]) { + data.forEach((d) => (d.layer = this)); + this.data.push(...data); + return this; + } +} diff --git a/src/app/views/querying/gis/models/RowResult.model.ts b/src/app/views/querying/gis/models/RowResult.model.ts new file mode 100644 index 00000000..0f3c7e40 --- /dev/null +++ b/src/app/views/querying/gis/models/RowResult.model.ts @@ -0,0 +1,63 @@ +import {Geometry, Point} from 'geojson'; +import {MapLayer} from "./MapLayer.model"; + +/** + * Represents one row in the results returned by Polypheny. + */ +export class RowResult { + /** + * Used for styling if the results are ordered + */ + index: number; + geometry: Geometry; + data: Record = {}; + cache: Record = {}; + layer?: MapLayer = undefined; + + constructor( + index: number, + geometry: Geometry, + data: Record | undefined = undefined, + ) { + this.index = index; + this.geometry = geometry; + + if (data) { + this.data = data; + } + } + + isPoint(){ + return this.geometry.type == "Point" + } + + getPoint() { + if (this.isPoint()){ + return this.geometry as Point; + } + throw new Error("Can only call getPoint() if geometry is actually of type Point!") + } + + copy(){ + // We leave out cache on purpose, because we will use the copy to compare both if the layer has changed. + return new RowResult(this.index, this.geometry, this.data); + } + + getNumberValueFromField(fieldName: string): number { + let finalValue: any = this.data; + + for (const key of fieldName.split(".")) { + if (finalValue && typeof finalValue === "object") { + finalValue = finalValue[key]; + } else { + return NaN; + } + } + + if (typeof finalValue === "number") { + return finalValue; + } + + return NaN; + } +} diff --git a/src/app/views/querying/gis/models/get-sample-maplayers.ts b/src/app/views/querying/gis/models/get-sample-maplayers.ts new file mode 100644 index 00000000..6d1c0555 --- /dev/null +++ b/src/app/views/querying/gis/models/get-sample-maplayers.ts @@ -0,0 +1,305 @@ +import { MapLayer } from './MapLayer.model'; +import * as GeoJSON from 'geojson'; +import { RowResult } from './RowResult.model'; + +export function getSampleMapLayers(): MapLayer[] { + const data = ` + { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 13.554898, + 52.333956 + ] + }, + "properties": { + "PLAC": "Kiekebusch,,Dahme-Spreewald,BRANDENBURG,DEUTSCHLAND," + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 13.983899, + 52.027206 + ] + }, + "properties": { + "PLAC": "Krugau,,Dahme-Spreewald,BRANDENBURG,DEUTSCHLAND," + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 13.958323, + 52.059514 + ] + }, + "properties": { + "PLAC": "Kuschkow,,Dahme-Spreewald,BRANDENBURG,DEUTSCHLAND," + } + } + ] +}`; + const data2 = ` + { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.82575, + 52.56049 + ] + }, + "properties": { + "PLAC": "Dahlen,,,SACHSEN-ANHALT,DEUTSCHLAND," + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.61667, + 51.63333 + ] + }, + "properties": { + "PLAC": "Gerbstedt,,,SACHSEN-ANHALT,DEUTSCHLAND," + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.67008, + 52.077432 + ] + }, + "properties": { + "PLAC": "Salbke,39122,Magdeburg,SACHSEN-ANHALT,DEUTSCHLAND," + } + } + ] +}`; + const data3 = ` +{ + "type": "FeatureCollection", + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:OGC:1.3:CRS84" + } + }, + "source": "© GeoBasis-DE / BKG 2013 (Daten verändert)", + "features": [ + { + "type": "Feature", + "properties": { + "ADE": 4, + "GF": 4, + "BSG": 1, + "RS": "01001", + "AGS": "01001", + "SDV_RS": "010010000000", + "GEN": "Flensburg", + "BEZ": "Kreisfreie Stadt", + "IBZ": 40, + "BEM": "--", + "NBD": "ja", + "SN_L": "01", + "SN_R": "0", + "SN_K": "01", + "SN_V1": "00", + "SN_V2": "00", + "SN_G": "000", + "FK_S3": "R", + "NUTS": "DEF01", + "RS_0": "010010000000", + "AGS_0": "01001000", + "WSK": "2008/01/01", + "DEBKG_ID": "DEBKGDL20000002R", + "destatis": { + "population": 89504, + "population_m": 44599, + "population_w": 44905 + } + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 9.412664108896104, + 54.8226409083269 + ], + [ + 9.42293404920774, + 54.82322297871825 + ], + [ + 9.437558196380342, + 54.80891340493068 + ], + [ + 9.428511475527905, + 54.800088026152174 + ], + [ + 9.436697178516075, + 54.78874409302239 + ], + [ + 9.438340826643119, + 54.803003371483264 + ], + [ + 9.442017460283664, + 54.8045726244278 + ], + [ + 9.440602675507414, + 54.800542052052265 + ], + [ + 9.44377911558914, + 54.805138125360195 + ], + [ + 9.45272991922579, + 54.80754852411532 + ], + [ + 9.457996005091418, + 54.81748018625207 + ], + [ + 9.467334502006006, + 54.82363231794173 + ], + [ + 9.490818773428426, + 54.82360740385943 + ], + [ + 9.492051905584956, + 54.81484953235985 + ], + [ + 9.505431081139028, + 54.81020748745684 + ], + [ + 9.503832434139722, + 54.804173364192415 + ], + [ + 9.499697545164823, + 54.803620434251044 + ], + [ + 9.505415377819059, + 54.799599946181225 + ], + [ + 9.49897574907422, + 54.79865203655195 + ], + [ + 9.506569452474432, + 54.79209752788728 + ], + [ + 9.502180299595505, + 54.77927472276625 + ], + [ + 9.505817873713303, + 54.77311082549625 + ], + [ + 9.49333489486901, + 54.76925229907075 + ], + [ + 9.474710042387382, + 54.7723792229719 + ], + [ + 9.47224934519076, + 54.76692973048646 + ], + [ + 9.460273736377953, + 54.76028982884087 + ], + [ + 9.460965252316052, + 54.75421956913382 + ], + [ + 9.453087744581662, + 54.75211765297414 + ], + [ + 9.379014517860757, + 54.75320021546097 + ], + [ + 9.35722059154765, + 54.77948194407854 + ], + [ + 9.405606646774057, + 54.79555897805081 + ], + [ + 9.402263643524694, + 54.808318620169885 + ], + [ + 9.411513078436418, + 54.816008174158625 + ], + [ + 9.404694993761636, + 54.82248286917383 + ], + [ + 9.412664108896104, + 54.8226409083269 + ] + ] + ] + } + } + ] +} +`; + const geoJson: GeoJSON.FeatureCollection = JSON.parse(data); + const geoJson2: GeoJSON.FeatureCollection = JSON.parse(data2); + const geoJson3: GeoJSON.FeatureCollection = JSON.parse(data3); + + return [ + new MapLayer('a').addData( + geoJson.features.map((f, i) => new RowResult(i, f.geometry)), + ), + new MapLayer('b').addData( + geoJson2.features.map((f, i) => new RowResult(i, f.geometry)), + ), + new MapLayer('Landkreise').addData( + geoJson3.features.map((f, i) => new RowResult(i, f.geometry)), + ), + ]; +} diff --git a/src/app/views/querying/gis/models/visualization-configuration.interface.ts b/src/app/views/querying/gis/models/visualization-configuration.interface.ts new file mode 100644 index 00000000..3e746e16 --- /dev/null +++ b/src/app/views/querying/gis/models/visualization-configuration.interface.ts @@ -0,0 +1,3 @@ +export interface VisualizationConfiguration { + +} diff --git a/src/app/views/querying/gis/models/visualization.interface.ts b/src/app/views/querying/gis/models/visualization.interface.ts new file mode 100644 index 00000000..0aa602ab --- /dev/null +++ b/src/app/views/querying/gis/models/visualization.interface.ts @@ -0,0 +1,16 @@ +import { VisualizationConfiguration } from './visualization-configuration.interface'; +import { Type } from '@angular/core'; +import {RowResult} from "./RowResult.model"; + +export interface Visualization { + name: string; + configurationComponentType: Type; + + apply(): void; + + copy(): Visualization; + + init(data: RowResult[]): void; + + getValueForAttribute(attr: string, data: RowResult): string | number; +} diff --git a/src/app/views/querying/gis/services/layersettings.service.ts b/src/app/views/querying/gis/services/layersettings.service.ts new file mode 100644 index 00000000..190b9976 --- /dev/null +++ b/src/app/views/querying/gis/services/layersettings.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@angular/core'; +import {BehaviorSubject, Subject} from 'rxjs'; +import { MapLayer } from '../models/MapLayer.model'; +import {Visualization} from "../models/visualization.interface"; + +@Injectable({ + providedIn: 'root', +}) +export class LayerSettingsService { + private selectedBaseLayer = new BehaviorSubject('EMPTY'); + selectedBaseLayer$ = this.selectedBaseLayer.asObservable(); + setBaseLayer(item: string) { + this.selectedBaseLayer.next(item); + } + + private layers = new BehaviorSubject([]); + layers$ = this.layers.asObservable(); + setLayers(layers: MapLayer[]) { + this.layers.next(layers); + } + + private modifiedVisualization = new BehaviorSubject(null); + modifiedVisualization$ = this.modifiedVisualization.asObservable(); + visualizationConfigurationChanged(visualization: Visualization): void { + this.modifiedVisualization.next(visualization); + } + + private canRerenderLayers = new BehaviorSubject(false); + canRerenderLayers$ = this.canRerenderLayers.asObservable(); + setCanRerenderLayers(canRerenderMap: boolean){ + this.canRerenderLayers.next(canRerenderMap); + } + + private rerenderButtonClickedSubject = new Subject(); + rerenderButtonClicked$ = this.rerenderButtonClickedSubject.asObservable(); + rerenderButtonClicked(){ + this.rerenderButtonClickedSubject.next(); + } + + private toggleLayerVisibilitySubject = new Subject(); + toggleLayerVisibility$ = this.toggleLayerVisibilitySubject.asObservable(); + toggleLayerVisibility(layer: MapLayer){ + this.toggleLayerVisibilitySubject.next(layer); + } +} diff --git a/src/app/views/views.module.ts b/src/app/views/views.module.ts index 86b2cd38..bb988b41 100644 --- a/src/app/views/views.module.ts +++ b/src/app/views/views.module.ts @@ -96,6 +96,7 @@ import { import {EditEntityComponent} from './schema-editing/edit-entity/edit-entity.component'; import {TreeModule} from '@ali-hm/angular-tree-component'; import {GisComponent} from "./querying/gis/gis.component"; +import {MapLayersComponent} from "./querying/gis/components/layers/map-layers.component"; @NgModule({ @@ -164,7 +165,8 @@ import {GisComponent} from "./querying/gis/gis.component"; PlaceholderDirective, ProgressComponent, ProgressBarComponent, - CollapseDirective + CollapseDirective, + MapLayersComponent ], declarations: [ EditColumnsComponent, From d0261b71ebc3e71aae7ca85c73b097c8f0494f5c Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Sat, 30 Nov 2024 16:48:14 +0100 Subject: [PATCH 05/60] Add code back in to render circles and paths on map --- .../data-view/data-map/data-map.component.ts | 368 ++++++++++++++++-- .../components/layers/map-layers.component.ts | 6 +- .../gis/components/map/map.component.ts | 14 +- .../area-shape-visualization.model.ts | 6 +- .../color-visualization-model.ts | 6 +- .../empty-visualization.model.ts | 6 +- .../label-visualization-model.ts | 6 +- .../point-shape-visualization.model.ts | 6 +- .../querying/gis/models/MapLayer.model.ts | 6 +- .../querying/gis/models/RowResult.model.ts | 4 +- .../gis/models/get-sample-maplayers.ts | 8 +- .../gis/models/visualization.interface.ts | 6 +- 12 files changed, 364 insertions(+), 78 deletions(-) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index b771c28d..176b2088 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -4,6 +4,9 @@ import * as d3 from 'd3'; import { GeoPath, GeoPermissibleObjects } from 'd3'; import * as d3Geo from 'd3-geo'; import * as L from 'leaflet'; +import {LayerSettingsService} from "../../../views/querying/gis/services/layersettings.service"; +import {MapLayer} from "../../../views/querying/gis/models/MapLayer.model"; +import {MapGeometryWithData} from "../../../views/querying/gis/models/RowResult.model"; @Component({ selector: 'app-data-map', @@ -11,34 +14,73 @@ import * as L from 'leaflet'; styleUrls: ['./data-map.component.scss'] }) export class DataMapComponent extends DataTemplateComponent implements AfterViewInit { - constructor() { - super(); - } - // If the map is shown inside the results section, different styling needs to be applied. @Input() isInsideResults : boolean = false; + + currentBaseLayer: L.TileLayer | undefined; + layers: MapLayer[] = []; + isLoading: boolean = false; + isLoadingMessage: string = 'TODO isLoadingMessage'; + canRerenderLayers: boolean = false; - // Leaflet readonly MIN_ZOOM = 0; readonly MAX_ZOOM = 19; readonly INITIAL_ZOOM = 6; private map!: L.Map; - private currentBaseLayer: L.TileLayer | undefined; - - // D3 private svg: | d3.Selection | undefined; private g: d3.Selection | undefined; - // private circles: - // | d3.Selection - // | undefined; - // private paths: - // | d3.Selection - // | undefined; + private circles: + | d3.Selection + | undefined; + private paths: + | d3.Selection + | undefined; private pathGenerator!: GeoPath; private tooltip!: d3.Selection; + constructor(protected layerSettings: LayerSettingsService) { + super(); + } + + ngOnInit() { + this.layerSettings.selectedBaseLayer$.subscribe((item) => { + if (!item) { + return; + } + + if (this.currentBaseLayer) { + this.map.removeLayer(this.currentBaseLayer); + } + + if (item != 'EMPTY') { + this.currentBaseLayer = L.tileLayer(item, { + maxZoom: this.MAX_ZOOM, + attribution: + '© OpenStreetMap contributors', + }).addTo(this.map); + } + }); + + this.layerSettings.layers$.subscribe((layers) => { + if (!layers || !layers.length) { + return; + } + + this.layers = layers; + this.renderLayersWithD3(); + }); + + this.layerSettings.canRerenderLayers$.subscribe((canRerenderLayers) => { + this.canRerenderLayers = canRerenderLayers; + }); + + this.layerSettings.toggleLayerVisibility$.subscribe((layer) => { + this.toggleLayerVisibility(layer); + }); + } + ngAfterViewInit(): void { const leafletMap = L.map('map').setView([52, 10], this.INITIAL_ZOOM); this.map = leafletMap; @@ -56,33 +98,33 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView .style('display', 'none') .style('z-index', '9999'); - // function projectPoint(this: any, x: number, y: number) { - // const point = leafletMap.latLngToLayerPoint(new L.LatLng(y, x)); - // this.stream.point(point.x, point.y); - // } - // - // const transform = d3Geo.geoTransform({ point: projectPoint }); - // this.pathGenerator = d3Geo.geoPath().projection(transform); - // - // this.map.on('zoomend', () => { - // this.updateSvgPosition(); - // }); - // this.map.on('moveend', () => { - // if (!this.svg || !this.g) { - // return; - // } - // const bounds = this.map.getBounds(); - // const topLeft = this.map.latLngToLayerPoint(bounds.getNorthWest()); - // const bottomRight = this.map.latLngToLayerPoint( - // bounds.getSouthEast(), - // ); - // this.svg - // .style('width', '999999px') - // .style('height', '999999px') - // .style('left', topLeft.x + 'px') - // .style('top', topLeft.y + 'px'); - // this.g.attr('transform', `translate(${-topLeft.x}, ${-topLeft.y})`); - // }); + function projectPoint(this: any, x: number, y: number) { + const point = leafletMap.latLngToLayerPoint(new L.LatLng(y, x)); + this.stream.point(point.x, point.y); + } + + const transform = d3Geo.geoTransform({ point: projectPoint }); + this.pathGenerator = d3Geo.geoPath().projection(transform); + + this.map.on('zoomend', () => { + this.updateSvgPosition(); + }); + this.map.on('moveend', () => { + if (!this.svg || !this.g) { + return; + } + const bounds = this.map.getBounds(); + const topLeft = this.map.latLngToLayerPoint(bounds.getNorthWest()); + const bottomRight = this.map.latLngToLayerPoint( + bounds.getSouthEast(), + ); + this.svg + .style('width', '999999px') + .style('height', '999999px') + .style('left', topLeft.x + 'px') + .style('top', topLeft.y + 'px'); + this.g.attr('transform', `translate(${-topLeft.x}, ${-topLeft.y})`); + }); this.currentBaseLayer = L.tileLayer( 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', @@ -93,5 +135,249 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView }, ).addTo(this.map); } + + showLoadingSpinner(message: string) { + this.isLoading = true; + this.isLoadingMessage = message; + } + + updateSvgPosition() { + this.showLoadingSpinner('Reposition shapes on map'); + + setTimeout(() => { + try { + if (!this.g || !this.svg) { + return; + } + + if (this.paths) { + this.paths.attr('d', (d) => this.pathGenerator(d.geometry)); + } + + if (this.circles) { + this.circles + .each((d) => { + const layerPoint = this.map.latLngToLayerPoint([ + d.getPoint()!.coordinates[1], + d.getPoint()!.coordinates[0], + ]); + d.cache['x'] = layerPoint.x; + d.cache['y'] = layerPoint.y; + }) + .attr('cx', (d) => d.cache['x']) + .attr('cy', (d) => d.cache['y']); + } + + const bounds = this.map.getBounds(); + const topLeft = this.map.latLngToLayerPoint( + bounds.getNorthWest(), + ); + const bottomRight = this.map.latLngToLayerPoint( + bounds.getSouthEast(), + ); + this.svg + .style('width', '999999px') + .style('height', '999999px') + .style('left', topLeft.x + 'px') + .style('top', topLeft.y + 'px'); + this.g.attr( + 'transform', + `translate(${-topLeft.x}, ${-topLeft.y})`, + ); + } finally { + this.isLoading = false; + } + }, 0); + } + + renderLayersWithD3() { + this.showLoadingSpinner('Rendering layers'); + + setTimeout(() => { + try { + if (!this.svg || !this.g) { + return; + } + + // Remove all previously added elements + this.g.selectAll('*').remove(); + + const points: MapGeometryWithData[] = []; + const paths: MapGeometryWithData[] = []; + + // Add shapes from each layer to array + for (const layer of this.layers.slice().reverse()) { + console.log(`Render layer [${layer.name}]. Initialize...`); + + // Initialize all configs + layer.pointShapeVisualization.init(layer.data); + layer.areaShapeVisualization.init(layer.data); + layer.colorVisualization.init(layer.data); + + points.push( + ...layer.data.filter( + (d) => d.geometry.type === 'Point', + ), + ); + paths.push( + ...layer.data.filter( + (d) => d.geometry.type !== 'Point', + ), + ); + } + + // Render all points + // TODO: Circles are always on the bottom this way... + console.log('Create Points: ', points); + this.circles = this.createPoints(points); + + console.log('Create Paths: ', paths); + this.paths = this.createPaths(paths); + + // Set SVG position correctly + this.updateSvgPosition(); + + // Center the map around the data. + // TODO: Do the same thing for paths. + const latLngs : L.LatLng[] = []; + this.circles!.each(d => { + // d has the property geometry, which is a GeoJSON point of type + latLngs.push(L.latLng(d.getPoint().coordinates[1], d.getPoint().coordinates[0],)); + }); + console.log(latLngs) + // TODO: Currently, only do this, if the dataset is not too big. Otherwise we zoom way out, + // and zooming back in can be very slow. (if the points are all over the world) + if (latLngs.length > 0 && latLngs.length <= 1000) { + const latLngBounds = L.latLngBounds(latLngs); + if (latLngBounds.isValid()){ + this.map.fitBounds(latLngBounds); + } + } + } finally { + this.isLoading = false; + } + }, 0); + } + + createPoints(points: MapGeometryWithData[]) { + if (!this.g) { + return; + } + + const tt = this.tooltip; + + return this.g + .selectAll('circle') + .data(points) + .enter() + .append('circle') + .attr('layer-name', (d) => d.layer!.name) + .attr('layer-index', (d) => d.layer!.index.toString()) + .attr('r', (d) => + d.layer!.pointShapeVisualization.getValueForAttribute('r', d), + ) + .attr('fill', (d) => + d.layer!.colorVisualization.getValueForAttribute('fill', d), + ) + .each((d) => { + const layerPoint = this.map.latLngToLayerPoint([ + d.getPoint().coordinates[1], + d.getPoint().coordinates[0], + ]); + d.cache['x'] = layerPoint.x; + d.cache['y'] = layerPoint.y; + }) + .attr('cx', (d) => d.cache['x']) + .attr('cy', (d) => d.cache['y']) + .style("pointer-events", "auto") + .style("cursor", "pointer") + .on('mouseover', function (event, d) { + tt.style('display', 'block').html(JSON.stringify(d.data, null, 1)); + }) + .on('mousemove', function (event) { + tt.style('top', event.pageY + 10 + 'px').style( + 'left', + event.pageX + 10 + 'px', + ); + }) + .on('mouseout', function () { + tt.style('display', 'none'); + }); + } + + createPaths(paths: MapGeometryWithData[]) { + if (!this.g) { + return; + } + + const tt = this.tooltip + + return this.g + .selectAll('.paths') + .data(paths) + .enter() + .append('path') + .attr('layer-name', (d) => d.layer!.name) + .attr('layer-index', (d) => d.layer!.index.toString()) + .attr('d', (d) => this.pathGenerator(d.geometry)) + .attr('stroke-width', (d) => + d.layer!.areaShapeVisualization.getValueForAttribute( + 'stroke-width', + d, + ), + ) + .attr('stroke', (d) => + d.layer!.colorVisualization.getValueForAttribute('stroke', d), + ) + .attr('fill', (d) => + d.layer!.colorVisualization.getValueForAttribute('fill', d), + ) + .attr('fill-opacity', (d) => + d.layer!.colorVisualization.getValueForAttribute( + 'fill-opacity', + d, + ), + ) + .style("pointer-events", "auto") + .style("cursor", "pointer") + .on('mouseover', function (event, d) { + console.log("hover") + tt.style('display', 'block').html(JSON.stringify(d.data, null, 2)); + }) + .on('mousemove', function (event) { + tt.style('top', event.pageY + 10 + 'px').style( + 'left', + event.pageX + 10 + 'px', + ); + }) + .on('mouseout', function () { + tt.style('display', 'none'); + }); + } + + toggleLayerVisibility(layer: MapLayer) { + if (!this.g?.node()) { + throw new Error('SVG g does not exist.'); + } + + const layerElements = this.g + .node()! + .querySelectorAll( + `[layer-name='${layer.name}'][layer-index='${layer.index.toString()}']`, + ); + + if (!layerElements.length) { + // Nothing to do + return; + } + + layerElements.forEach((elem) => { + if (layer.isActive) { + elem.classList.remove('layer-hidden'); + } else { + elem.classList.add('layer-hidden'); + } + }); + } } diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index 64d10761..8e093c06 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -21,7 +21,7 @@ import { ModalTitleDirective, PopoverDirective, } from '@coreui/angular'; -import {RowResult} from '../../models/RowResult.model'; +import {MapGeometryWithData} from '../../models/RowResult.model'; import * as GeoJSON from 'geojson'; import {FeatureCollection} from 'geojson'; import {MapLayer} from '../../models/MapLayer.model'; @@ -285,7 +285,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit { ).addData( geojson.features.map( (f, i) => - new RowResult( + new MapGeometryWithData( i, f.geometry, f.properties ? f.properties : {}, @@ -301,7 +301,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit { ).addData( this.loadedGeoJsonFile.features.map( (f, i) => - new RowResult( + new MapGeometryWithData( i, f.geometry, f.properties ? f.properties : {}, diff --git a/src/app/views/querying/gis/components/map/map.component.ts b/src/app/views/querying/gis/components/map/map.component.ts index 89af30ff..f296de6e 100644 --- a/src/app/views/querying/gis/components/map/map.component.ts +++ b/src/app/views/querying/gis/components/map/map.component.ts @@ -5,7 +5,7 @@ import * as d3Geo from 'd3-geo'; import * as L from 'leaflet'; import { LayerSettingsService } from '../../services/layersettings.service'; import { MapLayer } from '../../models/MapLayer.model'; -import { RowResult } from '../../models/RowResult.model'; +import { MapGeometryWithData } from '../../models/RowResult.model'; import { ButtonDirective, SpinnerComponent } from '@coreui/angular'; import { NgIf } from '@angular/common'; @@ -32,10 +32,10 @@ export class MapComponent implements OnInit, AfterViewInit { | undefined; private g: d3.Selection | undefined; private circles: - | d3.Selection + | d3.Selection | undefined; private paths: - | d3.Selection + | d3.Selection | undefined; private pathGenerator!: GeoPath; private tooltip!: d3.Selection; @@ -200,8 +200,8 @@ export class MapComponent implements OnInit, AfterViewInit { // Remove all previously added elements this.g.selectAll('*').remove(); - const points: RowResult[] = []; - const paths: RowResult[] = []; + const points: MapGeometryWithData[] = []; + const paths: MapGeometryWithData[] = []; // Add shapes from each layer to array for (const layer of this.layers.slice().reverse()) { @@ -257,7 +257,7 @@ export class MapComponent implements OnInit, AfterViewInit { }, 0); } - createPoints(points: RowResult[]) { + createPoints(points: MapGeometryWithData[]) { if (!this.g) { return; } @@ -303,7 +303,7 @@ export class MapComponent implements OnInit, AfterViewInit { }); } - createPaths(paths: RowResult[]) { + createPaths(paths: MapGeometryWithData[]) { if (!this.g) { return; } diff --git a/src/app/views/querying/gis/components/visualization/area-shape-visualization.model.ts b/src/app/views/querying/gis/components/visualization/area-shape-visualization.model.ts index be74e39c..59a95c2d 100644 --- a/src/app/views/querying/gis/components/visualization/area-shape-visualization.model.ts +++ b/src/app/views/querying/gis/components/visualization/area-shape-visualization.model.ts @@ -1,5 +1,5 @@ import { Visualization } from '../../models/visualization.interface'; -import { RowResult } from '../../models/RowResult.model'; +import { MapGeometryWithData } from '../../models/RowResult.model'; import { EmptyComponent } from './empty/empty.component'; import {AreaShapeComponent} from "./area-shape/area-shape.component"; @@ -16,7 +16,7 @@ export class AreaShapeVisualization implements Visualization { this.outlineThickness = outlineThickness; } - init(data: RowResult[]): void { + init(data: MapGeometryWithData[]): void { // } @@ -26,7 +26,7 @@ export class AreaShapeVisualization implements Visualization { return copy; } - getValueForAttribute(attr: string, data: RowResult): string | number { + getValueForAttribute(attr: string, data: MapGeometryWithData): string | number { switch (attr) { case 'stroke-width': return this.outlineThickness; diff --git a/src/app/views/querying/gis/components/visualization/color-visualization-model.ts b/src/app/views/querying/gis/components/visualization/color-visualization-model.ts index 2e320cd4..c9947a0d 100644 --- a/src/app/views/querying/gis/components/visualization/color-visualization-model.ts +++ b/src/app/views/querying/gis/components/visualization/color-visualization-model.ts @@ -1,5 +1,5 @@ import { Visualization } from '../../models/visualization.interface'; -import { RowResult } from '../../models/RowResult.model'; +import { MapGeometryWithData } from '../../models/RowResult.model'; import * as d3 from 'd3'; import * as turf from '@turf/turf'; import { ColorComponent } from './color/color.component'; @@ -24,7 +24,7 @@ export class ColorVisualization implements Visualization { this.color = color; } - init(data: RowResult[]): void { + init(data: MapGeometryWithData[]): void { if (this.selectedMode === this.modes[0]) { // Nothing to do return; @@ -67,7 +67,7 @@ export class ColorVisualization implements Visualization { return copy; } - getValueForAttribute(attr: string, data: RowResult): string | number { + getValueForAttribute(attr: string, data: MapGeometryWithData): string | number { switch (attr) { case 'stroke': switch (this.selectedMode) { diff --git a/src/app/views/querying/gis/components/visualization/empty-visualization.model.ts b/src/app/views/querying/gis/components/visualization/empty-visualization.model.ts index 7a60595d..fa88571e 100644 --- a/src/app/views/querying/gis/components/visualization/empty-visualization.model.ts +++ b/src/app/views/querying/gis/components/visualization/empty-visualization.model.ts @@ -1,5 +1,5 @@ import { Type } from '@angular/core'; -import { RowResult } from '../../models/RowResult.model'; +import { MapGeometryWithData } from '../../models/RowResult.model'; import { VisualizationConfiguration } from '../../models/visualization-configuration.interface'; import { Visualization } from '../../models/visualization.interface'; import {EmptyComponent} from "./empty/empty.component"; @@ -14,10 +14,10 @@ export class EmptyVisualization implements Visualization { copy(): Visualization { throw new Error('Method not implemented.'); } - init(data: RowResult[]): void { + init(data: MapGeometryWithData[]): void { throw new Error('Method not implemented.'); } - getValueForAttribute(attr: string, data: RowResult): string | number { + getValueForAttribute(attr: string, data: MapGeometryWithData): string | number { throw new Error('Method not implemented.'); } } diff --git a/src/app/views/querying/gis/components/visualization/label-visualization-model.ts b/src/app/views/querying/gis/components/visualization/label-visualization-model.ts index 5ac4f2b5..8ca0e344 100644 --- a/src/app/views/querying/gis/components/visualization/label-visualization-model.ts +++ b/src/app/views/querying/gis/components/visualization/label-visualization-model.ts @@ -1,5 +1,5 @@ import { Visualization } from '../../models/visualization.interface'; -import { RowResult } from '../../models/RowResult.model'; +import { MapGeometryWithData } from '../../models/RowResult.model'; import {LabelComponent} from "./label/label.component"; export class LabelVisualization implements Visualization { @@ -10,7 +10,7 @@ export class LabelVisualization implements Visualization { constructor() {} - init(data: RowResult[]): void { + init(data: MapGeometryWithData[]): void { // } @@ -20,7 +20,7 @@ export class LabelVisualization implements Visualization { return copy; } - getValueForAttribute(attr: string, data: RowResult): string | number { + getValueForAttribute(attr: string, data: MapGeometryWithData): string | number { return 'TODO'; // throw new Error(`Visualization does not support attribute [${attr}]`); } diff --git a/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts b/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts index ff937041..4db22bce 100644 --- a/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts +++ b/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts @@ -1,5 +1,5 @@ import { Visualization } from '../../models/visualization.interface'; -import { RowResult } from '../../models/RowResult.model'; +import { MapGeometryWithData } from '../../models/RowResult.model'; import { PointShapeComponent } from './point-shape/point-shape.component'; export class PointShapeVisualization implements Visualization { @@ -15,7 +15,7 @@ export class PointShapeVisualization implements Visualization { this.size = size; } - init(data: RowResult[]): void { + init(data: MapGeometryWithData[]): void { // } @@ -26,7 +26,7 @@ export class PointShapeVisualization implements Visualization { return copy; } - getValueForAttribute(attr: string, data: RowResult): string | number { + getValueForAttribute(attr: string, data: MapGeometryWithData): string | number { if (data.isPoint()) { switch (attr) { case 'r': diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index 24de17a8..a1b32de1 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -1,5 +1,5 @@ import { Visualization } from './visualization.interface'; -import { RowResult } from './RowResult.model'; +import { MapGeometryWithData } from './RowResult.model'; import { ColorVisualization } from '../components/visualization/color-visualization-model'; import {AreaShapeVisualization} from "../components/visualization/area-shape-visualization.model"; import {LabelVisualization} from "../components/visualization/label-visualization-model"; @@ -7,7 +7,7 @@ import {PointShapeVisualization} from "../components/visualization/point-shape-v export class MapLayer { name: string; - data: RowResult[] = []; + data: MapGeometryWithData[] = []; pointShapeVisualization: Visualization = new PointShapeVisualization(3); areaShapeVisualization: Visualization = new AreaShapeVisualization(1); @@ -37,7 +37,7 @@ export class MapLayer { return copy; } - addData(data: RowResult[]) { + addData(data: MapGeometryWithData[]) { data.forEach((d) => (d.layer = this)); this.data.push(...data); return this; diff --git a/src/app/views/querying/gis/models/RowResult.model.ts b/src/app/views/querying/gis/models/RowResult.model.ts index 0f3c7e40..6654906d 100644 --- a/src/app/views/querying/gis/models/RowResult.model.ts +++ b/src/app/views/querying/gis/models/RowResult.model.ts @@ -4,7 +4,7 @@ import {MapLayer} from "./MapLayer.model"; /** * Represents one row in the results returned by Polypheny. */ -export class RowResult { +export class MapGeometryWithData { /** * Used for styling if the results are ordered */ @@ -40,7 +40,7 @@ export class RowResult { copy(){ // We leave out cache on purpose, because we will use the copy to compare both if the layer has changed. - return new RowResult(this.index, this.geometry, this.data); + return new MapGeometryWithData(this.index, this.geometry, this.data); } getNumberValueFromField(fieldName: string): number { diff --git a/src/app/views/querying/gis/models/get-sample-maplayers.ts b/src/app/views/querying/gis/models/get-sample-maplayers.ts index 6d1c0555..e7694a9b 100644 --- a/src/app/views/querying/gis/models/get-sample-maplayers.ts +++ b/src/app/views/querying/gis/models/get-sample-maplayers.ts @@ -1,6 +1,6 @@ import { MapLayer } from './MapLayer.model'; import * as GeoJSON from 'geojson'; -import { RowResult } from './RowResult.model'; +import { MapGeometryWithData } from './RowResult.model'; export function getSampleMapLayers(): MapLayer[] { const data = ` @@ -293,13 +293,13 @@ export function getSampleMapLayers(): MapLayer[] { return [ new MapLayer('a').addData( - geoJson.features.map((f, i) => new RowResult(i, f.geometry)), + geoJson.features.map((f, i) => new MapGeometryWithData(i, f.geometry)), ), new MapLayer('b').addData( - geoJson2.features.map((f, i) => new RowResult(i, f.geometry)), + geoJson2.features.map((f, i) => new MapGeometryWithData(i, f.geometry)), ), new MapLayer('Landkreise').addData( - geoJson3.features.map((f, i) => new RowResult(i, f.geometry)), + geoJson3.features.map((f, i) => new MapGeometryWithData(i, f.geometry)), ), ]; } diff --git a/src/app/views/querying/gis/models/visualization.interface.ts b/src/app/views/querying/gis/models/visualization.interface.ts index 0aa602ab..8378cd7a 100644 --- a/src/app/views/querying/gis/models/visualization.interface.ts +++ b/src/app/views/querying/gis/models/visualization.interface.ts @@ -1,6 +1,6 @@ import { VisualizationConfiguration } from './visualization-configuration.interface'; import { Type } from '@angular/core'; -import {RowResult} from "./RowResult.model"; +import {MapGeometryWithData} from "./RowResult.model"; export interface Visualization { name: string; @@ -10,7 +10,7 @@ export interface Visualization { copy(): Visualization; - init(data: RowResult[]): void; + init(data: MapGeometryWithData[]): void; - getValueForAttribute(attr: string, data: RowResult): string | number; + getValueForAttribute(attr: string, data: MapGeometryWithData): string | number; } From a2ff5efdb8e0dffc0839094f063e58de5ffb361e Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Sat, 30 Nov 2024 17:05:06 +0100 Subject: [PATCH 06/60] Change padding to margin to fix layout problem --- src/app/components/data-view/data-map/data-map.component.ts | 2 +- .../querying/gis/components/layers/map-layers.component.scss | 2 +- .../querying/gis/components/layers/map-layers.component.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index 176b2088..8cd63272 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -16,7 +16,7 @@ import {MapGeometryWithData} from "../../../views/querying/gis/models/RowResult. export class DataMapComponent extends DataTemplateComponent implements AfterViewInit { // If the map is shown inside the results section, different styling needs to be applied. @Input() isInsideResults : boolean = false; - + currentBaseLayer: L.TileLayer | undefined; layers: MapLayer[] = []; isLoading: boolean = false; diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.scss b/src/app/views/querying/gis/components/layers/map-layers.component.scss index 4fc9fb90..b9701cf5 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.scss +++ b/src/app/views/querying/gis/components/layers/map-layers.component.scss @@ -3,7 +3,7 @@ height: 100%; display: flex; flex-direction: column; - padding: 1rem; + margin: 1rem; grid-gap: 1rem; max-height: 100%; diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index 8e093c06..3f4081a9 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -170,7 +170,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit { this.updateLayers(this.layers); }); - this.updateLayers(getSampleMapLayers()); + // this.updateLayers(getSampleMapLayers()); } @HostListener('window:resize') From 5351461c14948308def13b0e4a02405fe1473f78 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Mon, 2 Dec 2024 14:37:59 +0100 Subject: [PATCH 07/60] Add websocket + ability to run query to layer settings + heuristic for detecting geometry in results --- package.json | 1 + .../data-view/data-map/data-map.component.ts | 25 ++++-- .../data-view/data-view.component.ts | 6 +- .../layers/map-layers.component.html | 21 +---- .../components/layers/map-layers.component.ts | 86 ++++++++++++++++--- .../querying/gis/models/MapLayer.model.ts | 72 +++++++++++++++- 6 files changed, 174 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 7cb07780..20fe2357 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "ng": "ng", "start": "ng serve -o", + "start2": "ng serve --no-live-reload -o", "build": "ng build --configuration production --aot --optimization --progress --extract-licenses", "test": "ng test", "lint": "ng lint", diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index 8cd63272..64969a79 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -1,12 +1,13 @@ import {AfterViewInit, Component, effect, Input} from '@angular/core'; import {DataTemplateComponent} from '../data-template/data-template.component'; import * as d3 from 'd3'; -import { GeoPath, GeoPermissibleObjects } from 'd3'; +import {GeoPath, GeoPermissibleObjects} from 'd3'; import * as d3Geo from 'd3-geo'; import * as L from 'leaflet'; import {LayerSettingsService} from "../../../views/querying/gis/services/layersettings.service"; import {MapLayer} from "../../../views/querying/gis/models/MapLayer.model"; import {MapGeometryWithData} from "../../../views/querying/gis/models/RowResult.model"; +import {CombinedResult} from "../data-view.model"; @Component({ selector: 'app-data-map', @@ -15,7 +16,7 @@ import {MapGeometryWithData} from "../../../views/querying/gis/models/RowResult. }) export class DataMapComponent extends DataTemplateComponent implements AfterViewInit { // If the map is shown inside the results section, different styling needs to be applied. - @Input() isInsideResults : boolean = false; + @Input() isInsideResults: boolean = false; currentBaseLayer: L.TileLayer | undefined; layers: MapLayer[] = []; @@ -42,6 +43,20 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView constructor(protected layerSettings: LayerSettingsService) { super(); + + effect(() => { + // This code is only called when the map is shown inside the results. + const result = this.$result(); + if (!result) { + return; + } + this.createLayerFromResult(result) + }); + } + + createLayerFromResult(result: CombinedResult) { + this.layers = [ MapLayer.from(result) ] + this.renderLayersWithD3() } ngOnInit() { @@ -103,7 +118,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView this.stream.point(point.x, point.y); } - const transform = d3Geo.geoTransform({ point: projectPoint }); + const transform = d3Geo.geoTransform({point: projectPoint}); this.pathGenerator = d3Geo.geoPath().projection(transform); this.map.on('zoomend', () => { @@ -239,7 +254,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView // Center the map around the data. // TODO: Do the same thing for paths. - const latLngs : L.LatLng[] = []; + const latLngs: L.LatLng[] = []; this.circles!.each(d => { // d has the property geometry, which is a GeoJSON point of type latLngs.push(L.latLng(d.getPoint().coordinates[1], d.getPoint().coordinates[0],)); @@ -249,7 +264,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView // and zooming back in can be very slow. (if the points are all over the world) if (latLngs.length > 0 && latLngs.length <= 1000) { const latLngBounds = L.latLngBounds(latLngs); - if (latLngBounds.isValid()){ + if (latLngBounds.isValid()) { this.map.fitBounds(latLngBounds); } } diff --git a/src/app/components/data-view/data-view.component.ts b/src/app/components/data-view/data-view.component.ts index 08dde93c..6fb28add 100644 --- a/src/app/components/data-view/data-view.component.ts +++ b/src/app/components/data-view/data-view.component.ts @@ -176,7 +176,11 @@ export class DataViewComponent implements OnDestroy { } showAny(): boolean { - return !(this.$dataModel() === DataModel.RELATIONAL || this.$dataModel() === DataModel.DOCUMENT); + // TODO: Control should be more fine-grained. + // - Relational: Table, map + // - Document: Cards, map + // - Graph: Table / Cards, Graph, map + return true; } diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.html b/src/app/views/querying/gis/components/layers/map-layers.component.html index 829db7cf..66d3aed8 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.html +++ b/src/app/views/querying/gis/components/layers/map-layers.component.html @@ -131,24 +131,11 @@
    Add layer
    -
    - TODO -
    -
    - TODO -
    - -
    - Select which dataset will be loaded -
    - -
    + + Query + +
    diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index 3f4081a9..7121d3df 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -1,4 +1,13 @@ -import {AfterViewInit, Component, ElementRef, HostListener, OnInit, Renderer2} from '@angular/core'; +import { + AfterViewInit, + Component, effect, + ElementRef, + HostListener, + inject, + OnInit, + Renderer2, signal, untracked, + WritableSignal +} from '@angular/core'; import {LayerContext} from '../../models/LayerContext.model'; import {LayerSettingsService} from '../../services/layersettings.service'; import { @@ -40,6 +49,14 @@ import {NgxJsonViewerModule} from 'ngx-json-viewer'; import {ConfigSectionComponent} from '../config-section/config-section.component'; // noinspection ES6UnusedImports import {getSampleMapLayers} from '../../models/get-sample-maplayers'; +import {CrudService} from "../../../../../services/crud.service"; +import {WebSocket} from "../../../../../services/webSocket"; +import {SidebarNode} from "../../../../../models/sidebar-node.model"; +import {RelationalResult, Result} from "../../../../../components/data-view/models/result-set.model"; +import {InformationObject} from "../../../../../models/information-page.model"; +import {WebuiSettingsService} from "../../../../../services/webui-settings.service"; +import {Subscription} from "rxjs"; +import {QueryRequest} from "../../../../../models/ui-request.model"; type BaseLayer = { name: string; value: string }; @@ -49,8 +66,6 @@ type BaseLayer = { name: string; value: string }; imports: [ FormLabelDirective, FormControlDirective, - AsyncPipe, - NgComponentOutlet, NgIf, ModalComponent, ModalHeaderComponent, @@ -61,7 +76,6 @@ type BaseLayer = { name: string; value: string }; ButtonDirective, CdkDropList, CdkDrag, - CdkDragPlaceholder, CdkDragHandle, InputGroupComponent, InputGroupTextDirective, @@ -74,14 +88,23 @@ type BaseLayer = { name: string; value: string }; PopoverDirective, NgxJsonViewerModule, ConfigSectionComponent, - ListGroupDirective, - ListGroupItemDirective, + + ], templateUrl: './map-layers.component.html', styleUrl: './map-layers.component.scss', // changeDetection: ChangeDetectionStrategy.OnPush, }) export class MapLayersComponent implements OnInit, AfterViewInit { + // DI + private readonly _crud = inject(CrudService); + private readonly _settings = inject(WebuiSettingsService); + + // Querying + websocket: WebSocket; + results: WritableSignal[]> = signal([]); + private subscriptions = new Subscription(); + protected baseLayers: BaseLayer[] = [ { name: 'OSM', @@ -99,17 +122,22 @@ export class MapLayersComponent implements OnInit, AfterViewInit { protected selectedBaseLayer: BaseLayer = this.baseLayers[0]; protected layers: MapLayer[] = []; protected renderedLayers: MapLayer[] = []; + + // Add Layer + protected query = "" protected isAddLayerModalVisible = false; protected loadedGeoJsonFile?: GeoJSON.FeatureCollection = undefined; protected loadedGeoJsonFileName: string = ''; protected anyLayersVisible = false; protected addLayerModes: LayerContext[] = [ - LayerContext.Results, + // LayerContext.Results, LayerContext.Query, - LayerContext.DB, + // LayerContext.DB, LayerContext.External, ]; - protected addLayerMode: LayerContext = LayerContext.DB; + protected addLayerMode: LayerContext = LayerContext.Query; + + protected datasetToUrl = new Map([ ['Genealogy (100, data)', 'gedcom_coordinates_100_data.geojson'], ['Genealogy (Full, no data)', 'gedcom_coordinates_full.geojson'], @@ -133,6 +161,34 @@ export class MapLayersComponent implements OnInit, AfterViewInit { private renderer: Renderer2 // private cdRef: ChangeDetectorRef, ) { + this.websocket = new WebSocket(); + this.initWebsocket(); + + effect(() => { + const res = this.results(); + + untracked(() => { + console.log(res) + }) + }); + } + + private initWebsocket() { + const sub = this.websocket.onMessage().subscribe({ + next: msg => { + if (Array.isArray(msg) && ((msg[0].hasOwnProperty('data') || msg[0].hasOwnProperty('affectedTuples') || msg[0].hasOwnProperty('error')))) { // array of ResultSet + this.results.set([]>msg); + + } + }, + error: err => { + //this._leftSidebar.setError('Lost connection with the server.'); + setTimeout(() => { + this.initWebsocket(); + }, +this._settings.getSetting('reconnection.timeout')); + } + }); + this.subscriptions.add(sub); } ngAfterViewInit(): void { @@ -184,10 +240,10 @@ export class MapLayersComponent implements OnInit, AfterViewInit { const endTime = Date.now() + this.pollingDuration; this.pollingTimer = setInterval(() => { const currentHeight = this.getMapHeight(); - if (currentHeight != this.lastHeight){ + if (currentHeight != this.lastHeight) { this.setMapHeight(currentHeight); } else { - if (Date.now() > endTime){ + if (Date.now() > endTime) { clearInterval(this.pollingTimer); } } @@ -273,6 +329,12 @@ export class MapLayersComponent implements OnInit, AfterViewInit { async addLayer() { switch (this.addLayerMode) { case LayerContext.Query: + // TODO: Run query parse results + // TODO: queryLanguage, namespace + if (!this._crud.anyQuery(this.websocket, new QueryRequest(this.query, false, false, "MQL", "test"))) { + this.results.set([new RelationalResult('Could not establish a connection with the server.')]); + } + case LayerContext.Results: case LayerContext.DB: if (!this.selectedPolyphenyDataset) { @@ -355,4 +417,6 @@ export class MapLayersComponent implements OnInit, AfterViewInit { protected readonly Object = Object; protected readonly LayerContext = LayerContext; + + } diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index a1b32de1..b7df2cd3 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -1,9 +1,12 @@ -import { Visualization } from './visualization.interface'; -import { MapGeometryWithData } from './RowResult.model'; -import { ColorVisualization } from '../components/visualization/color-visualization-model'; +import {Visualization} from './visualization.interface'; +import {MapGeometryWithData} from './RowResult.model'; +import {ColorVisualization} from '../components/visualization/color-visualization-model'; import {AreaShapeVisualization} from "../components/visualization/area-shape-visualization.model"; import {LabelVisualization} from "../components/visualization/label-visualization-model"; import {PointShapeVisualization} from "../components/visualization/point-shape-visualization.model"; +import {CombinedResult} from "../../../../components/data-view/data-view.model"; +import {DataModel} from "../../../../models/ui-request.model"; +import {GeoJSON, Geometry} from "geojson"; export class MapLayer { name: string; @@ -42,4 +45,67 @@ export class MapLayer { this.data.push(...data); return this; } + + static from(result: CombinedResult): MapLayer { + console.log(result) + const layer = new MapLayer("Query") + const mapData = [] + + switch (result.dataModel) { + case DataModel.DOCUMENT: + for (let i = 0; i < result.data.length; i++) { + const json = result.data[i][0]; + const jsonObject: Record = JSON.parse(json) + const geometry = this.getGeometryFromData(jsonObject) + if (geometry) { + const geometryWithData = new MapGeometryWithData(i, geometry, jsonObject) + mapData.push(geometryWithData) + } + } + break; + case DataModel.RELATIONAL: + break; + case DataModel.GRAPH: + break; + default: + throw Error(`Cannot convert CombinedResult to MapLayer. Unknown document model: ${result.dataModel}`); + } + layer.addData(mapData); + return layer + } + + static getGeometryFromData(data: Record): Geometry | undefined { + // GeoJSON object + if ("geometry" in data) { + return data["geometry"] + } + + // Detect 2 columns that store lattidue / longitude coordinates + const latLong = [ + ["lat", "lon"], + ["latitude", "longitude"], + ["lati", 'long'], + ] + const isNumber = (value: any): boolean => { + return typeof value === 'number' && !isNaN(value); + }; + + for (const ll of latLong) { + const lat = ll[0] + const lon = ll[1] + + if (lat in data && lon in data && isNumber(data[lat]) && isNumber(data[lon])) { + return { + type: "Point", + coordinates: [data[lat], data[lon]] + } + } + } + + // TODO: Detect heuristic, so that we can automatically detect the most common geometry types + // - string in wkt format + // - If we are inside SQL, we should be able to use the schema, to detect Geometry objects? + + return undefined + } } From f8bf8aadc268d92d17ff81ffffe9e809daf7a620 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Mon, 2 Dec 2024 21:28:50 +0100 Subject: [PATCH 08/60] Add parsing for relational model for CombinedResult --- .../querying/gis/models/MapLayer.model.ts | 140 +++++++++++------- 1 file changed, 84 insertions(+), 56 deletions(-) diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index b7df2cd3..a6577b5a 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -1,14 +1,19 @@ import {Visualization} from './visualization.interface'; import {MapGeometryWithData} from './RowResult.model'; import {ColorVisualization} from '../components/visualization/color-visualization-model'; -import {AreaShapeVisualization} from "../components/visualization/area-shape-visualization.model"; -import {LabelVisualization} from "../components/visualization/label-visualization-model"; -import {PointShapeVisualization} from "../components/visualization/point-shape-visualization.model"; -import {CombinedResult} from "../../../../components/data-view/data-view.model"; -import {DataModel} from "../../../../models/ui-request.model"; -import {GeoJSON, Geometry} from "geojson"; +import {AreaShapeVisualization} from '../components/visualization/area-shape-visualization.model'; +import {LabelVisualization} from '../components/visualization/label-visualization-model'; +import {PointShapeVisualization} from '../components/visualization/point-shape-visualization.model'; +import {CombinedResult} from '../../../../components/data-view/data-view.model'; +import {DataModel} from '../../../../models/ui-request.model'; +import {Geometry} from 'geojson'; export class MapLayer { + + constructor(name: string) { + this.name = name; + } + name: string; data: MapGeometryWithData[] = []; @@ -18,52 +23,52 @@ export class MapLayer { labelVisualization: Visualization = new LabelVisualization(); // Computed (Not used in copy) - isActive: boolean = true; - isRemoved: boolean = false; - index: number = -1; - - constructor(name: string) { - this.name = name; - } - - copy() { - // Do not copy isActive and isRemoved, because we use the copy to check if - // anything changes so we need to rerender, but in these cases we do not need - // to rerender. - const copy = new MapLayer(this.name).addData( - this.data.map((d) => d.copy()), - ); - copy.pointShapeVisualization = this.pointShapeVisualization.copy(); - copy.areaShapeVisualization = this.areaShapeVisualization.copy(); - copy.colorVisualization = this.colorVisualization.copy(); - copy.labelVisualization = this.labelVisualization.copy(); - return copy; - } - - addData(data: MapGeometryWithData[]) { - data.forEach((d) => (d.layer = this)); - this.data.push(...data); - return this; - } + isActive = true; + isRemoved = false; + index = -1; static from(result: CombinedResult): MapLayer { - console.log(result) - const layer = new MapLayer("Query") - const mapData = [] + console.log(result); + const layer = new MapLayer('Query'); + const mapData = []; switch (result.dataModel) { case DataModel.DOCUMENT: for (let i = 0; i < result.data.length; i++) { const json = result.data[i][0]; - const jsonObject: Record = JSON.parse(json) - const geometry = this.getGeometryFromData(jsonObject) + const jsonObject: Record = Object.fromEntries( + Object.entries(JSON.parse(json)).map(([key, value]) => [key.toLowerCase(), value]) + ); + const geometry = this.getGeometryFromData(jsonObject); if (geometry) { - const geometryWithData = new MapGeometryWithData(i, geometry, jsonObject) - mapData.push(geometryWithData) + const geometryWithData = new MapGeometryWithData(i, geometry, jsonObject); + mapData.push(geometryWithData); } } break; case DataModel.RELATIONAL: + for (let rowIndex = 0; rowIndex < result.data.length; rowIndex++) { + const map = new Map(); + for (let i = 0; i < result.header.length; i++) { + // TODO: Geometry objects + const header = result.header[i]; + const key = header.name.toLowerCase(); + const value = result.data[rowIndex][i]; + if (header.dataType.startsWith('INTEGER')) { + map.set(key, parseInt(value, 10)); + } else if (header.dataType.startsWith('DECIMAL')) { + map.set(key, parseFloat(value)); + } else { + map.set(key, value); + } + } + const geometry = this.getGeometryFromData(map); + console.log(geometry); + if (geometry) { + const geometryWithData = new MapGeometryWithData(rowIndex, geometry, map); + mapData.push(geometryWithData); + } + } break; case DataModel.GRAPH: break; @@ -71,41 +76,64 @@ export class MapLayer { throw Error(`Cannot convert CombinedResult to MapLayer. Unknown document model: ${result.dataModel}`); } layer.addData(mapData); - return layer + console.log('Created layer: ', layer); + return layer; } static getGeometryFromData(data: Record): Geometry | undefined { // GeoJSON object - if ("geometry" in data) { - return data["geometry"] + if ('geometry' in data) { + return data['geometry']; } - // Detect 2 columns that store lattidue / longitude coordinates + // Detect 2 columns that store latitude / longitude coordinates const latLong = [ - ["lat", "lon"], - ["latitude", "longitude"], - ["lati", 'long'], - ] + ['lat', 'lon'], + ['latitude', 'longitude'], + ['lati', 'long'], + ]; const isNumber = (value: any): boolean => { return typeof value === 'number' && !isNaN(value); }; for (const ll of latLong) { - const lat = ll[0] - const lon = ll[1] + const lat = ll[0]; + const lon = ll[1]; - if (lat in data && lon in data && isNumber(data[lat]) && isNumber(data[lon])) { + if (data.has(lat) && + data.has(lon) && + isNumber(data.get(lat)) && + isNumber(data.get(lon))) { return { - type: "Point", - coordinates: [data[lat], data[lon]] - } + type: 'Point', + coordinates: [data.get(lon), data.get(lat)] + }; } } // TODO: Detect heuristic, so that we can automatically detect the most common geometry types // - string in wkt format - // - If we are inside SQL, we should be able to use the schema, to detect Geometry objects? - return undefined + return undefined; + } + + copy() { + // Do not copy isActive and isRemoved, because we use the copy to check if + // anything changes so we need to rerender, but in these cases we do not need + // to rerender. + const copy = new MapLayer(this.name).addData( + this.data.map((d) => d.copy()), + ); + copy.pointShapeVisualization = this.pointShapeVisualization.copy(); + copy.areaShapeVisualization = this.areaShapeVisualization.copy(); + copy.colorVisualization = this.colorVisualization.copy(); + copy.labelVisualization = this.labelVisualization.copy(); + return copy; + } + + addData(data: MapGeometryWithData[]) { + data.forEach((d) => (d.layer = this)); + this.data.push(...data); + return this; } } From ba35c95c68eb6e090760a92472028299690685ba Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Mon, 2 Dec 2024 21:59:35 +0100 Subject: [PATCH 09/60] layersettings.service.ts TsLint --- .../gis/services/layersettings.service.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/app/views/querying/gis/services/layersettings.service.ts b/src/app/views/querying/gis/services/layersettings.service.ts index 190b9976..5ec9328b 100644 --- a/src/app/views/querying/gis/services/layersettings.service.ts +++ b/src/app/views/querying/gis/services/layersettings.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import {BehaviorSubject, Subject} from 'rxjs'; import { MapLayer } from '../models/MapLayer.model'; -import {Visualization} from "../models/visualization.interface"; +import {Visualization} from '../models/visualization.interface'; @Injectable({ providedIn: 'root', @@ -9,36 +9,36 @@ import {Visualization} from "../models/visualization.interface"; export class LayerSettingsService { private selectedBaseLayer = new BehaviorSubject('EMPTY'); selectedBaseLayer$ = this.selectedBaseLayer.asObservable(); - setBaseLayer(item: string) { - this.selectedBaseLayer.next(item); - } private layers = new BehaviorSubject([]); layers$ = this.layers.asObservable(); - setLayers(layers: MapLayer[]) { - this.layers.next(layers); - } private modifiedVisualization = new BehaviorSubject(null); modifiedVisualization$ = this.modifiedVisualization.asObservable(); - visualizationConfigurationChanged(visualization: Visualization): void { - this.modifiedVisualization.next(visualization); - } private canRerenderLayers = new BehaviorSubject(false); canRerenderLayers$ = this.canRerenderLayers.asObservable(); - setCanRerenderLayers(canRerenderMap: boolean){ - this.canRerenderLayers.next(canRerenderMap); - } private rerenderButtonClickedSubject = new Subject(); rerenderButtonClicked$ = this.rerenderButtonClickedSubject.asObservable(); - rerenderButtonClicked(){ - this.rerenderButtonClickedSubject.next(); - } private toggleLayerVisibilitySubject = new Subject(); toggleLayerVisibility$ = this.toggleLayerVisibilitySubject.asObservable(); + setBaseLayer(item: string) { + this.selectedBaseLayer.next(item); + } + setLayers(layers: MapLayer[]) { + this.layers.next(layers); + } + visualizationConfigurationChanged(visualization: Visualization): void { + this.modifiedVisualization.next(visualization); + } + setCanRerenderLayers(canRerenderMap: boolean){ + this.canRerenderLayers.next(canRerenderMap); + } + rerenderButtonClicked(){ + this.rerenderButtonClickedSubject.next(); + } toggleLayerVisibility(layer: MapLayer){ this.toggleLayerVisibilitySubject.next(layer); } From 7052834480c8755a274ad70c14596b2d54d4761d Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Mon, 2 Dec 2024 22:34:19 +0100 Subject: [PATCH 10/60] Add parsing for graph model for CombinedResult --- .../querying/gis/models/MapLayer.model.ts | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index a6577b5a..da6f0387 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -34,14 +34,14 @@ export class MapLayer { switch (result.dataModel) { case DataModel.DOCUMENT: - for (let i = 0; i < result.data.length; i++) { - const json = result.data[i][0]; + for (let rowIndex = 0; rowIndex < result.data.length; rowIndex++) { + const json = result.data[rowIndex][0]; const jsonObject: Record = Object.fromEntries( Object.entries(JSON.parse(json)).map(([key, value]) => [key.toLowerCase(), value]) ); const geometry = this.getGeometryFromData(jsonObject); if (geometry) { - const geometryWithData = new MapGeometryWithData(i, geometry, jsonObject); + const geometryWithData = new MapGeometryWithData(rowIndex, geometry, jsonObject); mapData.push(geometryWithData); } } @@ -49,11 +49,11 @@ export class MapLayer { case DataModel.RELATIONAL: for (let rowIndex = 0; rowIndex < result.data.length; rowIndex++) { const map = new Map(); - for (let i = 0; i < result.header.length; i++) { + for (let headerIndex = 0; headerIndex < result.header.length; headerIndex++) { // TODO: Geometry objects - const header = result.header[i]; + const header = result.header[headerIndex]; const key = header.name.toLowerCase(); - const value = result.data[rowIndex][i]; + const value = result.data[rowIndex][headerIndex]; if (header.dataType.startsWith('INTEGER')) { map.set(key, parseInt(value, 10)); } else if (header.dataType.startsWith('DECIMAL')) { @@ -71,6 +71,34 @@ export class MapLayer { } break; case DataModel.GRAPH: + for (let rowIndex = 0; rowIndex < result.data.length; rowIndex++) { + const map = new Map(); + for (let headerIndex = 0; headerIndex < result.header.length; headerIndex++) { + const header = result.header[headerIndex]; + const key = header.name.toLowerCase(); + const datatype = header.dataType; + const value = result.data[rowIndex][headerIndex]; + + if (datatype.startsWith('NODE')) { + const json = JSON.parse(value); + const properties = json['properties']; + const propertiesLowercase: Record = Object.fromEntries( + Object.entries(properties).map(([key, value]) => [key.toLowerCase(), value]) + ); + // Node stored as JSON + map.set(key, propertiesLowercase); + } else { + // Other value + map.set(key, value); + } + } + + const geometry = this.getGeometryFromData(map); + if (geometry) { + const geometryWithData = new MapGeometryWithData(rowIndex, geometry, map); + mapData.push(geometryWithData); + } + } break; default: throw Error(`Cannot convert CombinedResult to MapLayer. Unknown document model: ${result.dataModel}`); From 54c78e7cd271de374ec5126ff415d9e90c7e1892 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Tue, 3 Dec 2024 17:58:27 +0100 Subject: [PATCH 11/60] Add navigation from results to map query mode + fix show/hide layer --- .../data-map/data-map.component.html | 19 +- .../data-map/data-map.component.scss | 5 +- .../data-view/data-map/data-map.component.ts | 19 +- .../layers/map-layers.component.html | 60 +-- .../components/layers/map-layers.component.ts | 22 +- .../gis/components/map/map.component.css | 48 --- .../gis/components/map/map.component.html | 14 - .../gis/components/map/map.component.ts | 380 ------------------ .../querying/gis/models/MapLayer.model.ts | 1 + src/scss/_custom.scss | 6 +- 10 files changed, 78 insertions(+), 496 deletions(-) delete mode 100644 src/app/views/querying/gis/components/map/map.component.css delete mode 100644 src/app/views/querying/gis/components/map/map.component.html delete mode 100644 src/app/views/querying/gis/components/map/map.component.ts diff --git a/src/app/components/data-view/data-map/data-map.component.html b/src/app/components/data-view/data-map/data-map.component.html index 21287257..5326c1e5 100644 --- a/src/app/components/data-view/data-map/data-map.component.html +++ b/src/app/components/data-view/data-map/data-map.component.html @@ -1,19 +1,24 @@ -
    -
    +
    +
    - + {{ this.isLoadingMessage }} Loading... - + {{ this.isLoadingMessage }} Loading...
    - - - +
    + + +
    diff --git a/src/app/components/data-view/data-map/data-map.component.scss b/src/app/components/data-view/data-map/data-map.component.scss index 100eb917..7b1dd75c 100644 --- a/src/app/components/data-view/data-map/data-map.component.scss +++ b/src/app/components/data-view/data-map/data-map.component.scss @@ -40,7 +40,10 @@ padding: 2rem; } -.render-button { +.map-overlay-buttons { + display: flex; + flex-direction: row; + gap: 1rem; position: absolute; z-index: 3; bottom: 2rem; diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index 64969a79..8410d076 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -55,11 +55,14 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView } createLayerFromResult(result: CombinedResult) { - this.layers = [ MapLayer.from(result) ] + this.layers = [MapLayer.from(result)] this.renderLayersWithD3() } ngOnInit() { + // Reset data on map + console.log("data-map.component.ts ngOnInit(). Layers=", this.layers) + this.layerSettings.selectedBaseLayer$.subscribe((item) => { if (!item) { return; @@ -259,7 +262,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView // d has the property geometry, which is a GeoJSON point of type latLngs.push(L.latLng(d.getPoint().coordinates[1], d.getPoint().coordinates[0],)); }); - console.log(latLngs) + // TODO: Currently, only do this, if the dataset is not too big. Otherwise we zoom way out, // and zooming back in can be very slow. (if the points are all over the world) if (latLngs.length > 0 && latLngs.length <= 1000) { @@ -388,11 +391,19 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView layerElements.forEach((elem) => { if (layer.isActive) { - elem.classList.remove('layer-hidden'); + elem.classList.remove('map-layer-hidden'); } else { - elem.classList.add('layer-hidden'); + elem.classList.add('map-layer-hidden'); } }); } + + navigateToMapQueryMode() { + // TODO: The layer that was created from the results only contains the first 10 rows. We somehow need to + // also need to give the map view a way to load the rest of the results. + console.log("Map layers before navigation", this.layers) + this.layerSettings.setLayers(this.layers) + this._router.navigate(['/views/querying/gis']) + } } diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.html b/src/app/views/querying/gis/components/layers/map-layers.component.html index 66d3aed8..b94eb8ed 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.html +++ b/src/app/views/querying/gis/components/layers/map-layers.component.html @@ -4,14 +4,14 @@
    - @for (layer of layers; track layer.name) { +

    Preview data @@ -26,11 +26,9 @@

    - + {{ layer.index }}
    @@ -41,19 +39,19 @@

    + d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z"/> + d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c8.4-19.3 10.6-41.4 4.8-63.3c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zM373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5L373 389.9z"/>

    @@ -61,10 +59,10 @@

    {{ layer.name }}({{ layer.data.length }}) + [cPopover]="dataPopover" + cPopoverPlacement="left" + [cPopoverTrigger]="'click'" + class="preview-data">({{ layer.data.length }})
    - } +

    +
    0 @@ -99,8 +98,8 @@

    @@ -125,8 +124,8 @@

    Add layer
    cSelect id="layerVisualizationSelect"> @@ -134,17 +133,18 @@
    Add layer
    Query - +
    diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index 7121d3df..54ab0a89 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -164,13 +164,9 @@ export class MapLayersComponent implements OnInit, AfterViewInit { this.websocket = new WebSocket(); this.initWebsocket(); - effect(() => { - const res = this.results(); - - untracked(() => { - console.log(res) - }) - }); + // effect(() => { + // const res = this.results(); + // }); } private initWebsocket() { @@ -196,17 +192,17 @@ export class MapLayersComponent implements OnInit, AfterViewInit { } ngOnInit(): void { + console.log("map-layers.component.ts ngOnInit(). Layers=", this.layers) + this.layerSettings.layers$.subscribe((layers) => { if (!layers) { return; } - this.layers = layers; + this.layers = [...layers]; + console.log("map-layers.component layerSettings.subscribe: ", this.layers) this.renderedLayers = this.deepCopyLayers(layers); this.layerSettings.setCanRerenderLayers(false); - - // Disable Change Detection and manually trigger to prevent rerenders when mouse moves - // this.cdRef.markForCheck(); }); this.layerSettings.modifiedVisualization$.subscribe((config) => { @@ -229,6 +225,10 @@ export class MapLayersComponent implements OnInit, AfterViewInit { // this.updateLayers(getSampleMapLayers()); } + trackByLayerName(index: number, layer: MapLayer): string { + return layer.name; + } + @HostListener('window:resize') onResize(): void { this.setMapHeight(this.getMapHeight()); diff --git a/src/app/views/querying/gis/components/map/map.component.css b/src/app/views/querying/gis/components/map/map.component.css deleted file mode 100644 index 694a796e..00000000 --- a/src/app/views/querying/gis/components/map/map.component.css +++ /dev/null @@ -1,48 +0,0 @@ -.map-container { - position: relative; - height: 100%; - border: 1px solid lightgray; -} - -#map { - position: absolute; - inset: 0; - z-index: 1; -} - -.map-overlay { - position: absolute; - inset: 0; - z-index: 2; - pointer-events: none; - display: grid; - background: rgba(0, 0, 0, 0.1); - place-items: center; -} - -.spinner-container { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.5rem; - background: rgba(0, 0, 0, 0.25); - padding: 2rem; -} - -.render-button { - position: absolute; - z-index: 3; - bottom: 2rem; - right: 2rem; -} - -.spinner-container > span { - font-size: 1.5rem; - font-weight: 400; - color: white; -} - - -.leaflet-overlay-pane > svg { - transition: opacity 50ms ease-in-out; -} diff --git a/src/app/views/querying/gis/components/map/map.component.html b/src/app/views/querying/gis/components/map/map.component.html deleted file mode 100644 index 297052cf..00000000 --- a/src/app/views/querying/gis/components/map/map.component.html +++ /dev/null @@ -1,14 +0,0 @@ -
    -
    -
    -
    - - {{ this.isLoadingMessage }} - - {{ this.isLoadingMessage }} -
    -
    - -
    diff --git a/src/app/views/querying/gis/components/map/map.component.ts b/src/app/views/querying/gis/components/map/map.component.ts deleted file mode 100644 index f296de6e..00000000 --- a/src/app/views/querying/gis/components/map/map.component.ts +++ /dev/null @@ -1,380 +0,0 @@ -import { AfterViewInit, Component, OnInit } from '@angular/core'; -import * as d3 from 'd3'; -import { GeoPath, GeoPermissibleObjects } from 'd3'; -import * as d3Geo from 'd3-geo'; -import * as L from 'leaflet'; -import { LayerSettingsService } from '../../services/layersettings.service'; -import { MapLayer } from '../../models/MapLayer.model'; -import { MapGeometryWithData } from '../../models/RowResult.model'; -import { ButtonDirective, SpinnerComponent } from '@coreui/angular'; -import { NgIf } from '@angular/common'; - -@Component({ - selector: 'app-map', - standalone: true, - imports: [SpinnerComponent, NgIf, ButtonDirective], - templateUrl: './map.component.html', - styleUrls: ['./map.component.css'], -}) -export class MapComponent implements OnInit, AfterViewInit { - currentBaseLayer: L.TileLayer | undefined; - layers: MapLayer[] = []; - isLoading: boolean = false; - isLoadingMessage: string = 'TODO isLoadingMessage'; - canRerenderLayers: boolean = false; - - readonly MIN_ZOOM = 0; - readonly MAX_ZOOM = 19; - readonly INITIAL_ZOOM = 6; - private map!: L.Map; - private svg: - | d3.Selection - | undefined; - private g: d3.Selection | undefined; - private circles: - | d3.Selection - | undefined; - private paths: - | d3.Selection - | undefined; - private pathGenerator!: GeoPath; - private tooltip!: d3.Selection; - - constructor(protected layerSettings: LayerSettingsService) {} - - ngOnInit() { - this.layerSettings.selectedBaseLayer$.subscribe((item) => { - if (!item) { - return; - } - - if (this.currentBaseLayer) { - this.map.removeLayer(this.currentBaseLayer); - } - - if (item != 'EMPTY') { - this.currentBaseLayer = L.tileLayer(item, { - maxZoom: this.MAX_ZOOM, - attribution: - '© OpenStreetMap contributors', - }).addTo(this.map); - } - }); - - this.layerSettings.layers$.subscribe((layers) => { - if (!layers || !layers.length) { - return; - } - - this.layers = layers; - this.renderLayersWithD3(); - }); - - this.layerSettings.canRerenderLayers$.subscribe((canRerenderLayers) => { - this.canRerenderLayers = canRerenderLayers; - }); - - this.layerSettings.toggleLayerVisibility$.subscribe((layer) => { - this.toggleLayerVisibility(layer); - }); - } - - ngAfterViewInit(): void { - const leafletMap = L.map('map').setView([52, 10], this.INITIAL_ZOOM); - this.map = leafletMap; - this.svg = d3.select(this.map.getPanes().overlayPane).append('svg'); - this.g = this.svg.append('g').attr('class', 'leaflet-zoom-hide'); - this.tooltip = d3 - .select('body') - .append('div') - .style('position', 'absolute') - .style('font-size', '0.75rem') - .style('font-family', 'monospace') - .style('background', 'white') - .style('border', '1px solid #ccc') - .style('padding', '5px') - .style('display', 'none') - .style('z-index', '9999'); - - function projectPoint(this: any, x: number, y: number) { - const point = leafletMap.latLngToLayerPoint(new L.LatLng(y, x)); - this.stream.point(point.x, point.y); - } - - const transform = d3Geo.geoTransform({ point: projectPoint }); - this.pathGenerator = d3Geo.geoPath().projection(transform); - - this.map.on('zoomend', () => { - this.updateSvgPosition(); - }); - this.map.on('moveend', () => { - if (!this.svg || !this.g) { - return; - } - const bounds = this.map.getBounds(); - const topLeft = this.map.latLngToLayerPoint(bounds.getNorthWest()); - const bottomRight = this.map.latLngToLayerPoint( - bounds.getSouthEast(), - ); - this.svg - .style('width', '999999px') - .style('height', '999999px') - .style('left', topLeft.x + 'px') - .style('top', topLeft.y + 'px'); - this.g.attr('transform', `translate(${-topLeft.x}, ${-topLeft.y})`); - }); - - this.currentBaseLayer = L.tileLayer( - 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - { - maxZoom: this.MAX_ZOOM, - attribution: - '© OpenStreetMap contributors', - }, - ).addTo(this.map); - } - - showLoadingSpinner(message: string) { - this.isLoading = true; - this.isLoadingMessage = message; - } - - updateSvgPosition() { - this.showLoadingSpinner('Reposition shapes on map'); - - setTimeout(() => { - try { - if (!this.g || !this.svg) { - return; - } - - if (this.paths) { - this.paths.attr('d', (d) => this.pathGenerator(d.geometry)); - } - - if (this.circles) { - this.circles - .each((d) => { - const layerPoint = this.map.latLngToLayerPoint([ - d.getPoint()!.coordinates[1], - d.getPoint()!.coordinates[0], - ]); - d.cache['x'] = layerPoint.x; - d.cache['y'] = layerPoint.y; - }) - .attr('cx', (d) => d.cache['x']) - .attr('cy', (d) => d.cache['y']); - } - - const bounds = this.map.getBounds(); - const topLeft = this.map.latLngToLayerPoint( - bounds.getNorthWest(), - ); - const bottomRight = this.map.latLngToLayerPoint( - bounds.getSouthEast(), - ); - this.svg - .style('width', '999999px') - .style('height', '999999px') - .style('left', topLeft.x + 'px') - .style('top', topLeft.y + 'px'); - this.g.attr( - 'transform', - `translate(${-topLeft.x}, ${-topLeft.y})`, - ); - } finally { - this.isLoading = false; - } - }, 0); - } - - renderLayersWithD3() { - this.showLoadingSpinner('Rendering layers'); - - setTimeout(() => { - try { - if (!this.svg || !this.g) { - return; - } - - // Remove all previously added elements - this.g.selectAll('*').remove(); - - const points: MapGeometryWithData[] = []; - const paths: MapGeometryWithData[] = []; - - // Add shapes from each layer to array - for (const layer of this.layers.slice().reverse()) { - console.log(`Render layer [${layer.name}]. Initialize...`); - - // Initialize all configs - layer.pointShapeVisualization.init(layer.data); - layer.areaShapeVisualization.init(layer.data); - layer.colorVisualization.init(layer.data); - - points.push( - ...layer.data.filter( - (d) => d.geometry.type === 'Point', - ), - ); - paths.push( - ...layer.data.filter( - (d) => d.geometry.type !== 'Point', - ), - ); - } - - // Render all points - // TODO: Circles are always on the bottom this way... - console.log('Create Points: ', points); - this.circles = this.createPoints(points); - - console.log('Create Paths: ', paths); - this.paths = this.createPaths(paths); - - // Set SVG position correctly - this.updateSvgPosition(); - - // Center the map around the data. - // TODO: Do the same thing for paths. - const latLngs : L.LatLng[] = []; - this.circles!.each(d => { - // d has the property geometry, which is a GeoJSON point of type - latLngs.push(L.latLng(d.getPoint().coordinates[1], d.getPoint().coordinates[0],)); - }); - console.log(latLngs) - // TODO: Currently, only do this, if the dataset is not too big. Otherwise we zoom way out, - // and zooming back in can be very slow. (if the points are all over the world) - if (latLngs.length > 0 && latLngs.length <= 1000) { - const latLngBounds = L.latLngBounds(latLngs); - if (latLngBounds.isValid()){ - this.map.fitBounds(latLngBounds); - } - } - } finally { - this.isLoading = false; - } - }, 0); - } - - createPoints(points: MapGeometryWithData[]) { - if (!this.g) { - return; - } - - const tt = this.tooltip; - - return this.g - .selectAll('circle') - .data(points) - .enter() - .append('circle') - .attr('layer-name', (d) => d.layer!.name) - .attr('layer-index', (d) => d.layer!.index.toString()) - .attr('r', (d) => - d.layer!.pointShapeVisualization.getValueForAttribute('r', d), - ) - .attr('fill', (d) => - d.layer!.colorVisualization.getValueForAttribute('fill', d), - ) - .each((d) => { - const layerPoint = this.map.latLngToLayerPoint([ - d.getPoint().coordinates[1], - d.getPoint().coordinates[0], - ]); - d.cache['x'] = layerPoint.x; - d.cache['y'] = layerPoint.y; - }) - .attr('cx', (d) => d.cache['x']) - .attr('cy', (d) => d.cache['y']) - .style("pointer-events", "auto") - .style("cursor", "pointer") - .on('mouseover', function (event, d) { - tt.style('display', 'block').html(JSON.stringify(d.data, null, 1)); - }) - .on('mousemove', function (event) { - tt.style('top', event.pageY + 10 + 'px').style( - 'left', - event.pageX + 10 + 'px', - ); - }) - .on('mouseout', function () { - tt.style('display', 'none'); - }); - } - - createPaths(paths: MapGeometryWithData[]) { - if (!this.g) { - return; - } - - const tt = this.tooltip - - return this.g - .selectAll('.paths') - .data(paths) - .enter() - .append('path') - .attr('layer-name', (d) => d.layer!.name) - .attr('layer-index', (d) => d.layer!.index.toString()) - .attr('d', (d) => this.pathGenerator(d.geometry)) - .attr('stroke-width', (d) => - d.layer!.areaShapeVisualization.getValueForAttribute( - 'stroke-width', - d, - ), - ) - .attr('stroke', (d) => - d.layer!.colorVisualization.getValueForAttribute('stroke', d), - ) - .attr('fill', (d) => - d.layer!.colorVisualization.getValueForAttribute('fill', d), - ) - .attr('fill-opacity', (d) => - d.layer!.colorVisualization.getValueForAttribute( - 'fill-opacity', - d, - ), - ) - .style("pointer-events", "auto") - .style("cursor", "pointer") - .on('mouseover', function (event, d) { - console.log("hover") - tt.style('display', 'block').html(JSON.stringify(d.data, null, 2)); - }) - .on('mousemove', function (event) { - tt.style('top', event.pageY + 10 + 'px').style( - 'left', - event.pageX + 10 + 'px', - ); - }) - .on('mouseout', function () { - tt.style('display', 'none'); - }); - } - - toggleLayerVisibility(layer: MapLayer) { - if (!this.g?.node()) { - throw new Error('SVG g does not exist.'); - } - - const layerElements = this.g - .node()! - .querySelectorAll( - `[layer-name='${layer.name}'][layer-index='${layer.index.toString()}']`, - ); - - if (!layerElements.length) { - // Nothing to do - return; - } - - layerElements.forEach((elem) => { - if (layer.isActive) { - elem.classList.remove('layer-hidden'); - } else { - elem.classList.add('layer-hidden'); - } - }); - } -} diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index da6f0387..ab1127e1 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -104,6 +104,7 @@ export class MapLayer { throw Error(`Cannot convert CombinedResult to MapLayer. Unknown document model: ${result.dataModel}`); } layer.addData(mapData); + layer.index = 1; console.log('Created layer: ', layer); return layer; } diff --git a/src/scss/_custom.scss b/src/scss/_custom.scss index 2475c180..703d4072 100644 --- a/src/scss/_custom.scss +++ b/src/scss/_custom.scss @@ -46,10 +46,14 @@ audio, video { background-color: $footer-bg; } - .add-btn { width: 50px; height: 50px; margin-top: 90px; flex: 0; } + +.map-layer-hidden { + /* This classes is added through JavaScript, and needs to be included in the global style to work. */ + visibility: hidden; +} From a0f03e34f1d36fd3990a1884890a6cf68d63f13c Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Tue, 3 Dec 2024 18:05:38 +0100 Subject: [PATCH 12/60] Add log statements to debug why layer settings do not show layers --- .../data-view/data-map/data-map.component.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index 8410d076..588a0e04 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -50,13 +50,10 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView if (!result) { return; } - this.createLayerFromResult(result) - }); - } - createLayerFromResult(result: CombinedResult) { - this.layers = [MapLayer.from(result)] - this.renderLayersWithD3() + // Create layer from results + this.layerSettings.setLayers([MapLayer.from(result)]) + }); } ngOnInit() { @@ -82,6 +79,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView }); this.layerSettings.layers$.subscribe((layers) => { + console.log("data-map.component.ts layerSettings.layers$.subscribe(). Layers=", layers) if (!layers || !layers.length) { return; } From 48949f3169952fe1ac133f1c38889a66bc6b45ff Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Tue, 3 Dec 2024 22:30:41 +0100 Subject: [PATCH 13/60] try to get queries to work --- .../data-view/data-map/data-map.component.ts | 22 +++++--- .../components/layers/map-layers.component.ts | 52 +++++++++++++------ .../querying/gis/models/MapLayer.model.ts | 1 - 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index 588a0e04..1084f679 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -1,4 +1,4 @@ -import {AfterViewInit, Component, effect, Input} from '@angular/core'; +import {AfterViewInit, Component, effect, Input, OnDestroy} from '@angular/core'; import {DataTemplateComponent} from '../data-template/data-template.component'; import * as d3 from 'd3'; import {GeoPath, GeoPermissibleObjects} from 'd3'; @@ -14,12 +14,13 @@ import {CombinedResult} from "../data-view.model"; templateUrl: './data-map.component.html', styleUrls: ['./data-map.component.scss'] }) -export class DataMapComponent extends DataTemplateComponent implements AfterViewInit { +export class DataMapComponent extends DataTemplateComponent implements AfterViewInit, OnDestroy { // If the map is shown inside the results section, different styling needs to be applied. @Input() isInsideResults: boolean = false; currentBaseLayer: L.TileLayer | undefined; layers: MapLayer[] = []; + resultLayer? : MapLayer = undefined; isLoading: boolean = false; isLoadingMessage: string = 'TODO isLoadingMessage'; canRerenderLayers: boolean = false; @@ -51,8 +52,12 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView return; } - // Create layer from results - this.layerSettings.setLayers([MapLayer.from(result)]) + // I could send the query string to the layers settings, and execute it there. + + // this.resultLayer = MapLayer.from(result); + + // TODO: If there already are results in the map query view, + // this.layerSettings.setLayers([MapLayer.from(result), ...this.layers]) }); } @@ -80,10 +85,6 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView this.layerSettings.layers$.subscribe((layers) => { console.log("data-map.component.ts layerSettings.layers$.subscribe(). Layers=", layers) - if (!layers || !layers.length) { - return; - } - this.layers = layers; this.renderLayersWithD3(); }); @@ -97,6 +98,11 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView }); } + ngOnDestroy() { + // TODO: This breaks the navigation from results to map query view + // this.layerSettings.setLayers([]) + } + ngAfterViewInit(): void { const leafletMap = L.map('map').setView([52, 10], this.INITIAL_ZOOM); this.map = leafletMap; diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index 54ab0a89..7adea6d2 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -1,5 +1,5 @@ import { - AfterViewInit, + AfterViewInit, ChangeDetectorRef, Component, effect, ElementRef, HostListener, @@ -56,7 +56,9 @@ import {RelationalResult, Result} from "../../../../../components/data-view/mode import {InformationObject} from "../../../../../models/information-page.model"; import {WebuiSettingsService} from "../../../../../services/webui-settings.service"; import {Subscription} from "rxjs"; -import {QueryRequest} from "../../../../../models/ui-request.model"; +import {EntityRequest, QueryRequest} from "../../../../../models/ui-request.model"; +import {CombinedResult} from "../../../../../components/data-view/data-view.model"; +import {CatalogService} from "../../../../../services/catalog.service"; type BaseLayer = { name: string; value: string }; @@ -99,6 +101,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit { // DI private readonly _crud = inject(CrudService); private readonly _settings = inject(WebuiSettingsService); + public readonly _catalog = inject(CatalogService); // Querying websocket: WebSocket; @@ -124,7 +127,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit { protected renderedLayers: MapLayer[] = []; // Add Layer - protected query = "" + protected query = "db.geoCollection2.find({})" protected isAddLayerModalVisible = false; protected loadedGeoJsonFile?: GeoJSON.FeatureCollection = undefined; protected loadedGeoJsonFileName: string = ''; @@ -158,26 +161,44 @@ export class MapLayersComponent implements OnInit, AfterViewInit { constructor( protected layerSettings: LayerSettingsService, private el: ElementRef, - private renderer: Renderer2 - // private cdRef: ChangeDetectorRef, + private renderer: Renderer2, ) { this.websocket = new WebSocket(); this.initWebsocket(); - // effect(() => { - // const res = this.results(); - // }); + effect(() => { + const res = this.results(); + console.log("res=", res) + if (res.length > 0){ + const combinedResult = CombinedResult.from(res[0]) + console.log("CombinedResult=", combinedResult) + this.updateLayers([ MapLayer.from(combinedResult) ]) + + if (combinedResult.hasMore){ + // TODO: What is EntityID and why is it null? + const request = new EntityRequest(combinedResult.entityId, combinedResult.namespace, combinedResult.currentPage); + console.log("get more") + + if (!this._crud.getEntityData(this.websocket, request)) { + console.log("Error getEntityData") + // this.results.set(CombinedResult.fromRelational(new RelationalResult('Could not establish a connection with the server.'))); + } + } + + } + }); } private initWebsocket() { const sub = this.websocket.onMessage().subscribe({ next: msg => { + console.log("websocket.msg=", msg) if (Array.isArray(msg) && ((msg[0].hasOwnProperty('data') || msg[0].hasOwnProperty('affectedTuples') || msg[0].hasOwnProperty('error')))) { // array of ResultSet this.results.set([]>msg); - } }, error: err => { + console.log("websocket.err=", err) //this._leftSidebar.setError('Lost connection with the server.'); setTimeout(() => { this.initWebsocket(); @@ -195,14 +216,11 @@ export class MapLayersComponent implements OnInit, AfterViewInit { console.log("map-layers.component.ts ngOnInit(). Layers=", this.layers) this.layerSettings.layers$.subscribe((layers) => { - if (!layers) { - return; - } - - this.layers = [...layers]; + this.layers = layers; console.log("map-layers.component layerSettings.subscribe: ", this.layers) this.renderedLayers = this.deepCopyLayers(layers); this.layerSettings.setCanRerenderLayers(false); + this.updateLayerUi(); }); this.layerSettings.modifiedVisualization$.subscribe((config) => { @@ -329,12 +347,14 @@ export class MapLayersComponent implements OnInit, AfterViewInit { async addLayer() { switch (this.addLayerMode) { case LayerContext.Query: - // TODO: Run query parse results // TODO: queryLanguage, namespace if (!this._crud.anyQuery(this.websocket, new QueryRequest(this.query, false, false, "MQL", "test"))) { this.results.set([new RelationalResult('Could not establish a connection with the server.')]); + console.log("Querry error") + } else { + console.log("Querry success") } - + break case LayerContext.Results: case LayerContext.DB: if (!this.selectedPolyphenyDataset) { diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index ab1127e1..da6f0387 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -104,7 +104,6 @@ export class MapLayer { throw Error(`Cannot convert CombinedResult to MapLayer. Unknown document model: ${result.dataModel}`); } layer.addData(mapData); - layer.index = 1; console.log('Created layer: ', layer); return layer; } From 631e02826a6436dfab94e9b0aa9fbcd2eafd901e Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Tue, 3 Dec 2024 23:26:04 +0100 Subject: [PATCH 14/60] Add leaflet-draw + npm packages for typescript types (d3, leaflet) --- angular.json | 3 +- package-lock.json | 265 +++++++++++++++++- package.json | 4 + .../data-graph/data-graph.component.ts | 19 +- .../data-view/data-map/data-map.component.ts | 19 +- 5 files changed, 299 insertions(+), 11 deletions(-) diff --git a/angular.json b/angular.json index 8c513849..63a02e2c 100644 --- a/angular.json +++ b/angular.json @@ -52,7 +52,8 @@ "node_modules/@ali-hm/angular-tree-component/css/angular-tree-component.css", "node_modules/katex/dist/katex.min.css", "node_modules/prismjs/themes/prism-okaidia.css", - "src/app/components/data-view/data-map/leaflet.css" + "src/app/components/data-view/data-map/leaflet.css", + "node_modules/leaflet-draw/dist/leaflet.draw.css" ], "stylePreprocessorOptions": { "includePaths": [ diff --git a/package-lock.json b/package-lock.json index 4fe6613c..025fca2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "jquery-ui": "^1.13.0", "katex": "^0.16.0", "leaflet": "^1.9.4", + "leaflet-draw": "^1.0.4", "lodash": "^4.17.20", "marked": "^9.1.6", "moment": "^2.30.1", @@ -75,9 +76,12 @@ "@angular/compiler-cli": "^17.2.3", "@angular/language-service": "^17.2.3", "@ngtools/webpack": "^17.2.2", + "@types/d3": "^7.4.3", "@types/hammerjs": "^2.0.36", "@types/jasmine": "~3.6.0", "@types/jasminewd2": "^2.0.8", + "@types/leaflet": "^1.9.14", + "@types/leaflet-draw": "^1.0.11", "@types/marked": "^4.3.0", "@types/node": "^12.11.1", "codelyzer": "^6.0.2", @@ -7753,11 +7757,197 @@ "@types/node": "*" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "dev": true + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "dev": true + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "dev": true + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true + }, "node_modules/@types/d3-scale": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", - "optional": true, + "devOptional": true, "dependencies": { "@types/d3-time": "*" } @@ -7766,19 +7956,65 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==", - "optional": true + "devOptional": true + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "dev": true, + "dependencies": { + "@types/d3-path": "*" + } }, "node_modules/@types/d3-time": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", - "optional": true + "devOptional": true + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } }, "node_modules/@types/d3-voronoi": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/@types/d3-voronoi/-/d3-voronoi-1.1.12.tgz", "integrity": "sha512-DauBl25PKZZ0WVJr42a6CNvI6efsdzofl9sajqZr2Gf5Gu733WkDdUGiPkUHXiUvYGzNNlFQde2wdZdfQPG+yw==" }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -7889,6 +8125,24 @@ "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", "dev": true }, + "node_modules/@types/leaflet": { + "version": "1.9.14", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.14.tgz", + "integrity": "sha512-sx2q6MDJaajwhKeVgPSvqXd8rhNJSTA3tMidQGduZn9S6WBYxDkCpSpV5xXEmSg7Cgdk/5vJGhVF1kMYLzauBg==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/leaflet-draw": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/leaflet-draw/-/leaflet-draw-1.0.11.tgz", + "integrity": "sha512-dyedtNm3aSmnpi6FM6VSl28cQuvP+MD7pgpXyO3Q1ZOCvrJKmzaDq0P3YZTnnBs61fQCKSnNYmbvCkDgFT9FHQ==", + "dev": true, + "dependencies": { + "@types/leaflet": "*" + } + }, "node_modules/@types/marked": { "version": "4.3.0", "dev": true, @@ -13475,6 +13729,11 @@ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" }, + "node_modules/leaflet-draw": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/leaflet-draw/-/leaflet-draw-1.0.4.tgz", + "integrity": "sha512-rsQ6saQO5ST5Aj6XRFylr5zvarWgzWnrg46zQ1MEOEIHsppdC/8hnN8qMoFvACsPvTioAuysya/TVtog15tyAQ==" + }, "node_modules/less": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", diff --git a/package.json b/package.json index 20fe2357..65829493 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "jquery-ui": "^1.13.0", "katex": "^0.16.0", "leaflet": "^1.9.4", + "leaflet-draw": "^1.0.4", "lodash": "^4.17.20", "marked": "^9.1.6", "moment": "^2.30.1", @@ -79,9 +80,12 @@ "@angular/compiler-cli": "^17.2.3", "@angular/language-service": "^17.2.3", "@ngtools/webpack": "^17.2.2", + "@types/d3": "^7.4.3", "@types/hammerjs": "^2.0.36", "@types/jasmine": "~3.6.0", "@types/jasminewd2": "^2.0.8", + "@types/leaflet": "^1.9.14", + "@types/leaflet-draw": "^1.0.11", "@types/marked": "^4.3.0", "@types/node": "^12.11.1", "codelyzer": "^6.0.2", diff --git a/src/app/components/data-view/data-graph/data-graph.component.ts b/src/app/components/data-view/data-graph/data-graph.component.ts index cf205d63..d4020642 100644 --- a/src/app/components/data-view/data-graph/data-graph.component.ts +++ b/src/app/components/data-view/data-graph/data-graph.component.ts @@ -135,7 +135,8 @@ export class DataGraphComponent extends DataTemplateComponent { .force('center', d3.forceCenter(width / 2, height / 2)) .force('charge', d3.forceManyBody().strength(-this.initialNodeIds.size)) .force('collide', d3.forceCollide(100).strength(0.9).radius(40)) - .force('link', d3.forceLink().id(d => d.id).distance(160)); + // TODO TS2339: Property id does not exist on type SimulationNodeDatum + // .force('link', d3.forceLink().id(d => d.id).distance(160)); // disable charge after initial setup @@ -333,13 +334,15 @@ export class DataGraphComponent extends DataTemplateComponent { t = newOverlay.append('path').attr('fill', 'transparent') .attr('stroke-width', overlayStroke) .attr('stroke', 'transparent') - .attr('d', arc({startAngle: -(Math.PI / 3), endAngle: (Math.PI / 3)})); + // TODO TS2769: No overload matches this call. + // .attr('d', arc({startAngle: -(Math.PI / 3), endAngle: (Math.PI / 3)})); right = newOverlay.append('path').attr('fill', 'grey') .attr('stroke-width', overlayStroke) .attr('stroke', 'white') .style('cursor', 'pointer') - .attr('d', arc({startAngle: 0, endAngle: Math.PI})) + // TODO TS2769: No overload matches this call. + // .attr('d', arc({startAngle: 0, endAngle: Math.PI})) .on('mouseover', function (d) { d3.select(this).attr('fill', 'darkgray'); }) @@ -366,7 +369,8 @@ export class DataGraphComponent extends DataTemplateComponent { .attr('stroke-width', overlayStroke) .attr('stroke', 'white') .style('cursor', 'pointer') - .attr('d', arc({startAngle: -Math.PI, endAngle: 0})) + // TODO TS2769: No overload matches this call. + // .attr('d', arc({startAngle: -Math.PI, endAngle: 0})) .on('mouseover', function (d) { d3.select(this).attr('fill', 'darkgray'); }) @@ -514,13 +518,16 @@ export class DataGraphComponent extends DataTemplateComponent { function activate() { // Attach nodes to the simulation, add listener on the "tick" event simulation - .nodes(graph.nodes) + // TODO TS2345: Argument of type Node[] is not assignable to parameter of type SimulationNodeDatum[] + // Type Node has no properties in common with type SimulationNodeDatum + // .nodes(graph.nodes) .on('tick', onTick); // Associate the lines with the "link" force simulation .force('link') - .links(graph.edges); + // TODO TS2339: Property links does not exist on type Force + // .links(graph.edges); simulation.alpha(1).alphaTarget(0).restart(); } diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index 1084f679..5bb01763 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -4,6 +4,7 @@ import * as d3 from 'd3'; import {GeoPath, GeoPermissibleObjects} from 'd3'; import * as d3Geo from 'd3-geo'; import * as L from 'leaflet'; +import 'leaflet-draw'; import {LayerSettingsService} from "../../../views/querying/gis/services/layersettings.service"; import {MapLayer} from "../../../views/querying/gis/models/MapLayer.model"; import {MapGeometryWithData} from "../../../views/querying/gis/models/RowResult.model"; @@ -20,7 +21,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView currentBaseLayer: L.TileLayer | undefined; layers: MapLayer[] = []; - resultLayer? : MapLayer = undefined; + resultLayer?: MapLayer = undefined; isLoading: boolean = false; isLoadingMessage: string = 'TODO isLoadingMessage'; canRerenderLayers: boolean = false; @@ -106,6 +107,22 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView ngAfterViewInit(): void { const leafletMap = L.map('map').setView([52, 10], this.INITIAL_ZOOM); this.map = leafletMap; + + // Leaflet.Draw edit toolbar + const drawnItems = new L.FeatureGroup(); + leafletMap.addLayer(drawnItems); + const drawControl = new L.Control.Draw({ + edit: { + featureGroup: drawnItems + } + }); + leafletMap.addControl(drawControl); + + leafletMap.on(L.Draw.Event.CREATED, function (event) { + const layer = event.layer + drawnItems.addLayer(layer); + }); + this.svg = d3.select(this.map.getPanes().overlayPane).append('svg'); this.g = this.svg.append('g').attr('class', 'leaflet-zoom-hide'); this.tooltip = d3 From a1ac558661fbe8225f9a37454626a161c7fe83b2 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Wed, 4 Dec 2024 00:34:08 +0100 Subject: [PATCH 15/60] disable other leaflet-draw options, 1 is enough --- .../data-view/data-map/data-map.component.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index 5bb01763..c0b7df20 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -114,11 +114,21 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView const drawControl = new L.Control.Draw({ edit: { featureGroup: drawnItems + }, + draw: { + circle: false, + marker: false, + polyline: false, + rectangle: false, + circlemarker: false } }); leafletMap.addControl(drawControl); leafletMap.on(L.Draw.Event.CREATED, function (event) { + + // TODO: What is the best way to use this shape to add a filter to another layer? + // - We could add this shape as its own layer. const layer = event.layer drawnItems.addLayer(layer); }); From 1b184adb54769d1dcac1385838adfb55932a5ade Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Fri, 6 Dec 2024 18:59:07 +0100 Subject: [PATCH 16/60] Extract app-query-editor and app-submit-query-button out of app-console We want to use these controls for the GIS query mode, but we do not want to include the whole editor. --- .../code-editor/query-editor.component.html | 31 ++++ .../code-editor/query-editor.component.scss | 32 +++++ .../code-editor/query-editor.component.ts | 134 ++++++++++++++++++ .../console/components/console-helper.ts | 3 + .../submit-query-button.component.html | 13 ++ .../submit-query-button.component.scss | 0 .../submit-query-button.component.ts | 23 +++ .../querying/console/console.component.html | 67 ++------- .../querying/console/console.component.scss | 33 ----- .../querying/console/console.component.ts | 127 +++-------------- src/app/views/views.module.ts | 6 + 11 files changed, 273 insertions(+), 196 deletions(-) create mode 100644 src/app/views/querying/console/components/code-editor/query-editor.component.html create mode 100644 src/app/views/querying/console/components/code-editor/query-editor.component.scss create mode 100644 src/app/views/querying/console/components/code-editor/query-editor.component.ts create mode 100644 src/app/views/querying/console/components/console-helper.ts create mode 100644 src/app/views/querying/console/components/submit-query-button/submit-query-button.component.html create mode 100644 src/app/views/querying/console/components/submit-query-button/submit-query-button.component.scss create mode 100644 src/app/views/querying/console/components/submit-query-button/submit-query-button.component.ts diff --git a/src/app/views/querying/console/components/code-editor/query-editor.component.html b/src/app/views/querying/console/components/code-editor/query-editor.component.html new file mode 100644 index 00000000..940e01b4 --- /dev/null +++ b/src/app/views/querying/console/components/code-editor/query-editor.component.html @@ -0,0 +1,31 @@ +
    +
    + Namespace: {{ activeNamespace() }} +
    + Namespace: + +
    +
    + + +
    +
    +
    + +
    +
    \ No newline at end of file diff --git a/src/app/views/querying/console/components/code-editor/query-editor.component.scss b/src/app/views/querying/console/components/code-editor/query-editor.component.scss new file mode 100644 index 00000000..1a17f128 --- /dev/null +++ b/src/app/views/querying/console/components/code-editor/query-editor.component.scss @@ -0,0 +1,32 @@ +.document-db-info { + text-align: right; +} + +.advanced-console-db-info { + display: inline-block; + + option { + font-size: 10px; + font-style: italic; + } +} + +.grey-bg { + background-color: #f0f3f5; + border: none; + border-bottom: 1px white solid; +} + +.grey-border { + border: 1px rgba(0, 0, 0, .125) solid; +} + +.advanced-console { + height: 185.617px; + margin: 1px; +} + +.simple-console { + height: 240px; + margin-bottom: 0; +} \ No newline at end of file diff --git a/src/app/views/querying/console/components/code-editor/query-editor.component.ts b/src/app/views/querying/console/components/code-editor/query-editor.component.ts new file mode 100644 index 00000000..f30e714f --- /dev/null +++ b/src/app/views/querying/console/components/code-editor/query-editor.component.ts @@ -0,0 +1,134 @@ +import { + Component, + effect, + EventEmitter, + inject, + Input, + OnInit, + Output, + signal, untracked, + ViewChild, + WritableSignal +} from '@angular/core'; +import {ToasterService} from "../../../../../components/toast-exposer/toaster.service"; +import {NamespaceModel} from "../../../../../models/catalog.model"; +import {QueryHistory} from "../../query-history.model"; +import {CatalogService} from "../../../../../services/catalog.service"; +import {usesAdvancedConsole} from "../console-helper"; +import {EditorComponent} from "../../../../../components/editor/editor.component"; + + +@Component({ + selector: 'app-query-editor', + templateUrl: './query-editor.component.html', + styleUrls: ['query-editor.component.scss'] +}) +export class QueryEditor implements OnInit { + public readonly _toast = inject(ToasterService); + public readonly _catalog = inject(CatalogService); + + readonly namespaces: WritableSignal = signal([]); + private readonly LOCAL_STORAGE_NAMESPACE_KEY = 'polypheny-namespace'; + showNamespaceConfig: boolean; + + @Input() language: WritableSignal = signal(""); + @Input() activeNamespace: WritableSignal = signal(""); + @ViewChild('editor', {static: false}) codeEditor: EditorComponent; + + constructor() { + effect(() => { + const namespace = this._catalog.namespaces(); + untracked(() => { + this.namespaces.set(Array.from(namespace.values())); + }); + }); + } + + ngOnInit() { + this.loadAndSetNamespaceDB(); + } + + setCode(code: string): void { + this.codeEditor.setCode(code); + } + + getCode(): string { + return this.codeEditor.getCode(); + } + + private loadAndSetNamespaceDB() { + let namespaceName = localStorage.getItem(this.LOCAL_STORAGE_NAMESPACE_KEY); + + if (namespaceName === null || (this.namespaces && this.namespaces.length > 0 && (this.namespaces().filter(n => n.name === namespaceName).length === 0))) { + if (this.namespaces() && this.namespaces().length > 0) { + namespaceName = this.namespaces()[0].name; + } else { + namespaceName = 'public'; + } + } + if (!namespaceName) { + return; + } + + this.activeNamespace.set(namespaceName); + this.storeNamespace(namespaceName); + } + + storeNamespace(name: string) { + localStorage.setItem(this.LOCAL_STORAGE_NAMESPACE_KEY, name); + } + + clearConsole() { + this.codeEditor.setCode(''); + } + + parse(code: string) { + const formatted = JSON.stringify(JSON.parse('[' + code + ']'), null, 4); + return formatted.substring(1, formatted.length - 1); + } + + formatQuery() { + let code = this.codeEditor.getCode(); + if (!code) { + return; + } + let before = ''; + const after = ')'; + + // here we replace the Json incompatible types with placeholders + const temp = code.match(/NumberDecimal\([^)]*\)/g); + + if (temp !== null) { + for (let i = 0; i < temp.length; i++) { + code = code.replace(temp[i], '"___' + i + '"'); + } + } + + const splits = code.split('('); + before = splits.shift() + '('; + + try { + let json = this.parse(splits.join('(').slice(0, -1)); + // we have to translate them back + if (temp !== null) { + for (let i = 0; i < temp.length; i++) { + json = json.replace('"___' + i + '"', temp[i]); + } + } + + this.codeEditor.setCode(before + json + after); + } catch (e) { + this._toast.warn(e); + } + } + + toggleNamespaceField() { + this.showNamespaceConfig = !this.showNamespaceConfig; + } + + changedDefaultDB(n) { + this.activeNamespace.set(n); + } + + protected readonly usesAdvancedConsole = usesAdvancedConsole; +} diff --git a/src/app/views/querying/console/components/console-helper.ts b/src/app/views/querying/console/components/console-helper.ts new file mode 100644 index 00000000..89eb6b70 --- /dev/null +++ b/src/app/views/querying/console/components/console-helper.ts @@ -0,0 +1,3 @@ +export function usesAdvancedConsole(lang: string) { + return lang === 'mql' || lang === 'cypher'; +} \ No newline at end of file diff --git a/src/app/views/querying/console/components/submit-query-button/submit-query-button.component.html b/src/app/views/querying/console/components/submit-query-button/submit-query-button.component.html new file mode 100644 index 00000000..12718b95 --- /dev/null +++ b/src/app/views/querying/console/components/submit-query-button/submit-query-button.component.html @@ -0,0 +1,13 @@ + + + + + diff --git a/src/app/views/querying/console/components/submit-query-button/submit-query-button.component.scss b/src/app/views/querying/console/components/submit-query-button/submit-query-button.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/views/querying/console/components/submit-query-button/submit-query-button.component.ts b/src/app/views/querying/console/components/submit-query-button/submit-query-button.component.ts new file mode 100644 index 00000000..ea9fc5f2 --- /dev/null +++ b/src/app/views/querying/console/components/submit-query-button/submit-query-button.component.ts @@ -0,0 +1,23 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + + +@Component({ + selector: 'app-submit-execute-button', + templateUrl: './submit-query-button.component.html', + styleUrls: ['submit-query-button.component.scss'] +}) +export class SubmitQueryButtonComponent { + @Input() loading: boolean = false; + @Input() language: string = 'sql'; + + @Output() languageChange = new EventEmitter(); + @Output() submitQuery = new EventEmitter(); + + onLanguageChange(newLanguage: string): void { + this.languageChange.emit(newLanguage); + } + + onExecute(): void { + this.submitQuery.emit(); + } +} diff --git a/src/app/views/querying/console/console.component.html b/src/app/views/querying/console/console.component.html index 830d2653..94a670ce 100644 --- a/src/app/views/querying/console/console.component.html +++ b/src/app/views/querying/console/console.component.html @@ -4,36 +4,8 @@ (keyup.shift.alt)="revertCache()" (keydown.alt.shift.enter)="submitQuery()" (keydown.alt.enter)="submitQuery()"> -
    -
    - Namespace: {{activeNamespace()}} -
    - Namespace: - -
    -
    - - -
    - -
    -
    - -
    -
    + +
      @@ -64,11 +36,11 @@ (click)="applyHistory( h.value.query, h.value.lang, false )" (dblclick)="applyHistory( h.value.query, h.value.lang, true)">
      - {{h.value.lang}} - {{h.value.displayTime()}} - {{h.value.fromNow()}} + {{ h.value.lang }} + {{ h.value.displayTime() }} + {{ h.value.fromNow() }}
      -
      {{_util.limitedString(h.value.query)}}
      +
      {{ _util.limitedString(h.value.query) }}
      - - + + > + @@ -135,9 +98,9 @@ - {{result.query}} + {{ result.query }} - {{result.affectedTuples}} + {{ result.affectedTuples }} ! diff --git a/src/app/views/querying/console/console.component.scss b/src/app/views/querying/console/console.component.scss index 57d7af54..96a9acb3 100644 --- a/src/app/views/querying/console/console.component.scss +++ b/src/app/views/querying/console/console.component.scss @@ -169,36 +169,3 @@ select { .viewButton { margin-right: 0.5em; } - -.document-db-info { - text-align: right; -} - -.advanced-console-db-info { - display: inline-block; - - option { - font-size: 10px; - font-style: italic; - } -} - -.grey-bg { - background-color: #f0f3f5; - border: none; - border-bottom: 1px white solid; -} - -.grey-border { - border: 1px rgba(0, 0, 0, .125) solid; -} - -.advanced-console { - height: 185.617px; - margin: 1px; -} - -.simple-console { - height: 240px; - margin-bottom: 0; -} diff --git a/src/app/views/querying/console/console.component.ts b/src/app/views/querying/console/console.component.ts index d838a3aa..aa67d6c0 100644 --- a/src/app/views/querying/console/console.component.ts +++ b/src/app/views/querying/console/console.component.ts @@ -28,6 +28,8 @@ import {ToasterService} from '../../../components/toast-exposer/toaster.service' import {ViewInformation} from '../../../components/data-view/data-view.component'; import {CatalogService} from '../../../services/catalog.service'; import {NamespaceModel} from '../../../models/catalog.model'; +import {usesAdvancedConsole} from "./components/console-helper"; +import {QueryEditor} from "./components/code-editor/query-editor.component"; @Component({ selector: 'app-console', @@ -35,23 +37,21 @@ import {NamespaceModel} from '../../../models/catalog.model'; styleUrls: ['./console.component.scss'] }) export class ConsoleComponent implements OnInit, OnDestroy { - private readonly _crud = inject(CrudService); private readonly _leftSidebar = inject(LeftSidebarService); private readonly _breadcrumb = inject(BreadcrumbService); private readonly _settings = inject(WebuiSettingsService); public readonly _util = inject(UtilService); - public readonly _toast = inject(ToasterService); public readonly _catalog = inject(CatalogService); private readonly _sidebar = inject(LeftSidebarService); - @ViewChild('editor', {static: false}) codeEditor; @ViewChild('historySearchInput') historySearchInput; + @ViewChild(QueryEditor) queryEditor!: QueryEditor; history: Map = new Map(); readonly MAX_HISTORY = 50; //maximum items in history private readonly LOCAL_STORAGE_HISTORY_KEY = 'query-history'; - private readonly LOCAL_STORAGE_NAMESPACE_KEY = 'polypheny-namespace'; + readonly activeNamespace: WritableSignal = signal(null); results: WritableSignal[]> = signal([]); collapsed: boolean[]; @@ -64,12 +64,13 @@ export class ConsoleComponent implements OnInit, OnDestroy { private subscriptions = new Subscription(); readonly loading: WritableSignal = signal(false); readonly language: WritableSignal = signal('sql'); + readonly query: WritableSignal = signal(''); + saveInHistory = true; showSearch = false; historySearchQuery = ''; confirmDeletingHistory; - readonly activeNamespace: WritableSignal = signal(null); - readonly namespaces: WritableSignal = signal([]); + delayedNamespace: string = null entityConfig: EntityConfig = { @@ -80,7 +81,6 @@ export class ConsoleComponent implements OnInit, OnDestroy { search: false, exploring: false }; - showNamespaceConfig: boolean; constructor() { this.websocket = new WebSocket(); @@ -88,20 +88,14 @@ export class ConsoleComponent implements OnInit, OnDestroy { // @ts-ignore if (window.Cypress) { (window).executeQuery = (query: string) => { - this.codeEditor.setCode(query); + this.queryEditor.setCode(query); + this.query.set(query) this.submitQuery(); }; } this.initWebsocket(); - - effect(() => { - const namespace = this._catalog.namespaces(); - untracked(() => { - this.namespaces.set(Array.from(namespace.values())); - }); - }); - + effect(() => { const res = this.results(); @@ -116,27 +110,6 @@ export class ConsoleComponent implements OnInit, OnDestroy { ngOnInit() { QueryHistory.fromJson(localStorage.getItem(this.LOCAL_STORAGE_HISTORY_KEY), this.history); this._breadcrumb.hide(); - - this.loadAndSetNamespaceDB(); - } - - private loadAndSetNamespaceDB() { - let namespaceName = localStorage.getItem(this.LOCAL_STORAGE_NAMESPACE_KEY); - - if (namespaceName === null || (this.namespaces && this.namespaces.length > 0 && (this.namespaces().filter(n => n.name === namespaceName).length === 0))) { - if (this.namespaces() && this.namespaces().length > 0) { - namespaceName = this.namespaces()[0].name; - } else { - namespaceName = 'public'; - } - } - if (!namespaceName) { - return; - } - - this.activeNamespace.set(namespaceName); - - this.storeNamespace(namespaceName); } ngOnDestroy() { @@ -148,17 +121,16 @@ export class ConsoleComponent implements OnInit, OnDestroy { window.onkeydown = null; } - submitQuery() { this.delayedNamespace = null; - const code = this.codeEditor.getCode(); + const code = this.query() if (!code) { return; } if (this.saveInHistory) { this.addToHistory(code, this.language()); } - if (this.usesAdvancedConsole(this.language())) { + if (usesAdvancedConsole(this.language())) { code.split(";").forEach((query: string) => { // maybe adjust const graphUse = /use *graph *([a-zA-Z][a-zA-Z0-9-_]*)/gmi @@ -177,14 +149,12 @@ export class ConsoleComponent implements OnInit, OnDestroy { } }) - if (code.match('show db')) { this._catalog.updateIfNecessary().subscribe(catalog => { this.loading.set(false); }); return; } - } this._leftSidebar.setNodes([]); @@ -224,7 +194,8 @@ export class ConsoleComponent implements OnInit, OnDestroy { applyHistory(query: string, lang: string, run: boolean) { this.language.set(lang); - this.codeEditor.setCode(query); + this.queryEditor.setCode(query); + this.query.set(query) if (run) { this.submitQuery(); } @@ -323,7 +294,7 @@ export class ConsoleComponent implements OnInit, OnDestroy { } else if (Array.isArray(msg) && ((msg[0].hasOwnProperty('data') || msg[0].hasOwnProperty('affectedTuples') || msg[0].hasOwnProperty('error')))) { // array of ResultSets if (this.delayedNamespace && !msg[0].hasOwnProperty('error')) { this.activeNamespace.set(this.delayedNamespace); - this.storeNamespace(this.delayedNamespace) + this.queryEditor.storeNamespace(this.delayedNamespace) } this.delayedNamespace = null; @@ -351,57 +322,7 @@ export class ConsoleComponent implements OnInit, OnDestroy { } createView(info: ViewInformation) { - this.codeEditor.setCode(info.fullQuery); - } - - executeView(info: ViewInformation) { - this.codeEditor.setCode(info.fullQuery); - this.submitQuery(); - } - - formatQuery() { - let code = this.codeEditor.getCode(); - if (!code) { - return; - } - let before = ''; - const after = ')'; - - // here we replace the Json incompatible types with placeholders - const temp = code.match(/NumberDecimal\([^)]*\)/g); - - if (temp !== null) { - for (let i = 0; i < temp.length; i++) { - code = code.replace(temp[i], '"___' + i + '"'); - } - } - - - const splits = code.split('('); - before = splits.shift() + '('; - - try { - let json = this.parse(splits.join('(').slice(0, -1)); - // we have to translate them back - if (temp !== null) { - for (let i = 0; i < temp.length; i++) { - json = json.replace('"___' + i + '"', temp[i]); - } - } - - this.codeEditor.setCode(before + json + after); - } catch (e) { - this._toast.warn(e); - } - } - - parse(code: string) { - const formatted = JSON.stringify(JSON.parse('[' + code + ']'), null, 4); - return formatted.substring(1, formatted.length - 1); - } - - private storeNamespace(name: string) { - localStorage.setItem(this.LOCAL_STORAGE_NAMESPACE_KEY, name); + this.queryEditor.setCode(info.fullQuery); } toggleCollapsed(i: number) { @@ -411,10 +332,6 @@ export class ConsoleComponent implements OnInit, OnDestroy { } } - clearConsole() { - this.codeEditor.setCode(''); - } - toggleCache(b: boolean) { if (this.originalCache === null) { this.originalCache = this.useCache; @@ -428,18 +345,6 @@ export class ConsoleComponent implements OnInit, OnDestroy { this.originalCache = null; } - usesAdvancedConsole(lang: string) { - return lang === 'mql' || lang === 'cypher'; - } - - toggleNamespaceField() { - this.showNamespaceConfig = !this.showNamespaceConfig; - } - - changedDefaultDB(n) { - this.activeNamespace.set(n); - } - setLanguage(language) { this.language.set(language); } diff --git a/src/app/views/views.module.ts b/src/app/views/views.module.ts index bb988b41..9cc983f8 100644 --- a/src/app/views/views.module.ts +++ b/src/app/views/views.module.ts @@ -97,6 +97,10 @@ import {EditEntityComponent} from './schema-editing/edit-entity/edit-entity.comp import {TreeModule} from '@ali-hm/angular-tree-component'; import {GisComponent} from "./querying/gis/gis.component"; import {MapLayersComponent} from "./querying/gis/components/layers/map-layers.component"; +import { + SubmitQueryButtonComponent +} from "./querying/console/components/submit-query-button/submit-query-button.component"; +import {QueryEditor} from "./querying/console/components/code-editor/query-editor.component"; @NgModule({ @@ -174,6 +178,8 @@ import {MapLayersComponent} from "./querying/gis/components/layers/map-layers.co GraphicalQueryingComponent, GisComponent, ConsoleComponent, + SubmitQueryButtonComponent, + QueryEditor, TableViewComponent, UmlComponent, SchemaEditingComponent, From 3e0fb815e3b9253f65f8ef31787700e8d8616086 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Fri, 6 Dec 2024 21:49:30 +0100 Subject: [PATCH 17/60] Add GIS components to ViewsModule and remove standalone:true --- .../code-editor/query-editor.component.ts | 2 +- .../querying/console/console.component.ts | 2 +- .../config-section.component.ts | 2 -- .../layers/map-layers.component.html | 9 ++---- .../layers/map-layers.component.scss | 9 ++++++ .../components/layers/map-layers.component.ts | 31 ++----------------- .../area-shape/area-shape.component.ts | 13 -------- .../visualization/color/color.component.ts | 13 -------- .../visualization/empty/empty.component.ts | 2 -- .../visualization/label/label.component.ts | 13 -------- .../point-shape/point-shape.component.ts | 13 -------- src/app/views/views.module.ts | 25 +++++++++++++-- 12 files changed, 38 insertions(+), 96 deletions(-) diff --git a/src/app/views/querying/console/components/code-editor/query-editor.component.ts b/src/app/views/querying/console/components/code-editor/query-editor.component.ts index f30e714f..c6960f4e 100644 --- a/src/app/views/querying/console/components/code-editor/query-editor.component.ts +++ b/src/app/views/querying/console/components/code-editor/query-editor.component.ts @@ -31,7 +31,7 @@ export class QueryEditor implements OnInit { private readonly LOCAL_STORAGE_NAMESPACE_KEY = 'polypheny-namespace'; showNamespaceConfig: boolean; - @Input() language: WritableSignal = signal(""); + @Input() language: WritableSignal = signal('sql'); @Input() activeNamespace: WritableSignal = signal(""); @ViewChild('editor', {static: false}) codeEditor: EditorComponent; diff --git a/src/app/views/querying/console/console.component.ts b/src/app/views/querying/console/console.component.ts index aa67d6c0..14fa637f 100644 --- a/src/app/views/querying/console/console.component.ts +++ b/src/app/views/querying/console/console.component.ts @@ -95,7 +95,7 @@ export class ConsoleComponent implements OnInit, OnDestroy { } this.initWebsocket(); - + effect(() => { const res = this.results(); diff --git a/src/app/views/querying/gis/components/config-section/config-section.component.ts b/src/app/views/querying/gis/components/config-section/config-section.component.ts index 3243ca7c..331ae4a4 100644 --- a/src/app/views/querying/gis/components/config-section/config-section.component.ts +++ b/src/app/views/querying/gis/components/config-section/config-section.component.ts @@ -5,8 +5,6 @@ import { NgComponentOutlet, NgIf } from '@angular/common'; @Component({ selector: 'app-config-section', - standalone: true, - imports: [ButtonDirective, CollapseDirective, NgComponentOutlet, NgIf], templateUrl: './config-section.component.html', styleUrl: './config-section.component.scss', }) diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.html b/src/app/views/querying/gis/components/layers/map-layers.component.html index b94eb8ed..374a22b1 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.html +++ b/src/app/views/querying/gis/components/layers/map-layers.component.html @@ -130,12 +130,9 @@
      Add layer
      -
      - - Query - - +
      + +
      diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.scss b/src/app/views/querying/gis/components/layers/map-layers.component.scss index b9701cf5..72fb6e64 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.scss +++ b/src/app/views/querying/gis/components/layers/map-layers.component.scss @@ -86,6 +86,15 @@ fill: #f3f4f7; } +.add-layer-query { + display: flex; + gap: 1rem; + + & > .query-controls { + align-content: flex-end; + } +} + .layer-card { display: flex; border: 1px solid lightgray; diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index 7adea6d2..3c9ecc3f 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -59,40 +59,12 @@ import {Subscription} from "rxjs"; import {EntityRequest, QueryRequest} from "../../../../../models/ui-request.model"; import {CombinedResult} from "../../../../../components/data-view/data-view.model"; import {CatalogService} from "../../../../../services/catalog.service"; +import {ViewsModule} from "../../../../views.module"; type BaseLayer = { name: string; value: string }; @Component({ selector: 'app-map-layers', - standalone: true, - imports: [ - FormLabelDirective, - FormControlDirective, - NgIf, - ModalComponent, - ModalHeaderComponent, - ModalBodyComponent, - ModalFooterComponent, - ModalTitleDirective, - ButtonCloseDirective, - ButtonDirective, - CdkDropList, - CdkDrag, - CdkDragHandle, - InputGroupComponent, - InputGroupTextDirective, - FormSelectDirective, - FormsModule, - NgForOf, - CardComponent, - CardHeaderComponent, - CardBodyComponent, - PopoverDirective, - NgxJsonViewerModule, - ConfigSectionComponent, - - - ], templateUrl: './map-layers.component.html', styleUrl: './map-layers.component.scss', // changeDetection: ChangeDetectionStrategy.OnPush, @@ -106,6 +78,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit { // Querying websocket: WebSocket; results: WritableSignal[]> = signal([]); + readonly language: WritableSignal = signal('sql'); private subscriptions = new Subscription(); protected baseLayers: BaseLayer[] = [ diff --git a/src/app/views/querying/gis/components/visualization/area-shape/area-shape.component.ts b/src/app/views/querying/gis/components/visualization/area-shape/area-shape.component.ts index 5c3a57a8..bbc8b1a9 100644 --- a/src/app/views/querying/gis/components/visualization/area-shape/area-shape.component.ts +++ b/src/app/views/querying/gis/components/visualization/area-shape/area-shape.component.ts @@ -16,19 +16,6 @@ import {AreaShapeVisualization} from "../area-shape-visualization.model"; @Component({ selector: 'app-area-shape', - standalone: true, - imports: [ - FormsModule, - InputGroupComponent, - InputGroupTextDirective, - FormControlDirective, - FormSelectDirective, - NgForOf, - NgIf, - FormCheckComponent, - FormCheckInputDirective, - FormCheckLabelDirective, - ], templateUrl: './area-shape.component.html', styleUrl: './area-shape.component.css', }) diff --git a/src/app/views/querying/gis/components/visualization/color/color.component.ts b/src/app/views/querying/gis/components/visualization/color/color.component.ts index 41d6ce53..92e35635 100644 --- a/src/app/views/querying/gis/components/visualization/color/color.component.ts +++ b/src/app/views/querying/gis/components/visualization/color/color.component.ts @@ -16,19 +16,6 @@ import { NgForOf, NgIf } from '@angular/common'; @Component({ selector: 'app-color', - standalone: true, - imports: [ - FormsModule, - InputGroupComponent, - InputGroupTextDirective, - FormControlDirective, - FormSelectDirective, - NgForOf, - NgIf, - FormCheckComponent, - FormCheckInputDirective, - FormCheckLabelDirective, - ], templateUrl: './color.component.html', styleUrl: './color.component.css', }) diff --git a/src/app/views/querying/gis/components/visualization/empty/empty.component.ts b/src/app/views/querying/gis/components/visualization/empty/empty.component.ts index 85c9421d..c483b717 100644 --- a/src/app/views/querying/gis/components/visualization/empty/empty.component.ts +++ b/src/app/views/querying/gis/components/visualization/empty/empty.component.ts @@ -5,8 +5,6 @@ import {LayerSettingsService} from "../../../services/layersettings.service"; @Component({ selector: 'app-empty', - standalone: true, - imports: [], templateUrl: './empty.component.html', }) export class EmptyComponent implements VisualizationConfiguration { diff --git a/src/app/views/querying/gis/components/visualization/label/label.component.ts b/src/app/views/querying/gis/components/visualization/label/label.component.ts index 195a1a65..de9d6eb6 100644 --- a/src/app/views/querying/gis/components/visualization/label/label.component.ts +++ b/src/app/views/querying/gis/components/visualization/label/label.component.ts @@ -17,19 +17,6 @@ import {LabelVisualization} from "../label-visualization-model"; @Component({ selector: 'app-area-shape', - standalone: true, - imports: [ - FormsModule, - InputGroupComponent, - InputGroupTextDirective, - FormControlDirective, - FormSelectDirective, - NgForOf, - NgIf, - FormCheckComponent, - FormCheckInputDirective, - FormCheckLabelDirective, - ], templateUrl: './label.component.html', styleUrl: './label.component.css', }) diff --git a/src/app/views/querying/gis/components/visualization/point-shape/point-shape.component.ts b/src/app/views/querying/gis/components/visualization/point-shape/point-shape.component.ts index 254556d4..e766766d 100644 --- a/src/app/views/querying/gis/components/visualization/point-shape/point-shape.component.ts +++ b/src/app/views/querying/gis/components/visualization/point-shape/point-shape.component.ts @@ -17,19 +17,6 @@ import {PointShapeVisualization} from "../point-shape-visualization.model"; @Component({ selector: 'app-point-shape', - standalone: true, - imports: [ - FormsModule, - InputGroupComponent, - InputGroupTextDirective, - FormControlDirective, - FormSelectDirective, - NgForOf, - NgIf, - FormCheckComponent, - FormCheckInputDirective, - FormCheckLabelDirective, - ], templateUrl: './point-shape.component.html', styleUrl: './point-shape.component.css', }) diff --git a/src/app/views/views.module.ts b/src/app/views/views.module.ts index 9cc983f8..39ccfbac 100644 --- a/src/app/views/views.module.ts +++ b/src/app/views/views.module.ts @@ -84,7 +84,7 @@ import { ModalHeaderComponent, ModalTitleDirective, ModalToggleDirective, - PlaceholderDirective, + PlaceholderDirective, PopoverDirective, ProgressBarComponent, ProgressComponent, RowComponent, @@ -101,6 +101,13 @@ import { SubmitQueryButtonComponent } from "./querying/console/components/submit-query-button/submit-query-button.component"; import {QueryEditor} from "./querying/console/components/code-editor/query-editor.component"; +import {ConfigSectionComponent} from "./querying/gis/components/config-section/config-section.component"; +import {AreaShapeComponent} from "./querying/gis/components/visualization/area-shape/area-shape.component"; +import {ColorComponent} from "./querying/gis/components/visualization/color/color.component"; +import {EmptyComponent} from "./querying/gis/components/visualization/empty/empty.component"; +import {LabelComponent} from "./querying/gis/components/visualization/label/label.component"; +import {PointShapeComponent} from "./querying/gis/components/visualization/point-shape/point-shape.component"; +import {NgxJsonViewerModule} from "ngx-json-viewer"; @NgModule({ @@ -170,7 +177,8 @@ import {QueryEditor} from "./querying/console/components/code-editor/query-edito ProgressComponent, ProgressBarComponent, CollapseDirective, - MapLayersComponent + NgxJsonViewerModule, + PopoverDirective, ], declarations: [ EditColumnsComponent, @@ -203,8 +211,19 @@ import {QueryEditor} from "./querying/console/components/code-editor/query-edito FileUploaderComponent, DockerconfigComponent, EditEntityComponent, + // GIS + MapLayersComponent, + ConfigSectionComponent, + AreaShapeComponent, + ColorComponent, + EmptyComponent, + LabelComponent, + PointShapeComponent ], - exports: [] + exports: [ + QueryEditor, + SubmitQueryButtonComponent + ] }) export class ViewsModule { } From 8dfce4f71ea3bda33f532901060492a98ce4e852 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Sat, 7 Dec 2024 16:49:36 +0100 Subject: [PATCH 18/60] Add noLimit to UiRequest --- .../containers/default-layout/default-layout.component.html | 2 +- src/app/models/ui-request.model.ts | 3 +++ .../querying/gis/components/layers/map-layers.component.ts | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/containers/default-layout/default-layout.component.html b/src/app/containers/default-layout/default-layout.component.html index 564ad5f0..0e3408a8 100644 --- a/src/app/containers/default-layout/default-layout.component.html +++ b/src/app/containers/default-layout/default-layout.component.html @@ -79,7 +79,7 @@
      - Map-based Query + Map-Based Query
    diff --git a/src/app/models/ui-request.model.ts b/src/app/models/ui-request.model.ts index 67586c55..56001655 100644 --- a/src/app/models/ui-request.model.ts +++ b/src/app/models/ui-request.model.ts @@ -69,6 +69,8 @@ export class QueryRequest extends UIRequest { language: string; namespace: string; cache: boolean; + // move to ui request + noLimit: boolean; constructor(query: string, analyze: boolean, cache: boolean, lang: string, namespace: string) { super(); @@ -78,6 +80,7 @@ export class QueryRequest extends UIRequest { this.language = lang; this.namespace = namespace; this.currentPage = 1; + this.noLimit = false; return this; } } diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index 3c9ecc3f..e9eb1d95 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -321,7 +321,9 @@ export class MapLayersComponent implements OnInit, AfterViewInit { switch (this.addLayerMode) { case LayerContext.Query: // TODO: queryLanguage, namespace - if (!this._crud.anyQuery(this.websocket, new QueryRequest(this.query, false, false, "MQL", "test"))) { + const request = new QueryRequest(this.query, false, false, 'MQL', 'doc'); + request.noLimit = true; + if (!this._crud.anyQuery(this.websocket, request)) { this.results.set([new RelationalResult('Could not establish a connection with the server.')]); console.log("Querry error") } else { From 09f45a3fe182c25d9a15896d9ea96d373128dc57 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Sat, 7 Dec 2024 17:49:46 +0100 Subject: [PATCH 19/60] Add "Add Layer From Query" --- package-lock.json | 8 + package.json | 1 + .../console/components/console-helper.ts | 6 +- .../submit-query-button.component.html | 6 +- .../submit-query-button.component.ts | 9 +- .../layers/map-layers.component.html | 57 +++-- .../layers/map-layers.component.scss | 1 + .../components/layers/map-layers.component.ts | 219 ++++++------------ .../querying/gis/models/MapLayer.model.ts | 4 + src/app/views/views.module.ts | 5 +- 10 files changed, 143 insertions(+), 173 deletions(-) diff --git a/package-lock.json b/package-lock.json index 025fca2f..ce50276f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,6 +84,7 @@ "@types/leaflet-draw": "^1.0.11", "@types/marked": "^4.3.0", "@types/node": "^12.11.1", + "@types/uuid": "^10.0.0", "codelyzer": "^6.0.2", "fstream": "^1.0.12", "html-webpack-plugin": "^5.5.3", @@ -8246,6 +8247,13 @@ "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", "optional": true }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", diff --git a/package.json b/package.json index 65829493..f9cf3f75 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@types/leaflet-draw": "^1.0.11", "@types/marked": "^4.3.0", "@types/node": "^12.11.1", + "@types/uuid": "^10.0.0", "codelyzer": "^6.0.2", "fstream": "^1.0.12", "html-webpack-plugin": "^5.5.3", diff --git a/src/app/views/querying/console/components/console-helper.ts b/src/app/views/querying/console/components/console-helper.ts index 89eb6b70..70a377a1 100644 --- a/src/app/views/querying/console/components/console-helper.ts +++ b/src/app/views/querying/console/components/console-helper.ts @@ -1,3 +1,7 @@ export function usesAdvancedConsole(lang: string) { return lang === 'mql' || lang === 'cypher'; -} \ No newline at end of file +} + +export const QUERY_LANGUAGES = Array.from( + ['SQL', 'MQL', 'CYPHER', 'CQL', 'PIG'] +); diff --git a/src/app/views/querying/console/components/submit-query-button/submit-query-button.component.html b/src/app/views/querying/console/components/submit-query-button/submit-query-button.component.html index 12718b95..df643749 100644 --- a/src/app/views/querying/console/components/submit-query-button/submit-query-button.component.html +++ b/src/app/views/querying/console/components/submit-query-button/submit-query-button.component.html @@ -1,11 +1,7 @@ - +
    diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.scss b/src/app/views/querying/gis/components/layers/map-layers.component.scss index 72fb6e64..41b211b8 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.scss +++ b/src/app/views/querying/gis/components/layers/map-layers.component.scss @@ -88,6 +88,7 @@ .add-layer-query { display: flex; + flex-direction: column; gap: 1rem; & > .query-controls { diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index e9eb1d95..80818675 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -1,67 +1,38 @@ import { - AfterViewInit, ChangeDetectorRef, - Component, effect, + AfterViewInit, Component, effect, ElementRef, HostListener, inject, OnInit, - Renderer2, signal, untracked, - WritableSignal + Renderer2, signal, ViewChild, WritableSignal } from '@angular/core'; import {LayerContext} from '../../models/LayerContext.model'; import {LayerSettingsService} from '../../services/layersettings.service'; -import { - ButtonCloseDirective, - ButtonDirective, - CardBodyComponent, - CardComponent, - CardHeaderComponent, - FormControlDirective, - FormLabelDirective, - FormSelectDirective, - InputGroupComponent, - InputGroupTextDirective, - ListGroupDirective, - ListGroupItemDirective, - ModalBodyComponent, - ModalComponent, - ModalFooterComponent, - ModalHeaderComponent, - ModalTitleDirective, - PopoverDirective, -} from '@coreui/angular'; import {MapGeometryWithData} from '../../models/RowResult.model'; import * as GeoJSON from 'geojson'; import {FeatureCollection} from 'geojson'; import {MapLayer} from '../../models/MapLayer.model'; -import {AsyncPipe, NgComponentOutlet, NgForOf, NgIf} from '@angular/common'; import isEqual from 'lodash/isEqual'; import { - CdkDrag, CdkDragDrop, - CdkDragHandle, - CdkDragPlaceholder, - CdkDropList, moveItemInArray, } from '@angular/cdk/drag-drop'; -import {FormsModule} from '@angular/forms'; -import {NgxJsonViewerModule} from 'ngx-json-viewer'; -import {ConfigSectionComponent} from '../config-section/config-section.component'; // noinspection ES6UnusedImports import {getSampleMapLayers} from '../../models/get-sample-maplayers'; -import {CrudService} from "../../../../../services/crud.service"; -import {WebSocket} from "../../../../../services/webSocket"; -import {SidebarNode} from "../../../../../models/sidebar-node.model"; -import {RelationalResult, Result} from "../../../../../components/data-view/models/result-set.model"; -import {InformationObject} from "../../../../../models/information-page.model"; -import {WebuiSettingsService} from "../../../../../services/webui-settings.service"; -import {Subscription} from "rxjs"; -import {EntityRequest, QueryRequest} from "../../../../../models/ui-request.model"; -import {CombinedResult} from "../../../../../components/data-view/data-view.model"; -import {CatalogService} from "../../../../../services/catalog.service"; -import {ViewsModule} from "../../../../views.module"; - -type BaseLayer = { name: string; value: string }; +import {CrudService} from '../../../../../services/crud.service'; +import {WebSocket} from '../../../../../services/webSocket'; +import {RelationalResult, Result} from '../../../../../components/data-view/models/result-set.model'; +import {WebuiSettingsService} from '../../../../../services/webui-settings.service'; +import {Subscription} from 'rxjs'; +import {EntityRequest, QueryRequest} from '../../../../../models/ui-request.model'; +import {CombinedResult} from '../../../../../components/data-view/data-view.model'; +import {CatalogService} from '../../../../../services/catalog.service'; +import {QueryEditor} from '../../../console/components/code-editor/query-editor.component'; + +interface BaseLayer { + name: string; + value: string; +} @Component({ selector: 'app-map-layers', @@ -70,16 +41,45 @@ type BaseLayer = { name: string; value: string }; // changeDetection: ChangeDetectionStrategy.OnPush, }) export class MapLayersComponent implements OnInit, AfterViewInit { + + constructor( + protected layerSettings: LayerSettingsService, + private el: ElementRef, + private renderer: Renderer2, + ) { + this.websocket = new WebSocket(); + this.initWebsocket(); + + effect(() => { + const res = this.results(); + console.log('res=', res); + if (res.length > 0) { + const combinedResult = CombinedResult.from(res[0]); + if (combinedResult.error) { + this.addLayerDialogErrorMessage = `There was an error executing the query. Error: ${combinedResult.error}`; + } else { + console.log('CombinedResult=', combinedResult); + this.addLayerInternal(MapLayer.from(combinedResult)); + localStorage.setItem(this.LOCAL_STORAGE_LAST_QUERY_KEY, combinedResult.query); + this.isAddLayerModalVisible = false; + } + } + }); + } + // DI private readonly _crud = inject(CrudService); private readonly _settings = inject(WebuiSettingsService); public readonly _catalog = inject(CatalogService); + @ViewChild(QueryEditor) queryEditor!: QueryEditor; + // Querying websocket: WebSocket; results: WritableSignal[]> = signal([]); readonly language: WritableSignal = signal('sql'); private subscriptions = new Subscription(); + private readonly LOCAL_STORAGE_LAST_QUERY_KEY = 'last_query_gis'; protected baseLayers: BaseLayer[] = [ { @@ -100,10 +100,10 @@ export class MapLayersComponent implements OnInit, AfterViewInit { protected renderedLayers: MapLayer[] = []; // Add Layer - protected query = "db.geoCollection2.find({})" protected isAddLayerModalVisible = false; + protected addLayerDialogErrorMessage = ''; protected loadedGeoJsonFile?: GeoJSON.FeatureCollection = undefined; - protected loadedGeoJsonFileName: string = ''; + protected loadedGeoJsonFileName = ''; protected anyLayersVisible = false; protected addLayerModes: LayerContext[] = [ // LayerContext.Results, @@ -113,65 +113,26 @@ export class MapLayersComponent implements OnInit, AfterViewInit { ]; protected addLayerMode: LayerContext = LayerContext.Query; - - protected datasetToUrl = new Map([ - ['Genealogy (100, data)', 'gedcom_coordinates_100_data.geojson'], - ['Genealogy (Full, no data)', 'gedcom_coordinates_full.geojson'], - ['Landkreise (D)', 'landkreise_simplify200.geojson'], - ['Basel Stadt Bevölkerung Quartiere', 'bs-stadt-bevoelkerung.geojson'], - ['Leeds Litter Bins', 'LitterBins20211201.geojson'], - ['Bern Urban Heat', 'bern-urban-heat.json'], - ]); - protected polyphenyDatasets = Array.from(this.datasetToUrl.keys()); - protected selectedPolyphenyDataset = ''; - // Correctly set height. private pollingTimer: any; - private pollingDuration: number = 3000; // 3 seconds - private pollInterval: number = 500; // Poll every 500ms - private lastHeight: string = ""; - - constructor( - protected layerSettings: LayerSettingsService, - private el: ElementRef, - private renderer: Renderer2, - ) { - this.websocket = new WebSocket(); - this.initWebsocket(); - - effect(() => { - const res = this.results(); - console.log("res=", res) - if (res.length > 0){ - const combinedResult = CombinedResult.from(res[0]) - console.log("CombinedResult=", combinedResult) - this.updateLayers([ MapLayer.from(combinedResult) ]) - - if (combinedResult.hasMore){ - // TODO: What is EntityID and why is it null? - const request = new EntityRequest(combinedResult.entityId, combinedResult.namespace, combinedResult.currentPage); - console.log("get more") - - if (!this._crud.getEntityData(this.websocket, request)) { - console.log("Error getEntityData") - // this.results.set(CombinedResult.fromRelational(new RelationalResult('Could not establish a connection with the server.'))); - } - } - - } - }); - } + private pollingDuration = 3000; // 3 seconds + private pollInterval = 500; // Poll every 500ms + private lastHeight = ''; + protected readonly Object = Object; + protected readonly LayerContext = LayerContext; + protected readonly queryLanguages = ['CYPHER', 'SQL', 'MQL']; + readonly activeNamespace: WritableSignal = signal(null); private initWebsocket() { const sub = this.websocket.onMessage().subscribe({ next: msg => { - console.log("websocket.msg=", msg) + console.log('websocket.msg=', msg); if (Array.isArray(msg) && ((msg[0].hasOwnProperty('data') || msg[0].hasOwnProperty('affectedTuples') || msg[0].hasOwnProperty('error')))) { // array of ResultSet this.results.set([]>msg); } }, error: err => { - console.log("websocket.err=", err) + console.log('websocket.err=', err); //this._leftSidebar.setError('Lost connection with the server.'); setTimeout(() => { this.initWebsocket(); @@ -183,14 +144,16 @@ export class MapLayersComponent implements OnInit, AfterViewInit { ngAfterViewInit(): void { this.startPollingHeight(); + const lastQuery = localStorage.getItem(this.LOCAL_STORAGE_LAST_QUERY_KEY); + if (lastQuery) { + console.log(this.queryEditor); + this.queryEditor.setCode(lastQuery); + } } ngOnInit(): void { - console.log("map-layers.component.ts ngOnInit(). Layers=", this.layers) - this.layerSettings.layers$.subscribe((layers) => { this.layers = layers; - console.log("map-layers.component layerSettings.subscribe: ", this.layers) this.renderedLayers = this.deepCopyLayers(layers); this.layerSettings.setCanRerenderLayers(false); this.updateLayerUi(); @@ -205,7 +168,6 @@ export class MapLayersComponent implements OnInit, AfterViewInit { this.deepCopyLayers(this.layers), this.renderedLayers, ); - console.log('Config changed. Rerender layers?', canRerenderLayers); this.layerSettings.setCanRerenderLayers(canRerenderLayers); }); @@ -216,8 +178,8 @@ export class MapLayersComponent implements OnInit, AfterViewInit { // this.updateLayers(getSampleMapLayers()); } - trackByLayerName(index: number, layer: MapLayer): string { - return layer.name; + trackByLayerUuid(index: number, layer: MapLayer): string { + return layer.uuid; } @HostListener('window:resize') @@ -231,7 +193,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit { const endTime = Date.now() + this.pollingDuration; this.pollingTimer = setInterval(() => { const currentHeight = this.getMapHeight(); - if (currentHeight != this.lastHeight) { + if (currentHeight !== this.lastHeight) { this.setMapHeight(currentHeight); } else { if (Date.now() > endTime) { @@ -242,7 +204,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit { } private getMapHeight(): string { - return `${(document.querySelector("#map") as HTMLElement).offsetHeight}px` + return `${(document.querySelector('#map') as HTMLElement).offsetHeight}px`; } private setMapHeight(mapHeight: string): void { @@ -285,16 +247,6 @@ export class MapLayersComponent implements OnInit, AfterViewInit { this.layerSettings.setBaseLayer(selectedLayer.value); } - // onLayerVisualizationChange( - // selectedLayer: MapLayer, - // selectedVisualization: Visualization, - // ) { - // selectedLayer.updateConfigInjector(); - // this.layerSettings.visualizationConfigurationChanged( - // selectedLayer.visualization, - // ); - // } - addLayerInternal(layer: MapLayer) { const newLayers = [ layer, @@ -318,38 +270,20 @@ export class MapLayersComponent implements OnInit, AfterViewInit { } async addLayer() { + this.addLayerDialogErrorMessage = ''; + switch (this.addLayerMode) { case LayerContext.Query: - // TODO: queryLanguage, namespace - const request = new QueryRequest(this.query, false, false, 'MQL', 'doc'); + const request = new QueryRequest(this.queryEditor.getCode(), false, false, this.language(), this.activeNamespace()); request.noLimit = true; if (!this._crud.anyQuery(this.websocket, request)) { - this.results.set([new RelationalResult('Could not establish a connection with the server.')]); - console.log("Querry error") - } else { - console.log("Querry success") + this.addLayerDialogErrorMessage = 'There was an error executing this query.'; } - break + // Dialog will be hidden when result has arrived in constructor.efffect + break; case LayerContext.Results: case LayerContext.DB: - if (!this.selectedPolyphenyDataset) { - break; - } - const url = `assets/${this.datasetToUrl.get(this.selectedPolyphenyDataset)}`; - const geojson = await this.fetchGeoJsonFile(url); - const layer = new MapLayer( - this.selectedPolyphenyDataset, - ).addData( - geojson.features.map( - (f, i) => - new MapGeometryWithData( - i, - f.geometry, - f.properties ? f.properties : {}, - ), - ), - ); - this.addLayerInternal(layer); + alert('TODO'); break; case LayerContext.External: if (this.loadedGeoJsonFile) { @@ -366,13 +300,13 @@ export class MapLayersComponent implements OnInit, AfterViewInit { ), ); console.log('Added GeoJSON layer: ', layer); + this.isAddLayerModalVisible = false; this.addLayerInternal(layer); } else { - alert(`No file selected / File could not be loaded.`); + this.addLayerDialogErrorMessage = 'No file selected / File could not be loaded.'; } break; } - this.isAddLayerModalVisible = false; } dropLayer(event: CdkDragDrop) { @@ -409,9 +343,4 @@ export class MapLayersComponent implements OnInit, AfterViewInit { this.anyLayersVisible = this.layers.filter((d) => !d.isRemoved).length > 0; } - - protected readonly Object = Object; - protected readonly LayerContext = LayerContext; - - } diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index da6f0387..ea505fb8 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -7,13 +7,16 @@ import {PointShapeVisualization} from '../components/visualization/point-shape-v import {CombinedResult} from '../../../../components/data-view/data-view.model'; import {DataModel} from '../../../../models/ui-request.model'; import {Geometry} from 'geojson'; +import {v4} from 'uuid'; export class MapLayer { constructor(name: string) { this.name = name; + this.uuid = v4(); } + uuid: string; name: string; data: MapGeometryWithData[] = []; @@ -152,6 +155,7 @@ export class MapLayer { const copy = new MapLayer(this.name).addData( this.data.map((d) => d.copy()), ); + copy.uuid = this.uuid; copy.pointShapeVisualization = this.pointShapeVisualization.copy(); copy.areaShapeVisualization = this.areaShapeVisualization.copy(); copy.colorVisualization = this.colorVisualization.copy(); diff --git a/src/app/views/views.module.ts b/src/app/views/views.module.ts index 39ccfbac..2dbeb120 100644 --- a/src/app/views/views.module.ts +++ b/src/app/views/views.module.ts @@ -46,6 +46,7 @@ import {GraphEditGraphComponent} from './schema-editing/graph-edit-graph/graph-e import {FileUploaderComponent} from './forms/form-generator/file-uploader/file-uploader.component'; import {DockerconfigComponent} from './dockerconfig/dockerconfig.component'; import { + AlertComponent, BadgeComponent, BorderDirective, ButtonCloseDirective, @@ -69,7 +70,7 @@ import { FormCheckLabelDirective, FormControlDirective, FormDirective, - FormFeedbackComponent, + FormFeedbackComponent, FormLabelDirective, FormSelectDirective, FormTextDirective, GutterDirective, @@ -179,6 +180,8 @@ import {NgxJsonViewerModule} from "ngx-json-viewer"; CollapseDirective, NgxJsonViewerModule, PopoverDirective, + FormLabelDirective, + AlertComponent, ], declarations: [ EditColumnsComponent, From daf3becfd53c15597a5eb09487fa00933c0ffd74 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Sun, 8 Dec 2024 23:26:45 +0100 Subject: [PATCH 20/60] Fix submitQuery() in console --- .../querying/console/console.component.ts | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/app/views/querying/console/console.component.ts b/src/app/views/querying/console/console.component.ts index 14fa637f..cd659d97 100644 --- a/src/app/views/querying/console/console.component.ts +++ b/src/app/views/querying/console/console.component.ts @@ -28,8 +28,8 @@ import {ToasterService} from '../../../components/toast-exposer/toaster.service' import {ViewInformation} from '../../../components/data-view/data-view.component'; import {CatalogService} from '../../../services/catalog.service'; import {NamespaceModel} from '../../../models/catalog.model'; -import {usesAdvancedConsole} from "./components/console-helper"; -import {QueryEditor} from "./components/code-editor/query-editor.component"; +import {usesAdvancedConsole} from './components/console-helper'; +import {QueryEditor} from './components/code-editor/query-editor.component'; @Component({ selector: 'app-console', @@ -64,14 +64,13 @@ export class ConsoleComponent implements OnInit, OnDestroy { private subscriptions = new Subscription(); readonly loading: WritableSignal = signal(false); readonly language: WritableSignal = signal('sql'); - readonly query: WritableSignal = signal(''); saveInHistory = true; showSearch = false; historySearchQuery = ''; confirmDeletingHistory; - delayedNamespace: string = null + delayedNamespace: string = null; entityConfig: EntityConfig = { create: false, @@ -89,7 +88,6 @@ export class ConsoleComponent implements OnInit, OnDestroy { if (window.Cypress) { (window).executeQuery = (query: string) => { this.queryEditor.setCode(query); - this.query.set(query) this.submitQuery(); }; } @@ -102,7 +100,7 @@ export class ConsoleComponent implements OnInit, OnDestroy { untracked(() => { this.collapsed = new Array(res.length); this.collapsed.fill(false); - }) + }); }); } @@ -123,7 +121,7 @@ export class ConsoleComponent implements OnInit, OnDestroy { submitQuery() { this.delayedNamespace = null; - const code = this.query() + const code = this.queryEditor.getCode(); if (!code) { return; } @@ -131,15 +129,15 @@ export class ConsoleComponent implements OnInit, OnDestroy { this.addToHistory(code, this.language()); } if (usesAdvancedConsole(this.language())) { - code.split(";").forEach((query: string) => { + code.split(';').forEach((query: string) => { // maybe adjust - const graphUse = /use *graph *([a-zA-Z][a-zA-Z0-9-_]*)/gmi + const graphUse = /use *graph *([a-zA-Z][a-zA-Z0-9-_]*)/gmi; const matchGraph = graphUse.exec(query.trim()); if (matchGraph !== null && matchGraph.length > 1) { this.delayedNamespace = matchGraph[1]; } - const useRegex = /use ([a-zA-Z][a-zA-Z0-9-_]*)/gmi + const useRegex = /use ([a-zA-Z][a-zA-Z0-9-_]*)/gmi; const match = useRegex.exec(query.trim()); if (match !== null && match.length > 1) { const namespace = match[1]; @@ -147,7 +145,7 @@ export class ConsoleComponent implements OnInit, OnDestroy { this.delayedNamespace = namespace; } } - }) + }); if (code.match('show db')) { this._catalog.updateIfNecessary().subscribe(catalog => { @@ -195,7 +193,6 @@ export class ConsoleComponent implements OnInit, OnDestroy { applyHistory(query: string, lang: string, run: boolean) { this.language.set(lang); this.queryEditor.setCode(query); - this.query.set(query) if (run) { this.submitQuery(); } @@ -294,7 +291,7 @@ export class ConsoleComponent implements OnInit, OnDestroy { } else if (Array.isArray(msg) && ((msg[0].hasOwnProperty('data') || msg[0].hasOwnProperty('affectedTuples') || msg[0].hasOwnProperty('error')))) { // array of ResultSets if (this.delayedNamespace && !msg[0].hasOwnProperty('error')) { this.activeNamespace.set(this.delayedNamespace); - this.queryEditor.storeNamespace(this.delayedNamespace) + this.queryEditor.storeNamespace(this.delayedNamespace); } this.delayedNamespace = null; @@ -326,7 +323,7 @@ export class ConsoleComponent implements OnInit, OnDestroy { } toggleCollapsed(i: number) { - console.log(i) + console.log(i); if (this.collapsed !== undefined && this.collapsed[i] !== undefined) { this.collapsed[i] = !this.collapsed[i]; } From 98a4e5e8a1493a4c5e67d1b7b3ff2367852e0395 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Sun, 8 Dec 2024 23:27:03 +0100 Subject: [PATCH 21/60] map-layers cleanup + layer name from query --- .../data-view/data-map/data-map.component.ts | 43 ++++++++----------- .../components/layers/map-layers.component.ts | 10 +---- .../querying/gis/models/MapLayer.model.ts | 2 +- 3 files changed, 22 insertions(+), 33 deletions(-) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index c0b7df20..cab05778 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -5,10 +5,12 @@ import {GeoPath, GeoPermissibleObjects} from 'd3'; import * as d3Geo from 'd3-geo'; import * as L from 'leaflet'; import 'leaflet-draw'; -import {LayerSettingsService} from "../../../views/querying/gis/services/layersettings.service"; -import {MapLayer} from "../../../views/querying/gis/models/MapLayer.model"; -import {MapGeometryWithData} from "../../../views/querying/gis/models/RowResult.model"; -import {CombinedResult} from "../data-view.model"; +import {LayerSettingsService} from '../../../views/querying/gis/services/layersettings.service'; +import {MapLayer} from '../../../views/querying/gis/models/MapLayer.model'; +import {MapGeometryWithData} from '../../../views/querying/gis/models/RowResult.model'; +import {CombinedResult} from '../data-view.model'; + +// tslint:disable:no-non-null-assertion @Component({ selector: 'app-data-map', @@ -17,14 +19,14 @@ import {CombinedResult} from "../data-view.model"; }) export class DataMapComponent extends DataTemplateComponent implements AfterViewInit, OnDestroy { // If the map is shown inside the results section, different styling needs to be applied. - @Input() isInsideResults: boolean = false; + @Input() isInsideResults = false; currentBaseLayer: L.TileLayer | undefined; layers: MapLayer[] = []; resultLayer?: MapLayer = undefined; - isLoading: boolean = false; - isLoadingMessage: string = 'TODO isLoadingMessage'; - canRerenderLayers: boolean = false; + isLoading = false; + isLoadingMessage = 'TODO isLoadingMessage'; + canRerenderLayers = false; readonly MIN_ZOOM = 0; readonly MAX_ZOOM = 19; @@ -63,9 +65,6 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView } ngOnInit() { - // Reset data on map - console.log("data-map.component.ts ngOnInit(). Layers=", this.layers) - this.layerSettings.selectedBaseLayer$.subscribe((item) => { if (!item) { return; @@ -75,7 +74,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView this.map.removeLayer(this.currentBaseLayer); } - if (item != 'EMPTY') { + if (item !== 'EMPTY') { this.currentBaseLayer = L.tileLayer(item, { maxZoom: this.MAX_ZOOM, attribution: @@ -85,7 +84,6 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView }); this.layerSettings.layers$.subscribe((layers) => { - console.log("data-map.component.ts layerSettings.layers$.subscribe(). Layers=", layers) this.layers = layers; this.renderLayersWithD3(); }); @@ -129,7 +127,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView // TODO: What is the best way to use this shape to add a filter to another layer? // - We could add this shape as its own layer. - const layer = event.layer + const layer = event.layer; drawnItems.addLayer(layer); }); @@ -256,7 +254,6 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView // Add shapes from each layer to array for (const layer of this.layers.slice().reverse()) { - console.log(`Render layer [${layer.name}]. Initialize...`); // Initialize all configs layer.pointShapeVisualization.init(layer.data); @@ -338,8 +335,8 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView }) .attr('cx', (d) => d.cache['x']) .attr('cy', (d) => d.cache['y']) - .style("pointer-events", "auto") - .style("cursor", "pointer") + .style('pointer-events', 'auto') + .style('cursor', 'pointer') .on('mouseover', function (event, d) { tt.style('display', 'block').html(JSON.stringify(d.data, null, 1)); }) @@ -359,7 +356,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView return; } - const tt = this.tooltip + const tt = this.tooltip; return this.g .selectAll('.paths') @@ -387,10 +384,9 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView d, ), ) - .style("pointer-events", "auto") - .style("cursor", "pointer") + .style('pointer-events', 'auto') + .style('cursor', 'pointer') .on('mouseover', function (event, d) { - console.log("hover") tt.style('display', 'block').html(JSON.stringify(d.data, null, 2)); }) .on('mousemove', function (event) { @@ -432,9 +428,8 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView navigateToMapQueryMode() { // TODO: The layer that was created from the results only contains the first 10 rows. We somehow need to // also need to give the map view a way to load the rest of the results. - console.log("Map layers before navigation", this.layers) - this.layerSettings.setLayers(this.layers) - this._router.navigate(['/views/querying/gis']) + this.layerSettings.setLayers(this.layers); + this._router.navigate(['/views/querying/gis']); } } diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index 80818675..43e2b0d9 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -21,10 +21,10 @@ import { import {getSampleMapLayers} from '../../models/get-sample-maplayers'; import {CrudService} from '../../../../../services/crud.service'; import {WebSocket} from '../../../../../services/webSocket'; -import {RelationalResult, Result} from '../../../../../components/data-view/models/result-set.model'; +import {Result} from '../../../../../components/data-view/models/result-set.model'; import {WebuiSettingsService} from '../../../../../services/webui-settings.service'; import {Subscription} from 'rxjs'; -import {EntityRequest, QueryRequest} from '../../../../../models/ui-request.model'; +import {QueryRequest} from '../../../../../models/ui-request.model'; import {CombinedResult} from '../../../../../components/data-view/data-view.model'; import {CatalogService} from '../../../../../services/catalog.service'; import {QueryEditor} from '../../../console/components/code-editor/query-editor.component'; @@ -38,7 +38,6 @@ interface BaseLayer { selector: 'app-map-layers', templateUrl: './map-layers.component.html', styleUrl: './map-layers.component.scss', - // changeDetection: ChangeDetectionStrategy.OnPush, }) export class MapLayersComponent implements OnInit, AfterViewInit { @@ -52,13 +51,11 @@ export class MapLayersComponent implements OnInit, AfterViewInit { effect(() => { const res = this.results(); - console.log('res=', res); if (res.length > 0) { const combinedResult = CombinedResult.from(res[0]); if (combinedResult.error) { this.addLayerDialogErrorMessage = `There was an error executing the query. Error: ${combinedResult.error}`; } else { - console.log('CombinedResult=', combinedResult); this.addLayerInternal(MapLayer.from(combinedResult)); localStorage.setItem(this.LOCAL_STORAGE_LAST_QUERY_KEY, combinedResult.query); this.isAddLayerModalVisible = false; @@ -126,13 +123,11 @@ export class MapLayersComponent implements OnInit, AfterViewInit { private initWebsocket() { const sub = this.websocket.onMessage().subscribe({ next: msg => { - console.log('websocket.msg=', msg); if (Array.isArray(msg) && ((msg[0].hasOwnProperty('data') || msg[0].hasOwnProperty('affectedTuples') || msg[0].hasOwnProperty('error')))) { // array of ResultSet this.results.set([]>msg); } }, error: err => { - console.log('websocket.err=', err); //this._leftSidebar.setError('Lost connection with the server.'); setTimeout(() => { this.initWebsocket(); @@ -146,7 +141,6 @@ export class MapLayersComponent implements OnInit, AfterViewInit { this.startPollingHeight(); const lastQuery = localStorage.getItem(this.LOCAL_STORAGE_LAST_QUERY_KEY); if (lastQuery) { - console.log(this.queryEditor); this.queryEditor.setCode(lastQuery); } } diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index ea505fb8..895276f2 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -32,7 +32,7 @@ export class MapLayer { static from(result: CombinedResult): MapLayer { console.log(result); - const layer = new MapLayer('Query'); + const layer = new MapLayer(result.query); const mapData = []; switch (result.dataModel) { From 1a6e9b681c19bbfcb6a7b8f573c74ebb1afac708 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Mon, 9 Dec 2024 00:32:33 +0100 Subject: [PATCH 22/60] Map-based query: Fix lifecycle issues (unsubscribe from service, run query from results on button press) --- .../data-view/data-map/data-map.component.ts | 41 +++++---- .../layers/map-layers.component.html | 8 +- .../components/layers/map-layers.component.ts | 91 +++++++++++++------ src/app/views/querying/gis/gis.component.ts | 5 +- .../querying/gis/models/MapLayer.model.ts | 27 +++++- .../gis/services/layersettings.service.ts | 7 ++ 6 files changed, 123 insertions(+), 56 deletions(-) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index cab05778..295c73f3 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -23,10 +23,10 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView currentBaseLayer: L.TileLayer | undefined; layers: MapLayer[] = []; - resultLayer?: MapLayer = undefined; isLoading = false; isLoadingMessage = 'TODO isLoadingMessage'; canRerenderLayers = false; + previewResult : CombinedResult = null; readonly MIN_ZOOM = 0; readonly MAX_ZOOM = 19; @@ -49,23 +49,25 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView super(); effect(() => { - // This code is only called when the map is shown inside the results. + if (!this.isInsideResults) { + return + } + + // Display the results. const result = this.$result(); if (!result) { return; } - // I could send the query string to the layers settings, and execute it there. - - // this.resultLayer = MapLayer.from(result); - - // TODO: If there already are results in the map query view, - // this.layerSettings.setLayers([MapLayer.from(result), ...this.layers]) + // The CombinedResult will be given to the LayerSettings, if the user clicks on the button to show the + // results in the full GIS query mode. + this.previewResult = result; + this.layerSettings.setLayers([MapLayer.from(result)]); }); } ngOnInit() { - this.layerSettings.selectedBaseLayer$.subscribe((item) => { + this.subscriptions.add(this.layerSettings.selectedBaseLayer$.subscribe((item) => { if (!item) { return; } @@ -81,25 +83,25 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView '© OpenStreetMap contributors', }).addTo(this.map); } - }); + })); - this.layerSettings.layers$.subscribe((layers) => { + this.subscriptions.add(this.layerSettings.layers$.subscribe((layers) => { this.layers = layers; this.renderLayersWithD3(); - }); + })); - this.layerSettings.canRerenderLayers$.subscribe((canRerenderLayers) => { + this.subscriptions.add(this.layerSettings.canRerenderLayers$.subscribe((canRerenderLayers) => { this.canRerenderLayers = canRerenderLayers; - }); + })); - this.layerSettings.toggleLayerVisibility$.subscribe((layer) => { + this.subscriptions.add(this.layerSettings.toggleLayerVisibility$.subscribe((layer) => { this.toggleLayerVisibility(layer); - }); + })); } ngOnDestroy() { // TODO: This breaks the navigation from results to map query view - // this.layerSettings.setLayers([]) + this.subscriptions.unsubscribe(); } ngAfterViewInit(): void { @@ -426,9 +428,8 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView } navigateToMapQueryMode() { - // TODO: The layer that was created from the results only contains the first 10 rows. We somehow need to - // also need to give the map view a way to load the rest of the results. - this.layerSettings.setLayers(this.layers); + // Give CombinedResult to map-layers component, so that the full query can be run and added from there. + this.layerSettings.setResultsQuery(this.previewResult); this._router.navigate(['/views/querying/gis']); } } diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.html b/src/app/views/querying/gis/components/layers/map-layers.component.html index 2e919a3d..1481eefc 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.html +++ b/src/app/views/querying/gis/components/layers/map-layers.component.html @@ -65,9 +65,11 @@

    class="preview-data">({{ layer.data.length }})

    - - @@ -161,7 +163,7 @@
    Add layer
    Error
    - {{addLayerDialogErrorMessage}} + {{ addLayerDialogErrorMessage }}
    diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index 43e2b0d9..98066186 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -2,7 +2,7 @@ import { AfterViewInit, Component, effect, ElementRef, HostListener, - inject, + inject, OnDestroy, OnInit, Renderer2, signal, ViewChild, WritableSignal } from '@angular/core'; @@ -39,7 +39,7 @@ interface BaseLayer { templateUrl: './map-layers.component.html', styleUrl: './map-layers.component.scss', }) -export class MapLayersComponent implements OnInit, AfterViewInit { +export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { constructor( protected layerSettings: LayerSettingsService, @@ -49,6 +49,9 @@ export class MapLayersComponent implements OnInit, AfterViewInit { this.websocket = new WebSocket(); this.initWebsocket(); + // Starting a new GIS query session: Remove all layers which were previously added and not cleaned up. + this.updateLayers([]) + effect(() => { const res = this.results(); if (res.length > 0) { @@ -64,6 +67,14 @@ export class MapLayersComponent implements OnInit, AfterViewInit { }); } + ngOnDestroy(): void { + // When we navigate away from the query mode, we remove all the layers from the layer settings as well + // as from the map. Otherwise, they show up when we don't want them, e.g. in the results view. + this.updateLayers([]); + this.subscriptions.unsubscribe(); + clearInterval(this.pollingTimer); + } + // DI private readonly _crud = inject(CrudService); private readonly _settings = inject(WebuiSettingsService); @@ -112,8 +123,6 @@ export class MapLayersComponent implements OnInit, AfterViewInit { // Correctly set height. private pollingTimer: any; - private pollingDuration = 3000; // 3 seconds - private pollInterval = 500; // Poll every 500ms private lastHeight = ''; protected readonly Object = Object; protected readonly LayerContext = LayerContext; @@ -146,28 +155,37 @@ export class MapLayersComponent implements OnInit, AfterViewInit { } ngOnInit(): void { - this.layerSettings.layers$.subscribe((layers) => { + this.subscriptions.add(this.layerSettings.layers$.subscribe((layers) => { this.layers = layers; - this.renderedLayers = this.deepCopyLayers(layers); + this.renderedLayers = this.deepCopyLayers(layers, false); this.layerSettings.setCanRerenderLayers(false); this.updateLayerUi(); - }); + })) - this.layerSettings.modifiedVisualization$.subscribe((config) => { + this.subscriptions.add(this.layerSettings.modifiedVisualization$.subscribe((config) => { if (!config) { return; } const canRerenderLayers = !isEqual( - this.deepCopyLayers(this.layers), + this.deepCopyLayers(this.layers, false), this.renderedLayers, ); this.layerSettings.setCanRerenderLayers(canRerenderLayers); - }); + })); - this.layerSettings.rerenderButtonClicked$.subscribe(() => { + this.subscriptions.add(this.layerSettings.rerenderButtonClicked$.subscribe(() => { this.updateLayers(this.layers); - }); + })); + + this.subscriptions.add(this.layerSettings.queryFromConsoleResults$.subscribe((query) => { + if (query){ + console.log("Run full query from results", query); + this.submitQuery(query.query, query.language.toString(), query.namespace); + // Remove it, so that if we navigate away and back again, we won't run the query twice. + this.layerSettings.setResultsQuery(null); + } + })); // this.updateLayers(getSampleMapLayers()); } @@ -184,21 +202,35 @@ export class MapLayersComponent implements OnInit, AfterViewInit { private startPollingHeight(): void { // A bit dirty, but it works for now. For some reason, a second after the map is created, the size changes. // We just poll for the first few seconds after the component is created, and update the size if it changes. - const endTime = Date.now() + this.pollingDuration; + const endTime = Date.now() + 3000; this.pollingTimer = setInterval(() => { const currentHeight = this.getMapHeight(); + + if (currentHeight === undefined){ + return + } + if (currentHeight !== this.lastHeight) { this.setMapHeight(currentHeight); - } else { - if (Date.now() > endTime) { - clearInterval(this.pollingTimer); - } } - }, this.pollInterval); + // TODO: Somehow, sometimes, the onResize event does not capture all changes in the viewport, e.g. + // when interacting with windows snapping or doing other fast stuff. For now, just always poll while + // the component is active. + // else { + // if (Date.now() > endTime) { + // clearInterval(this.pollingTimer); + // } + // } + }, 500); } - private getMapHeight(): string { - return `${(document.querySelector('#map') as HTMLElement).offsetHeight}px`; + private getMapHeight(): string | undefined { + const elem = (document.querySelector('#map') as HTMLElement) + if (elem === null){ + return undefined + } else { + return `${elem.offsetHeight}px` + } } private setMapHeight(mapHeight: string): void { @@ -206,8 +238,10 @@ export class MapLayersComponent implements OnInit, AfterViewInit { this.renderer.setStyle(this.el.nativeElement, 'height', mapHeight); } - deepCopyLayers(layers: MapLayer[]) { - return layers.map((layer) => layer.copy()); + deepCopyLayers(layers: MapLayer[], includeData = true) { + // We only case if the configuration has changed. Copying the data over every time something changes would + // cause big performance problems. + return layers.map((layer) => layer.copy(includeData)); } async loadGeoJsonFile($event: Event) { @@ -263,17 +297,22 @@ export class MapLayersComponent implements OnInit, AfterViewInit { return geojson; } + submitQuery(query: string, language: string, namespace: string) : boolean { + const request = new QueryRequest(query, false, false, language, namespace); + request.noLimit = true; + return this._crud.anyQuery(this.websocket, request) + } + async addLayer() { this.addLayerDialogErrorMessage = ''; switch (this.addLayerMode) { case LayerContext.Query: - const request = new QueryRequest(this.queryEditor.getCode(), false, false, this.language(), this.activeNamespace()); - request.noLimit = true; - if (!this._crud.anyQuery(this.websocket, request)) { + if (!this.submitQuery(this.queryEditor.getCode(), this.language(), this.activeNamespace())){ this.addLayerDialogErrorMessage = 'There was an error executing this query.'; } - // Dialog will be hidden when result has arrived in constructor.efffect + // Dialog will be hidden when result has arrived in constructor.effect, because it is possible to + // get the error in the result, and not directly here. break; case LayerContext.Results: case LayerContext.DB: diff --git a/src/app/views/querying/gis/gis.component.ts b/src/app/views/querying/gis/gis.component.ts index 732d0318..88243ace 100644 --- a/src/app/views/querying/gis/gis.component.ts +++ b/src/app/views/querying/gis/gis.component.ts @@ -28,9 +28,10 @@ export class GisComponent implements OnInit, OnDestroy { private readonly _sidebar = inject(LeftSidebarService); ngOnDestroy(): void { - console.log("GisComponent.ngOnDestroy") + } + ngOnInit(): void { - console.log("GisComponent.ngOnInit") + this._sidebar.hide(); } } diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index 895276f2..444f96ad 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -19,6 +19,8 @@ export class MapLayer { uuid: string; name: string; data: MapGeometryWithData[] = []; + containsPoints = false; + containsAreas = false; pointShapeVisualization: Visualization = new PointShapeVisualization(3); areaShapeVisualization: Visualization = new AreaShapeVisualization(1); @@ -148,13 +150,17 @@ export class MapLayer { return undefined; } - copy() { + copy(includeData = true) { // Do not copy isActive and isRemoved, because we use the copy to check if // anything changes so we need to rerender, but in these cases we do not need // to rerender. - const copy = new MapLayer(this.name).addData( - this.data.map((d) => d.copy()), - ); + + const copy = new MapLayer(this.name) + if (includeData){ + copy.addData( + this.data.map((d) => d.copy()), + ); + } copy.uuid = this.uuid; copy.pointShapeVisualization = this.pointShapeVisualization.copy(); copy.areaShapeVisualization = this.areaShapeVisualization.copy(); @@ -164,7 +170,18 @@ export class MapLayer { } addData(data: MapGeometryWithData[]) { - data.forEach((d) => (d.layer = this)); + this.containsPoints = false; + this.containsAreas = false; + + data.forEach((d) => { + if (d.isPoint()){ + this.containsPoints = true; + } else { + this.containsAreas = true; + } + d.layer = this + return; + }); this.data.push(...data); return this; } diff --git a/src/app/views/querying/gis/services/layersettings.service.ts b/src/app/views/querying/gis/services/layersettings.service.ts index 5ec9328b..e412293a 100644 --- a/src/app/views/querying/gis/services/layersettings.service.ts +++ b/src/app/views/querying/gis/services/layersettings.service.ts @@ -2,11 +2,15 @@ import { Injectable } from '@angular/core'; import {BehaviorSubject, Subject} from 'rxjs'; import { MapLayer } from '../models/MapLayer.model'; import {Visualization} from '../models/visualization.interface'; +import {CombinedResult} from "../../../../components/data-view/data-view.model"; @Injectable({ providedIn: 'root', }) export class LayerSettingsService { + private queryFromConsoleResults = new BehaviorSubject(null); + queryFromConsoleResults$ = this.queryFromConsoleResults.asObservable(); + private selectedBaseLayer = new BehaviorSubject('EMPTY'); selectedBaseLayer$ = this.selectedBaseLayer.asObservable(); @@ -24,6 +28,9 @@ export class LayerSettingsService { private toggleLayerVisibilitySubject = new Subject(); toggleLayerVisibility$ = this.toggleLayerVisibilitySubject.asObservable(); + setResultsQuery(result: CombinedResult){ + this.queryFromConsoleResults.next(result); + } setBaseLayer(item: string) { this.selectedBaseLayer.next(item); } From 5dabcbd3ca2f002b3572b96fbab66cfaa5625c37 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Mon, 9 Dec 2024 01:13:21 +0100 Subject: [PATCH 23/60] Detect GEOMETRY columns from relational CombinedResult --- .../data-view/data-map/data-map.component.ts | 4 +- .../querying/gis/models/MapLayer.model.ts | 61 ++++++++++++++++--- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index 295c73f3..73346bc5 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -240,6 +240,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView } renderLayersWithD3() { + console.log("Map: Render layers...") this.showLoadingSpinner('Rendering layers'); setTimeout(() => { @@ -276,10 +277,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView // Render all points // TODO: Circles are always on the bottom this way... - console.log('Create Points: ', points); this.circles = this.createPoints(points); - - console.log('Create Paths: ', paths); this.paths = this.createPaths(paths); // Set SVG position correctly diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index 444f96ad..7c161ebf 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -33,7 +33,7 @@ export class MapLayer { index = -1; static from(result: CombinedResult): MapLayer { - console.log(result); + console.log("MapLayer from result: ", result); const layer = new MapLayer(result.query); const mapData = []; @@ -41,7 +41,7 @@ export class MapLayer { case DataModel.DOCUMENT: for (let rowIndex = 0; rowIndex < result.data.length; rowIndex++) { const json = result.data[rowIndex][0]; - const jsonObject: Record = Object.fromEntries( + const jsonObject: Map = new Map( Object.entries(JSON.parse(json)).map(([key, value]) => [key.toLowerCase(), value]) ); const geometry = this.getGeometryFromData(jsonObject); @@ -54,12 +54,14 @@ export class MapLayer { case DataModel.RELATIONAL: for (let rowIndex = 0; rowIndex < result.data.length; rowIndex++) { const map = new Map(); + for (let headerIndex = 0; headerIndex < result.header.length; headerIndex++) { - // TODO: Geometry objects const header = result.header[headerIndex]; const key = header.name.toLowerCase(); const value = result.data[rowIndex][headerIndex]; - if (header.dataType.startsWith('INTEGER')) { + if (header.dataType.startsWith('GEOMETRY')) { + map.set(key, JSON.parse(value)); + } else if (header.dataType.startsWith('INTEGER')) { map.set(key, parseInt(value, 10)); } else if (header.dataType.startsWith('DECIMAL')) { map.set(key, parseFloat(value)); @@ -67,8 +69,8 @@ export class MapLayer { map.set(key, value); } } + const geometry = this.getGeometryFromData(map); - console.log(geometry); if (geometry) { const geometryWithData = new MapGeometryWithData(rowIndex, geometry, map); mapData.push(geometryWithData); @@ -113,10 +115,12 @@ export class MapLayer { return layer; } - static getGeometryFromData(data: Record): Geometry | undefined { - // GeoJSON object - if ('geometry' in data) { - return data['geometry']; + static getGeometryFromData(data: Map): Geometry | undefined { + // Detect GeoJSON objects + for (const value of data.values()) { + if (this.isGeoJSON(value)){ + return value; + } } // Detect 2 columns that store latitude / longitude coordinates @@ -150,6 +154,45 @@ export class MapLayer { return undefined; } + static isGeoJSON(obj: any): boolean { + if (!obj || typeof obj !== 'object') return false; + + const validTypes: string[] = [ + 'Feature', + 'FeatureCollection', + 'GeometryCollection', + 'Point', + 'MultiPoint', + 'LineString', + 'MultiLineString', + 'Polygon', + 'MultiPolygon', + ]; + + if (!obj.type || !validTypes.includes(obj.type)) { + return false; + } + + switch (obj.type) { + case 'Feature': + return obj.hasOwnProperty('geometry') && obj.hasOwnProperty('properties'); + case 'FeatureCollection': + return Array.isArray(obj.features); + case 'GeometryCollection': + return Array.isArray(obj.geometries); + case 'Point': + case 'MultiPoint': + case 'LineString': + case 'MultiLineString': + case 'Polygon': + case 'MultiPolygon': + return obj.hasOwnProperty('coordinates'); + default: + return false; + } + } + + copy(includeData = true) { // Do not copy isActive and isRemoved, because we use the copy to check if // anything changes so we need to rerender, but in these cases we do not need From b7e697b90d61f734bb302392c91c2c92c421e9da Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Mon, 9 Dec 2024 01:36:37 +0100 Subject: [PATCH 24/60] GIS: Add button for each layer to zoom the map to fit the layer + automatically zoom first added layer --- .../data-view/data-map/data-map.component.ts | 40 +++++++++++-------- .../layers/map-layers.component.html | 10 ++++- .../layers/map-layers.component.scss | 2 +- .../components/layers/map-layers.component.ts | 4 ++ .../querying/gis/models/MapLayer.model.ts | 39 +++++++++++++++++- .../gis/services/layersettings.service.ts | 6 +++ 6 files changed, 81 insertions(+), 20 deletions(-) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index 73346bc5..a5590e61 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -26,7 +26,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView isLoading = false; isLoadingMessage = 'TODO isLoadingMessage'; canRerenderLayers = false; - previewResult : CombinedResult = null; + previewResult: CombinedResult = null; readonly MIN_ZOOM = 0; readonly MAX_ZOOM = 19; @@ -97,6 +97,23 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView this.subscriptions.add(this.layerSettings.toggleLayerVisibility$.subscribe((layer) => { this.toggleLayerVisibility(layer); })); + + this.subscriptions.add(this.layerSettings.fitLayerToMap$.subscribe((layer) => { + if (layer) { + this.fitLayerToMap(layer) + } + + })); + } + + fitLayerToMap(layer: MapLayer) { + const bounds = layer.getBounds() + if (bounds.length > 0){ + const latLngBounds = L.latLngBounds(bounds); + if (latLngBounds.isValid()) { + this.map.fitBounds(latLngBounds); + } + } } ngOnDestroy() { @@ -283,22 +300,13 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView // Set SVG position correctly this.updateSvgPosition(); - // Center the map around the data. - // TODO: Do the same thing for paths. - const latLngs: L.LatLng[] = []; - this.circles!.each(d => { - // d has the property geometry, which is a GeoJSON point of type - latLngs.push(L.latLng(d.getPoint().coordinates[1], d.getPoint().coordinates[0],)); - }); - - // TODO: Currently, only do this, if the dataset is not too big. Otherwise we zoom way out, - // and zooming back in can be very slow. (if the points are all over the world) - if (latLngs.length > 0 && latLngs.length <= 1000) { - const latLngBounds = L.latLngBounds(latLngs); - if (latLngBounds.isValid()) { - this.map.fitBounds(latLngBounds); - } + // Only for the first layer we add: Adjust map to points from layer. + // Otherwise: Maybe the user is already looking at the data they are interested in -> do not change + // map position, because it could be unwanted. + if (this.layers.length == 1){ + this.fitLayerToMap(this.layers[0]) } + } finally { this.isLoading = false; } diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.html b/src/app/views/querying/gis/components/layers/map-layers.component.html index 1481eefc..04903f2a 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.html +++ b/src/app/views/querying/gis/components/layers/map-layers.component.html @@ -35,7 +35,13 @@

    - + - +
    (null); + addLayerFilterPolygon$ = this.addLayerFilterPolygon.asObservable(); + private fitLayerToMap = new BehaviorSubject(null); fitLayerToMap$ = this.fitLayerToMap.asObservable(); @@ -31,28 +34,40 @@ export class LayerSettingsService { private toggleLayerVisibilitySubject = new Subject(); toggleLayerVisibility$ = this.toggleLayerVisibilitySubject.asObservable(); - setFitLayerToMap(layer: MapLayer){ + + addPolygonFilterForLayer(layer: MapLayer) { + this.addLayerFilterPolygon.next(layer); + } + + setFitLayerToMap(layer: MapLayer) { this.fitLayerToMap.next(layer) } - setResultsQuery(result: CombinedResult){ + + setResultsQuery(result: CombinedResult) { this.queryFromConsoleResults.next(result); } + setBaseLayer(item: string) { this.selectedBaseLayer.next(item); } + setLayers(layers: MapLayer[]) { this.layers.next(layers); } + visualizationConfigurationChanged(visualization: Visualization): void { this.modifiedVisualization.next(visualization); } - setCanRerenderLayers(canRerenderMap: boolean){ + + setCanRerenderLayers(canRerenderMap: boolean) { this.canRerenderLayers.next(canRerenderMap); } - rerenderButtonClicked(){ + + rerenderButtonClicked() { this.rerenderButtonClickedSubject.next(); } - toggleLayerVisibility(layer: MapLayer){ + + toggleLayerVisibility(layer: MapLayer) { this.toggleLayerVisibilitySubject.next(layer); } } From e30c7efe23a17447aa3fa59c6161d0a27b121dc0 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Tue, 10 Dec 2024 17:53:49 +0100 Subject: [PATCH 27/60] Reset everything in LayerSettingsService once we navigate away from the map-layers component --- .../components/layers/map-layers.component.ts | 8 ++- .../gis/services/layersettings.service.ts | 71 ++++++++++++------- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index 732b640d..51c2e243 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -68,9 +68,11 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { } ngOnDestroy(): void { - // When we navigate away from the query mode, we remove all the layers from the layer settings as well - // as from the map. Otherwise, they show up when we don't want them, e.g. in the results view. - this.updateLayers([]); + // This component is only destroyed once we navigate away from the Map-Based Query Mode. In this case, + // we want to remove everything that belongs to this session. + // Note: Don't do the same thing for the map: Because when we are navigating from the results to the full + // query mode, we want to keep the session going, and transfer the last-run query over. + this.layerSettings.reset(); this.subscriptions.unsubscribe(); clearInterval(this.pollingTimer); } diff --git a/src/app/views/querying/gis/services/layersettings.service.ts b/src/app/views/querying/gis/services/layersettings.service.ts index b75c9a1a..5c89f31d 100644 --- a/src/app/views/querying/gis/services/layersettings.service.ts +++ b/src/app/views/querying/gis/services/layersettings.service.ts @@ -1,5 +1,5 @@ import {Injectable} from '@angular/core'; -import {BehaviorSubject, Subject} from 'rxjs'; +import {BehaviorSubject, Observable, Subject} from 'rxjs'; import {MapLayer} from '../models/MapLayer.model'; import {Visualization} from '../models/visualization.interface'; import {CombinedResult} from "../../../../components/data-view/data-view.model"; @@ -8,32 +8,53 @@ import {CombinedResult} from "../../../../components/data-view/data-view.model"; providedIn: 'root', }) export class LayerSettingsService { - private addLayerFilterPolygon = new BehaviorSubject(null); - addLayerFilterPolygon$ = this.addLayerFilterPolygon.asObservable(); - - private fitLayerToMap = new BehaviorSubject(null); - fitLayerToMap$ = this.fitLayerToMap.asObservable(); - - private queryFromConsoleResults = new BehaviorSubject(null); - queryFromConsoleResults$ = this.queryFromConsoleResults.asObservable(); - - private selectedBaseLayer = new BehaviorSubject('EMPTY'); - selectedBaseLayer$ = this.selectedBaseLayer.asObservable(); - - private layers = new BehaviorSubject([]); - layers$ = this.layers.asObservable(); - - private modifiedVisualization = new BehaviorSubject(null); - modifiedVisualization$ = this.modifiedVisualization.asObservable(); - - private canRerenderLayers = new BehaviorSubject(false); - canRerenderLayers$ = this.canRerenderLayers.asObservable(); + private addLayerFilterPolygon: BehaviorSubject; + addLayerFilterPolygon$: Observable; + private fitLayerToMap: BehaviorSubject; + fitLayerToMap$: Observable; + private queryFromConsoleResults: BehaviorSubject; + queryFromConsoleResults$: Observable; + private selectedBaseLayer: BehaviorSubject; + selectedBaseLayer$: Observable; + private layers: BehaviorSubject; + layers$: Observable; + private modifiedVisualization: BehaviorSubject; + modifiedVisualization$: Observable; + private canRerenderLayers: BehaviorSubject; + canRerenderLayers$: Observable; + private rerenderButtonClickedSubject: Subject; + rerenderButtonClicked$: Observable; + private toggleLayerVisibilitySubject: Subject; + toggleLayerVisibility$: Observable; + + constructor() { + this.reset() + } - private rerenderButtonClickedSubject = new Subject(); - rerenderButtonClicked$ = this.rerenderButtonClickedSubject.asObservable(); + reset() { + // To make sure that information from one session does not spill over the next session, we recreate all + // reactive variables, so that they don't hold any old values. Otherwise, the next time we subscribe, we will + // receive the most recent value, which could be from an old session. + this.addLayerFilterPolygon = new BehaviorSubject(null); + this.addLayerFilterPolygon$ = this.addLayerFilterPolygon.asObservable(); + this.fitLayerToMap = new BehaviorSubject(null); + this.fitLayerToMap$ = this.fitLayerToMap.asObservable(); + this.queryFromConsoleResults = new BehaviorSubject(null); + this.queryFromConsoleResults$ = this.queryFromConsoleResults.asObservable(); + this.selectedBaseLayer = new BehaviorSubject('EMPTY'); + this.selectedBaseLayer$ = this.selectedBaseLayer.asObservable(); + this.layers = new BehaviorSubject([]); + this.layers$ = this.layers.asObservable(); + this.modifiedVisualization = new BehaviorSubject(null); + this.modifiedVisualization$ = this.modifiedVisualization.asObservable(); + this.canRerenderLayers = new BehaviorSubject(false); + this.canRerenderLayers$ = this.canRerenderLayers.asObservable(); + this.rerenderButtonClickedSubject = new Subject(); + this.rerenderButtonClicked$ = this.rerenderButtonClickedSubject.asObservable(); + this.toggleLayerVisibilitySubject = new Subject(); + this.toggleLayerVisibility$ = this.toggleLayerVisibilitySubject.asObservable(); + } - private toggleLayerVisibilitySubject = new Subject(); - toggleLayerVisibility$ = this.toggleLayerVisibilitySubject.asObservable(); addPolygonFilterForLayer(layer: MapLayer) { this.addLayerFilterPolygon.next(layer); From c0458da1b18d7118ef70be6444f091f1c57d1cf3 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Tue, 10 Dec 2024 18:28:18 +0100 Subject: [PATCH 28/60] Use Object (Record) instead of Map --- .../data-preview/data-preview.component.ts | 2 +- .../layers/map-layers.component.html | 2 +- .../components/layers/map-layers.component.ts | 2 +- .../querying/gis/models/MapLayer.model.ts | 59 +++++++++++-------- .../querying/gis/models/RowResult.model.ts | 4 +- 5 files changed, 39 insertions(+), 30 deletions(-) diff --git a/src/app/views/querying/gis/components/configuration/data-preview/data-preview.component.ts b/src/app/views/querying/gis/components/configuration/data-preview/data-preview.component.ts index 0952f2cb..48a2af87 100644 --- a/src/app/views/querying/gis/components/configuration/data-preview/data-preview.component.ts +++ b/src/app/views/querying/gis/components/configuration/data-preview/data-preview.component.ts @@ -18,7 +18,7 @@ export class DataPreviewComponent implements MapLayerConfigurationComponent { private layerSettings: LayerSettingsService, ) { this.layer = config.layer; - this.previewObject = Object.fromEntries(config.layer.data[0].data); + this.previewObject = config.layer.data[0].data; } protected readonly Object = Object; diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.html b/src/app/views/querying/gis/components/layers/map-layers.component.html index 5bb034be..a14a1548 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.html +++ b/src/app/views/querying/gis/components/layers/map-layers.component.html @@ -56,7 +56,7 @@
    - (), + f.properties ? f.properties : {}, ), ), ); diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index bef03d1c..34b00c66 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -45,7 +45,7 @@ export class MapLayer { case DataModel.DOCUMENT: for (let rowIndex = 0; rowIndex < result.data.length; rowIndex++) { const json = result.data[rowIndex][0]; - const jsonObject: Map = new Map( + const jsonObject = Object.fromEntries( Object.entries(JSON.parse(json)).map(([key, value]) => [key.toLowerCase(), value]) ); const geometry = this.getGeometryFromData(jsonObject); @@ -55,35 +55,39 @@ export class MapLayer { } } break; + case DataModel.RELATIONAL: for (let rowIndex = 0; rowIndex < result.data.length; rowIndex++) { - const map = new Map(); + const obj: Record = {}; for (let headerIndex = 0; headerIndex < result.header.length; headerIndex++) { const header = result.header[headerIndex]; const key = header.name.toLowerCase(); const value = result.data[rowIndex][headerIndex]; + if (header.dataType.startsWith('GEOMETRY')) { - map.set(key, JSON.parse(value)); + obj[key] = JSON.parse(value); } else if (header.dataType.startsWith('INTEGER')) { - map.set(key, parseInt(value, 10)); + obj[key] = parseInt(value, 10); } else if (header.dataType.startsWith('DECIMAL')) { - map.set(key, parseFloat(value)); + obj[key] = parseFloat(value); } else { - map.set(key, value); + obj[key] = value; } } - const geometry = this.getGeometryFromData(map); + const geometry = this.getGeometryFromData(obj); if (geometry) { - const geometryWithData = new MapGeometryWithData(rowIndex, geometry, map); + const geometryWithData = new MapGeometryWithData(rowIndex, geometry, obj); mapData.push(geometryWithData); } } break; + case DataModel.GRAPH: for (let rowIndex = 0; rowIndex < result.data.length; rowIndex++) { - const map = new Map(); + const obj: Record = {}; + for (let headerIndex = 0; headerIndex < result.header.length; headerIndex++) { const header = result.header[headerIndex]; const key = header.name.toLowerCase(); @@ -93,37 +97,39 @@ export class MapLayer { if (datatype.startsWith('NODE')) { const json = JSON.parse(value); const properties = json['properties']; - const propertiesLowercase: Record = Object.fromEntries( + // Node stored as JSON + obj[key] = Object.fromEntries( Object.entries(properties).map(([key, value]) => [key.toLowerCase(), value]) ); - // Node stored as JSON - map.set(key, propertiesLowercase); } else { // Other value - map.set(key, value); + obj[key] = value; } } - const geometry = this.getGeometryFromData(map); + const geometry = this.getGeometryFromData(obj); if (geometry) { - const geometryWithData = new MapGeometryWithData(rowIndex, geometry, map); + const geometryWithData = new MapGeometryWithData(rowIndex, geometry, obj); mapData.push(geometryWithData); } } break; + default: throw Error(`Cannot convert CombinedResult to MapLayer. Unknown document model: ${result.dataModel}`); } + layer.addData(mapData); console.log('Created layer: ', layer); return layer; } - static getGeometryFromData(data: Map): Geometry | undefined { + + static getGeometryFromData(data: Record): Geometry | undefined { // Detect GeoJSON objects - for (const value of data.values()) { - if (this.isGeoJSON(value)){ - return value; + for (const key in data) { + if (data.hasOwnProperty(key) && this.isGeoJSON(data[key])) { + return data[key]; } } @@ -141,23 +147,26 @@ export class MapLayer { const lat = ll[0]; const lon = ll[1]; - if (data.has(lat) && - data.has(lon) && - isNumber(data.get(lat)) && - isNumber(data.get(lon))) { + if ( + data.hasOwnProperty(lat) && + data.hasOwnProperty(lon) && + isNumber(data[lat]) && + isNumber(data[lon]) + ) { return { type: 'Point', - coordinates: [data.get(lon), data.get(lat)] + coordinates: [data[lon], data[lat]], }; } } // TODO: Detect heuristic, so that we can automatically detect the most common geometry types - // - string in wkt format + // - string in WKT format return undefined; } + static isGeoJSON(obj: any): boolean { if (!obj || typeof obj !== 'object') return false; diff --git a/src/app/views/querying/gis/models/RowResult.model.ts b/src/app/views/querying/gis/models/RowResult.model.ts index 31c56cdb..6654906d 100644 --- a/src/app/views/querying/gis/models/RowResult.model.ts +++ b/src/app/views/querying/gis/models/RowResult.model.ts @@ -10,14 +10,14 @@ export class MapGeometryWithData { */ index: number; geometry: Geometry; - data: Map = new Map(); + data: Record = {}; cache: Record = {}; layer?: MapLayer = undefined; constructor( index: number, geometry: Geometry, - data: Map | undefined = undefined, + data: Record | undefined = undefined, ) { this.index = index; this.geometry = geometry; From 73f7cc3938bd0e091add8f1b8df5124f35774492 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Tue, 10 Dec 2024 18:47:22 +0100 Subject: [PATCH 29/60] Use layer-id instead of layer-name + layer-index for hiding / removing layers on the map --- src/app/components/data-view/data-map/data-map.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index 4e2f5954..7a3d0fba 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -350,7 +350,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView .data(points) .enter() .append('circle') - .attr('layer-name', (d) => d.layer!.name) + .attr('layer-id', (d) => d.layer!.uuid) .attr('layer-index', (d) => d.layer!.index.toString()) .attr('r', (d) => d.layer!.pointShapeVisualization.getValueForAttribute('r', d), @@ -396,7 +396,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView .data(paths) .enter() .append('path') - .attr('layer-name', (d) => d.layer!.name) + .attr('layer-id', (d) => d.layer!.uuid) .attr('layer-index', (d) => d.layer!.index.toString()) .attr('d', (d) => this.pathGenerator(d.geometry)) .attr('stroke-width', (d) => @@ -441,7 +441,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView const layerElements = this.g .node()! .querySelectorAll( - `[layer-name='${layer.name}'][layer-index='${layer.index.toString()}']`, + `[layer-id='${layer.uuid}']`, ); if (!layerElements.length) { From 86da72f2f11fc72469da680a9c46ec70b7f4919d Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Tue, 10 Dec 2024 19:17:39 +0100 Subject: [PATCH 30/60] MapLayer.addData(): Remove everything where geometry is null --- src/app/components/data-view/data-map/data-map.component.ts | 2 +- .../querying/gis/components/layers/map-layers.component.ts | 3 ++- .../visualization/area-shape-visualization.model.ts | 2 +- .../components/visualization/color-visualization-model.ts | 2 +- .../components/visualization/empty-visualization.model.ts | 2 +- .../components/visualization/label-visualization-model.ts | 2 +- .../visualization/point-shape-visualization.model.ts | 2 +- .../{RowResult.model.ts => MapGeometryWithData.model.ts} | 0 src/app/views/querying/gis/models/MapLayer.model.ts | 5 ++++- src/app/views/querying/gis/models/get-sample-maplayers.ts | 2 +- src/app/views/querying/gis/models/visualization.interface.ts | 2 +- 11 files changed, 14 insertions(+), 10 deletions(-) rename src/app/views/querying/gis/models/{RowResult.model.ts => MapGeometryWithData.model.ts} (100%) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index 7a3d0fba..69a2a05b 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -7,7 +7,7 @@ import * as L from 'leaflet'; import 'leaflet-draw'; import {LayerSettingsService} from '../../../views/querying/gis/services/layersettings.service'; import {MapLayer} from '../../../views/querying/gis/models/MapLayer.model'; -import {MapGeometryWithData} from '../../../views/querying/gis/models/RowResult.model'; +import {MapGeometryWithData} from '../../../views/querying/gis/models/MapGeometryWithData.model'; import {CombinedResult} from '../data-view.model'; // tslint:disable:no-non-null-assertion diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index becd3b60..d011e053 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -8,7 +8,7 @@ import { } from '@angular/core'; import {LayerContext} from '../../models/LayerContext.model'; import {LayerSettingsService} from '../../services/layersettings.service'; -import {MapGeometryWithData} from '../../models/RowResult.model'; +import {MapGeometryWithData} from '../../models/MapGeometryWithData.model'; import * as GeoJSON from 'geojson'; import {FeatureCollection} from 'geojson'; import {MapLayer} from '../../models/MapLayer.model'; @@ -256,6 +256,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { const file = input.files[0]; this.loadedGeoJsonFileName = file.name; this.loadedGeoJsonFile = JSON.parse(await file.text()); + console.log("Loaded GeoJSON file!") } } catch (error) { if (error instanceof Error) { diff --git a/src/app/views/querying/gis/components/visualization/area-shape-visualization.model.ts b/src/app/views/querying/gis/components/visualization/area-shape-visualization.model.ts index 65e13740..a21877ad 100644 --- a/src/app/views/querying/gis/components/visualization/area-shape-visualization.model.ts +++ b/src/app/views/querying/gis/components/visualization/area-shape-visualization.model.ts @@ -1,5 +1,5 @@ import { Visualization } from '../../models/visualization.interface'; -import { MapGeometryWithData } from '../../models/RowResult.model'; +import { MapGeometryWithData } from '../../models/MapGeometryWithData.model'; import {AreaShapeComponent} from "./area-shape/area-shape.component"; import {MapLayerConfiguration} from "../../models/MapLayerConfiguration.interface"; diff --git a/src/app/views/querying/gis/components/visualization/color-visualization-model.ts b/src/app/views/querying/gis/components/visualization/color-visualization-model.ts index ed5558eb..5fc2dff2 100644 --- a/src/app/views/querying/gis/components/visualization/color-visualization-model.ts +++ b/src/app/views/querying/gis/components/visualization/color-visualization-model.ts @@ -1,5 +1,5 @@ import { Visualization } from '../../models/visualization.interface'; -import { MapGeometryWithData } from '../../models/RowResult.model'; +import { MapGeometryWithData } from '../../models/MapGeometryWithData.model'; import * as d3 from 'd3'; import * as turf from '@turf/turf'; import { ColorComponent } from './color/color.component'; diff --git a/src/app/views/querying/gis/components/visualization/empty-visualization.model.ts b/src/app/views/querying/gis/components/visualization/empty-visualization.model.ts index 02824d7c..b5090bca 100644 --- a/src/app/views/querying/gis/components/visualization/empty-visualization.model.ts +++ b/src/app/views/querying/gis/components/visualization/empty-visualization.model.ts @@ -1,5 +1,5 @@ import { Type } from '@angular/core'; -import { MapGeometryWithData } from '../../models/RowResult.model'; +import { MapGeometryWithData } from '../../models/MapGeometryWithData.model'; import { MapLayerConfigurationComponent } from '../../models/visualization-configuration.interface'; import { Visualization } from '../../models/visualization.interface'; import {EmptyComponent} from "./empty/empty.component"; diff --git a/src/app/views/querying/gis/components/visualization/label-visualization-model.ts b/src/app/views/querying/gis/components/visualization/label-visualization-model.ts index ac4a965c..07d77439 100644 --- a/src/app/views/querying/gis/components/visualization/label-visualization-model.ts +++ b/src/app/views/querying/gis/components/visualization/label-visualization-model.ts @@ -1,5 +1,5 @@ import { Visualization } from '../../models/visualization.interface'; -import { MapGeometryWithData } from '../../models/RowResult.model'; +import { MapGeometryWithData } from '../../models/MapGeometryWithData.model'; import {LabelComponent} from "./label/label.component"; import {MapLayerConfiguration} from "../../models/MapLayerConfiguration.interface"; diff --git a/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts b/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts index f0da4716..f7a538c3 100644 --- a/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts +++ b/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts @@ -1,5 +1,5 @@ import { Visualization } from '../../models/visualization.interface'; -import { MapGeometryWithData } from '../../models/RowResult.model'; +import { MapGeometryWithData } from '../../models/MapGeometryWithData.model'; import { PointShapeComponent } from './point-shape/point-shape.component'; import {MapLayerConfiguration} from "../../models/MapLayerConfiguration.interface"; diff --git a/src/app/views/querying/gis/models/RowResult.model.ts b/src/app/views/querying/gis/models/MapGeometryWithData.model.ts similarity index 100% rename from src/app/views/querying/gis/models/RowResult.model.ts rename to src/app/views/querying/gis/models/MapGeometryWithData.model.ts diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index 34b00c66..c2b8a881 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -1,5 +1,5 @@ import {Visualization} from './visualization.interface'; -import {MapGeometryWithData} from './RowResult.model'; +import {MapGeometryWithData} from './MapGeometryWithData.model'; import {ColorVisualization} from '../components/visualization/color-visualization-model'; import {AreaShapeVisualization} from '../components/visualization/area-shape-visualization.model'; import {LabelVisualization} from '../components/visualization/label-visualization-model'; @@ -265,6 +265,9 @@ export class MapLayer { this.containsPoints = false; this.containsAreas = false; + // Remove everything that does not have a geometry. + data = data.filter((d) => d.geometry !== null); + data.forEach((d) => { if (d.isPoint()){ this.containsPoints = true; diff --git a/src/app/views/querying/gis/models/get-sample-maplayers.ts b/src/app/views/querying/gis/models/get-sample-maplayers.ts index e7694a9b..5cdb711c 100644 --- a/src/app/views/querying/gis/models/get-sample-maplayers.ts +++ b/src/app/views/querying/gis/models/get-sample-maplayers.ts @@ -1,6 +1,6 @@ import { MapLayer } from './MapLayer.model'; import * as GeoJSON from 'geojson'; -import { MapGeometryWithData } from './RowResult.model'; +import { MapGeometryWithData } from './MapGeometryWithData.model'; export function getSampleMapLayers(): MapLayer[] { const data = ` diff --git a/src/app/views/querying/gis/models/visualization.interface.ts b/src/app/views/querying/gis/models/visualization.interface.ts index 8eb5d8de..5898b43a 100644 --- a/src/app/views/querying/gis/models/visualization.interface.ts +++ b/src/app/views/querying/gis/models/visualization.interface.ts @@ -1,6 +1,6 @@ import { MapLayerConfigurationComponent } from './visualization-configuration.interface'; import { Type } from '@angular/core'; -import {MapGeometryWithData} from "./RowResult.model"; +import {MapGeometryWithData} from "./MapGeometryWithData.model"; import {MapLayerConfiguration} from "./MapLayerConfiguration.interface"; export interface Visualization extends MapLayerConfiguration { From 76531af8dc182891b344b436e7d28bacc4e34601 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Wed, 11 Dec 2024 01:40:14 +0100 Subject: [PATCH 31/60] Filter Section + Export Button --- .../data-view/data-map/data-map.component.ts | 67 ++-- .../components/configuration/DataPreview.ts | 4 + .../components/configuration/FilterConfig.ts | 32 ++ .../configuration/filter/filter.component.css | 7 + .../filter/filter.component.html | 12 + .../configuration/filter/filter.component.ts | 36 ++ .../layers/map-layers.component.html | 20 +- .../layers/map-layers.component.scss | 323 ++++++++++-------- .../components/layers/map-layers.component.ts | 64 +++- .../querying/gis/models/MapLayer.model.ts | 15 +- .../models/MapLayerConfiguration.interface.ts | 2 + .../gis/services/layersettings.service.ts | 31 +- src/app/views/views.module.ts | 4 +- 13 files changed, 421 insertions(+), 196 deletions(-) create mode 100644 src/app/views/querying/gis/components/configuration/FilterConfig.ts create mode 100644 src/app/views/querying/gis/components/configuration/filter/filter.component.css create mode 100644 src/app/views/querying/gis/components/configuration/filter/filter.component.html create mode 100644 src/app/views/querying/gis/components/configuration/filter/filter.component.ts diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index 69a2a05b..11eaaafd 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -9,6 +9,8 @@ import {LayerSettingsService} from '../../../views/querying/gis/services/layerse import {MapLayer} from '../../../views/querying/gis/models/MapLayer.model'; import {MapGeometryWithData} from '../../../views/querying/gis/models/MapGeometryWithData.model'; import {CombinedResult} from '../data-view.model'; +import {LatLng} from "leaflet"; +import {Polygon, Position} from "geojson"; // tslint:disable:no-non-null-assertion @@ -27,7 +29,12 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView isLoadingMessage = 'TODO isLoadingMessage'; canRerenderLayers = false; previewResult: CombinedResult = null; + + // leaflet-draw leafletDrawControl = null; + layerIdToPoylgon = new Map() + currentDrawingLayer: MapLayer = null; + polygonTool = null; readonly MIN_ZOOM = 0; readonly MAX_ZOOM = 19; @@ -142,37 +149,53 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView circlemarker: false } }); - // leafletMap.addControl(this.leafletDrawControl); - // leafletMap.on(L.Draw.Event.CREATED, function (event) { - // const layer = event.layer; - // drawnItems.addLayer(layer); - // }); + leafletMap.on(L.Draw.Event.CREATED, (event) => { + if (this.currentDrawingLayer === null) { + throw new Error("We are drawing, but there is no layer set?") + } + const polygon: L.Polygon = event.layer; + this.layerIdToPoylgon.set(this.currentDrawingLayer.uuid, polygon); + console.log("Polygon added for layer ", this.currentDrawingLayer.uuid, polygon) + leafletMap.removeControl(this.leafletDrawControl) + this.polygonTool.disable() + drawnItems.addLayer(polygon); + + // Send coordinates back to map-layers + let coordinates = polygon.getLatLngs() as LatLng[][]; + let positions: Position[][] = coordinates.map((ring) => + ring.map((c) => [c.lng, c.lat]) + ); + const geoJsonPolygon: Polygon = { + type: "Polygon", + coordinates: positions, + } + this.layerSettings.addPolygonToLayer(this.currentDrawingLayer, geoJsonPolygon); + }); this.subscriptions.add(this.layerSettings.addLayerFilterPolygon$.subscribe((mapLayer) => { if (mapLayer === null) { return; } - console.log("addLayerFilterPolygon$", leafletMap, this.leafletDrawControl.options.draw.polygon) - - const drawControl = this.leafletDrawControl - leafletMap.addControl(drawControl) + this.currentDrawingLayer = mapLayer; + leafletMap.addControl(this.leafletDrawControl); const polygonTool = new L.Draw.Polygon(leafletMap, this.leafletDrawControl.options.draw.polygon); polygonTool.enable(); + this.polygonTool = polygonTool; + })); - // TODO: Do not create new handler in here... - leafletMap.on(L.Draw.Event.CREATED, function (event) { - const polygon = event.layer; - - if (polygon instanceof L.Polygon){ - const coordinates = polygon.getLatLngs(); - console.log("polygon coordinates", coordinates) - } - - leafletMap.removeControl(drawControl) - polygonTool.disable() - drawnItems.addLayer(polygon); - }); + this.subscriptions.add(this.layerSettings.removeLayerFilterPolygon$.subscribe((mapLayer) => { + if (mapLayer === null) { + return; + } + console.log("data-map remove layer filter polygon for ", mapLayer, this.layerIdToPoylgon); + console.trace(); + if (!this.layerIdToPoylgon.has(mapLayer.uuid)) { + return; + } + const polygon = this.layerIdToPoylgon.get(mapLayer.uuid); + console.log("Remove polygon", polygon); + drawnItems.removeLayer(polygon); })); this.svg = d3.select(this.map.getPanes().overlayPane).append('svg'); diff --git a/src/app/views/querying/gis/components/configuration/DataPreview.ts b/src/app/views/querying/gis/components/configuration/DataPreview.ts index aa68ed5c..15095a91 100644 --- a/src/app/views/querying/gis/components/configuration/DataPreview.ts +++ b/src/app/views/querying/gis/components/configuration/DataPreview.ts @@ -10,4 +10,8 @@ export class DataPreview implements MapLayerConfiguration { this.layer = layer; } + copy(): MapLayerConfiguration { + throw new Error("Layer should not need to be copied, there is no configuration that can change."); + } + } \ No newline at end of file diff --git a/src/app/views/querying/gis/components/configuration/FilterConfig.ts b/src/app/views/querying/gis/components/configuration/FilterConfig.ts new file mode 100644 index 00000000..4053cf3e --- /dev/null +++ b/src/app/views/querying/gis/components/configuration/FilterConfig.ts @@ -0,0 +1,32 @@ +import {MapLayerConfiguration} from "../../models/MapLayerConfiguration.interface"; +import {Polygon} from 'geojson' +import {FilterComponent} from "./filter/filter.component"; +import {MapLayer} from "../../models/MapLayer.model"; + +export class FilterConfig implements MapLayerConfiguration { + configurationComponentType = FilterComponent + filterPolygon : Polygon = null; + filterPolygonText = "" + layer: MapLayer; + + constructor(layer : MapLayer) { + this.layer = layer; + } + + copy(): MapLayerConfiguration { + const copy = new FilterConfig(this.layer) + copy.filterPolygon = this.filterPolygon; + copy.filterPolygonText = this.filterPolygonText; + return copy; + } + + addPolygon(polygon: Polygon) { + this.filterPolygon = polygon; + this.filterPolygonText = "Polygon"; + } + + removePolygon() { + this.filterPolygon = null; + this.filterPolygonText = ""; + } +} \ No newline at end of file diff --git a/src/app/views/querying/gis/components/configuration/filter/filter.component.css b/src/app/views/querying/gis/components/configuration/filter/filter.component.css new file mode 100644 index 00000000..3e275ac2 --- /dev/null +++ b/src/app/views/querying/gis/components/configuration/filter/filter.component.css @@ -0,0 +1,7 @@ +div { + display: flex; + flex-direction: column; + gap: 1rem; +} + + diff --git a/src/app/views/querying/gis/components/configuration/filter/filter.component.html b/src/app/views/querying/gis/components/configuration/filter/filter.component.html new file mode 100644 index 00000000..65639820 --- /dev/null +++ b/src/app/views/querying/gis/components/configuration/filter/filter.component.html @@ -0,0 +1,12 @@ +
    + + Polygon + + + +
    diff --git a/src/app/views/querying/gis/components/configuration/filter/filter.component.ts b/src/app/views/querying/gis/components/configuration/filter/filter.component.ts new file mode 100644 index 00000000..05213629 --- /dev/null +++ b/src/app/views/querying/gis/components/configuration/filter/filter.component.ts @@ -0,0 +1,36 @@ +import {Component, Inject} from '@angular/core'; +import {MapLayerConfigurationComponent} from '../../../models/visualization-configuration.interface'; +import {LayerSettingsService} from '../../../services/layersettings.service'; +import {DataPreview} from "../DataPreview"; +import {MapLayer} from "../../../models/MapLayer.model"; +import {FilterConfig} from "../FilterConfig"; + +@Component({ + selector: 'app-data-preview', + templateUrl: './filter.component.html', + styleUrl: './filter.component.css', +}) +export class FilterComponent implements MapLayerConfigurationComponent { + + constructor( + @Inject('config') protected config: FilterConfig, + private layerSettings: LayerSettingsService, + ) { + // + } + + configChanged() { + this.layerSettings.visualizationConfigurationChanged(this.config); + } + + drawOrRemovePolygon() { + if (this.config.filterPolygon) { + console.log("drawOrRemovePolygon REMOVE") + this.config.removePolygon(); + this.layerSettings.removePolygonFilterForLayer(this.config.layer); + } else { + console.log("drawOrRemovePolygon ADD") + this.layerSettings.addPolygonFilterForLayer(this.config.layer) + } + } +} diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.html b/src/app/views/querying/gis/components/layers/map-layers.component.html index a14a1548..54e4137c 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.html +++ b/src/app/views/querying/gis/components/layers/map-layers.component.html @@ -50,15 +50,16 @@
    - {{ layer.name }} ({{ layer.data.length }}) -
    -
    - + {{ layer.name }} + ({{ layer.data.length }})
    + @@ -103,6 +104,17 @@ + + diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.scss b/src/app/views/querying/gis/components/layers/map-layers.component.scss index d0bda68d..6a16b435 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.scss +++ b/src/app/views/querying/gis/components/layers/map-layers.component.scss @@ -1,218 +1,241 @@ .layers-container { - position: relative; - height: 100%; - display: flex; - flex-direction: column; - margin: 1rem; - grid-gap: 1rem; + position: relative; + height: 100%; + display: flex; + flex-direction: column; + margin: 1rem; + grid-gap: 1rem; - max-height: 100%; - overflow-y: auto; + max-height: 100%; + overflow-y: auto; - & > .spacer-top { - flex: 1; - } + & > .spacer-top { + flex: 1; + } - & > .spacer-bottom { - margin-bottom: 2rem; - } + & > .spacer-bottom { + margin-bottom: 2rem; + } } .drag-drop-layers { - display: flex; - flex-direction: column; - grid-gap: 1rem; + display: flex; + flex-direction: column; + grid-gap: 1rem; } .top-bottom-label { - text-align: center; - color: lightgray; - margin: 0 6rem; - letter-spacing: 0.5px; - user-select: none; + text-align: center; + color: lightgray; + margin: 0 6rem; + letter-spacing: 0.5px; + user-select: none; } .top-label { - border-bottom: 1px solid lightgray; + border-bottom: 1px solid lightgray; } .bottom-label { - border-top: 1px solid lightgray; + border-top: 1px solid lightgray; } .base-layers { - display: flex; - justify-content: space-evenly; - padding: 0.5rem 0.25rem; + display: flex; + justify-content: space-evenly; + padding: 0.5rem 0.25rem; } .base-layers > div { - display: grid; - place-items: center; - height: 50px; - width: 50px; - border-radius: 0.2rem; - transition: all 100ms ease-in-out; + display: grid; + place-items: center; + height: 50px; + width: 50px; + border-radius: 0.2rem; + transition: all 100ms ease-in-out; } .base-layers > div:hover { - background: #f1f1f1; - cursor: pointer; + background: #f1f1f1; + cursor: pointer; } .base-layers > div > svg { - width: 30px; - fill: #bfdab3; + width: 30px; + fill: #bfdab3; } .add-layer-button { - all: unset; - border-radius: 100%; - border: 1px solid #1a186c; - background: #4b49b6; - position: absolute; - bottom: -1.75rem; - left: 50%; - transform: translateX(-50%); - padding: 1.5rem; - display: grid; - place-items: center; - cursor: pointer; + all: unset; + border-radius: 100%; + border: 1px solid #1a186c; + background: #4b49b6; + position: absolute; + bottom: -1.75rem; + left: 50%; + transform: translateX(-50%); + padding: 1.5rem; + display: grid; + place-items: center; + cursor: pointer; } .add-layer-button > svg { - width: 1rem; - transform: scale(1.2); - fill: #f3f4f7; + width: 1rem; + transform: scale(1.2); + fill: #f3f4f7; } .add-layer-query { - display: flex; - flex-direction: column; - gap: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; - & > .query-controls { - align-content: flex-end; - } + & > .query-controls { + align-content: flex-end; + } } .layer-card { - display: flex; - border: 1px solid lightgray; - flex-direction: column; - background: white; - - & > .card-header { - display: grid; - place-items: center; - grid-gap: 0.5rem; - grid-template-columns: 1fr 1fr 1fr; - color: lightgray; - - & > span { - width: 100%; - user-select: none; - } + display: flex; + border: 1px solid lightgray; + flex-direction: column; + background: white; - & > .resize-grip { - display: grid; - place-items: center; - width: 2rem; - height: 15px; - grid-template-columns: repeat(3, 1fr); - grid-template-rows: repeat(2, 1fr); - cursor: grab; - - & > span { - height: 3px; - width: 3px; - background-color: #bbb; - border-radius: 50%; - display: inline-block; - } - } + & > .card-header { + display: grid; + place-items: center; + grid-gap: 0.5rem; + grid-template-columns: 1fr 1fr 1fr; + color: lightgray; - & > .icons { - display: flex; - gap: 0.35rem; - width: 100%; - justify-content: flex-end; - - & > .eye, & > .remove, & > .zoom { - all: unset; - - & > svg { - width: 1rem; - fill: lightgray; - transition: all 100ms ease-in-out; - cursor: pointer; - - &:hover { - fill: #4b49b6; - } - } - } - } + & > span { + width: 100%; + user-select: none; } - & > .layer-card-body { - display: flex; - flex-direction: column; - gap: 1rem; + & > .resize-grip { + display: grid; + place-items: center; + width: 2rem; + height: 15px; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); + cursor: grab; + + & > span { + height: 3px; + width: 3px; + background-color: #bbb; + border-radius: 50%; + display: inline-block; + } } - & .title { - word-break: break-all; + & > .icons { + display: flex; + gap: 0.35rem; + width: 100%; + justify-content: flex-end; - & > .layer-name { - text-transform: uppercase; - letter-spacing: 0.5px; - font-weight: bold; - } + & > .eye, & > .remove, & > .zoom { + all: unset; + + & > svg { + width: 1rem; + fill: lightgray; + transition: all 100ms ease-in-out; + cursor: pointer; - & > .layer-size { - text-transform: uppercase; - letter-spacing: 0.5px; + &:hover { + fill: #4b49b6; + } } + } + } + } + + & > .layer-card-body { + display: flex; + flex-direction: column; + gap: 1rem; + } - & > .preview-data { - all: unset; - color: #4645ab; - cursor: pointer; - user-select: none; - margin-left: 0.25rem; + & .title { + word-break: break-all; - &:hover { - text-decoration: underline; - } - } + & > .layer-name { + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: bold; } - & .config-sections { - display: flex; - flex-direction: column; - gap: 0.5rem; + & > .layer-size { + text-transform: uppercase; + letter-spacing: 0.5px; } - & .config-container { - flex: 1; + & > .preview-data { + all: unset; + color: #4645ab; + cursor: pointer; + user-select: none; + margin-left: 0.25rem; + + &:hover { + text-decoration: underline; + } } + } + + & .config-sections { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + & .config-container { + flex: 1; + } } .layer-card-placeholder { - border: 2px dashed #007bff; - background-color: #e9f5ff; - height: 50px; + border: 2px dashed #007bff; + background-color: #e9f5ff; + height: 50px; } .json-viewer-popover-body { - max-height: 40vh; - overflow-y: auto; + max-height: 40vh; + overflow-y: auto; } .add-layer-modal-body { - display: flex; - flex-direction: column; - gap: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; } +.share-section { + display: flex; + align-items: center; + gap: 1rem; + + & > span { + color: #bdbdbd; + } +} + +.share-button { + & > svg { + fill: #27aae1; + width: 16px; + margin-right: 0.25rem; + } + + &:hover { + & > svg { + fill: currentColor; + } + } +} diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index d011e053..2c179a1f 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -164,7 +164,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { this.updateLayerUi(); })) - this.subscriptions.add(this.layerSettings.modifiedVisualization$.subscribe((config) => { + this.subscriptions.add(this.layerSettings.modifiedConfig$.subscribe((config) => { if (!config) { return; } @@ -181,7 +181,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { })); this.subscriptions.add(this.layerSettings.queryFromConsoleResults$.subscribe((query) => { - if (query){ + if (query) { console.log("Run full query from results", query); this.submitQuery(query.query, query.language.toString(), query.namespace); // Remove it, so that if we navigate away and back again, we won't run the query twice. @@ -189,6 +189,14 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { } })); + this.subscriptions.add(this.layerSettings.layerPolygonFilter$.subscribe((layerAndPolygon) => { + if (layerAndPolygon === null) { + return + } + const [layer, polygon] = layerAndPolygon; + layer.filterConfig.addPolygon(polygon); + })); + // this.updateLayers(getSampleMapLayers()); } @@ -208,7 +216,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { this.pollingTimer = setInterval(() => { const currentHeight = this.getMapHeight(); - if (currentHeight === undefined){ + if (currentHeight === undefined) { return } @@ -228,7 +236,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { private getMapHeight(): string | undefined { const elem = (document.querySelector('#map') as HTMLElement) - if (elem === null){ + if (elem === null) { return undefined } else { return `${elem.offsetHeight}px` @@ -266,7 +274,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { } } - fitLayerToMap(layer: MapLayer){ + fitLayerToMap(layer: MapLayer) { this.layerSettings.setFitLayerToMap(layer); } @@ -304,13 +312,13 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { return geojson; } - submitQuery(query: string, language: string, namespace: string) : boolean { + submitQuery(query: string, language: string, namespace: string): boolean { const request = new QueryRequest(query, false, false, language, namespace); request.noLimit = true; return this._crud.anyQuery(this.websocket, request) } - filterLayer(layer: MapLayer){ + filterLayer(layer: MapLayer) { console.log("Filter layer", layer) this.layerSettings.addPolygonFilterForLayer(layer); } @@ -320,7 +328,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { switch (this.addLayerMode) { case LayerContext.Query: - if (!this.submitQuery(this.queryEditor.getCode(), this.language(), this.activeNamespace())){ + if (!this.submitQuery(this.queryEditor.getCode(), this.language(), this.activeNamespace())) { this.addLayerDialogErrorMessage = 'There was an error executing this query.'; } // Dialog will be hidden when result has arrived in constructor.effect, because it is possible to @@ -388,4 +396,44 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { this.anyLayersVisible = this.layers.filter((d) => !d.isRemoved).length > 0; } + + export() { + if (this.layers.length === 0){ + return; + } + + // Create and download a GeoJSON file. + const geoJSON = { + type: "FeatureCollection", + features: this.layers.flatMap(layer => + layer.data.map(item => ({ + type: "Feature", + geometry: item.geometry, + properties: { + ...item.data, + layerName: layer.name, + layerUUID: layer.uuid, + }, + })) + ), + }; + + const jsonString = JSON.stringify(geoJSON, null, 2); + const blob = new Blob([jsonString], {type: "application/json"}); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + const now = new Date(); + // Timestamp looks like 01-12-2024-15-30 + const timestamp = [ + now.getDate().toString().padStart(2, '0'), + (now.getMonth() + 1).toString().padStart(2, '0'), + now.getFullYear(), + now.getHours().toString().padStart(2, '0'), + now.getMinutes().toString().padStart(2, '0') + ].join("-"); + a.download = `${timestamp}_polypheny_map_layers_export.geojson`; + a.click(); + URL.revokeObjectURL(url); + } } diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index c2b8a881..a67e45fb 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -11,6 +11,7 @@ import {v4} from 'uuid' import * as L from 'leaflet'; import {MapLayerConfiguration} from "./MapLayerConfiguration.interface"; import {DataPreview} from "../components/configuration/DataPreview"; +import {FilterConfig} from "../components/configuration/FilterConfig"; export class MapLayer { @@ -24,8 +25,10 @@ export class MapLayer { data: MapGeometryWithData[] = []; containsPoints = false; containsAreas = false; + isQueryLayer = false; - dataPreview : MapLayerConfiguration = new DataPreview(this) + dataPreview: MapLayerConfiguration = new DataPreview(this); + filterConfig: FilterConfig = new FilterConfig(this); pointShapeVisualization: Visualization = new PointShapeVisualization(3); areaShapeVisualization: Visualization = new AreaShapeVisualization(1); colorVisualization: Visualization = new ColorVisualization('red'); @@ -39,6 +42,7 @@ export class MapLayer { static from(result: CombinedResult): MapLayer { console.log("MapLayer from result: ", result); const layer = new MapLayer(result.query); + layer.isQueryLayer = true; const mapData = []; switch (result.dataModel) { @@ -206,9 +210,9 @@ export class MapLayer { } getBounds(): L.LatLng[] { - const bounds : L.LatLng[] = []; + const bounds: L.LatLng[] = []; - for (const data of this.data){ + for (const data of this.data) { switch (data.geometry.type) { case 'Point': bounds.push(L.latLng(data.geometry.coordinates[1], data.geometry.coordinates[0])); @@ -248,7 +252,7 @@ export class MapLayer { // to rerender. const copy = new MapLayer(this.name) - if (includeData){ + if (includeData) { copy.addData( this.data.map((d) => d.copy()), ); @@ -258,6 +262,7 @@ export class MapLayer { copy.areaShapeVisualization = this.areaShapeVisualization.copy(); copy.colorVisualization = this.colorVisualization.copy(); copy.labelVisualization = this.labelVisualization.copy(); + copy.filterConfig = this.filterConfig.copy() as FilterConfig; return copy; } @@ -269,7 +274,7 @@ export class MapLayer { data = data.filter((d) => d.geometry !== null); data.forEach((d) => { - if (d.isPoint()){ + if (d.isPoint()) { this.containsPoints = true; } else { this.containsAreas = true; diff --git a/src/app/views/querying/gis/models/MapLayerConfiguration.interface.ts b/src/app/views/querying/gis/models/MapLayerConfiguration.interface.ts index 2341b4ce..31ee19a6 100644 --- a/src/app/views/querying/gis/models/MapLayerConfiguration.interface.ts +++ b/src/app/views/querying/gis/models/MapLayerConfiguration.interface.ts @@ -3,4 +3,6 @@ import {MapLayerConfigurationComponent} from "./visualization-configuration.inte export interface MapLayerConfiguration { configurationComponentType: Type; + + copy(): MapLayerConfiguration; } \ No newline at end of file diff --git a/src/app/views/querying/gis/services/layersettings.service.ts b/src/app/views/querying/gis/services/layersettings.service.ts index 5c89f31d..e2eef87a 100644 --- a/src/app/views/querying/gis/services/layersettings.service.ts +++ b/src/app/views/querying/gis/services/layersettings.service.ts @@ -3,13 +3,21 @@ import {BehaviorSubject, Observable, Subject} from 'rxjs'; import {MapLayer} from '../models/MapLayer.model'; import {Visualization} from '../models/visualization.interface'; import {CombinedResult} from "../../../../components/data-view/data-view.model"; +import {MapLayerConfiguration} from "../models/MapLayerConfiguration.interface"; +import {Polygon} from "geojson"; + @Injectable({ providedIn: 'root', }) export class LayerSettingsService { + // Filter private addLayerFilterPolygon: BehaviorSubject; addLayerFilterPolygon$: Observable; + private removeLayerFilterPolygon: BehaviorSubject; + removeLayerFilterPolygon$: Observable; + private layerPolygonFilter: BehaviorSubject<[MapLayer, Polygon]>; + layerPolygonFilter$: Observable<[MapLayer, Polygon]>; private fitLayerToMap: BehaviorSubject; fitLayerToMap$: Observable; private queryFromConsoleResults: BehaviorSubject; @@ -18,8 +26,8 @@ export class LayerSettingsService { selectedBaseLayer$: Observable; private layers: BehaviorSubject; layers$: Observable; - private modifiedVisualization: BehaviorSubject; - modifiedVisualization$: Observable; + private modifiedConfig: BehaviorSubject; + modifiedConfig$: Observable; private canRerenderLayers: BehaviorSubject; canRerenderLayers$: Observable; private rerenderButtonClickedSubject: Subject; @@ -37,6 +45,10 @@ export class LayerSettingsService { // receive the most recent value, which could be from an old session. this.addLayerFilterPolygon = new BehaviorSubject(null); this.addLayerFilterPolygon$ = this.addLayerFilterPolygon.asObservable(); + this.removeLayerFilterPolygon = new BehaviorSubject(null); + this.removeLayerFilterPolygon$ = this.removeLayerFilterPolygon.asObservable(); + this.layerPolygonFilter = new BehaviorSubject<[MapLayer, Polygon]>(null); + this.layerPolygonFilter$ = this.layerPolygonFilter.asObservable(); this.fitLayerToMap = new BehaviorSubject(null); this.fitLayerToMap$ = this.fitLayerToMap.asObservable(); this.queryFromConsoleResults = new BehaviorSubject(null); @@ -45,8 +57,8 @@ export class LayerSettingsService { this.selectedBaseLayer$ = this.selectedBaseLayer.asObservable(); this.layers = new BehaviorSubject([]); this.layers$ = this.layers.asObservable(); - this.modifiedVisualization = new BehaviorSubject(null); - this.modifiedVisualization$ = this.modifiedVisualization.asObservable(); + this.modifiedConfig = new BehaviorSubject(null); + this.modifiedConfig$ = this.modifiedConfig.asObservable(); this.canRerenderLayers = new BehaviorSubject(false); this.canRerenderLayers$ = this.canRerenderLayers.asObservable(); this.rerenderButtonClickedSubject = new Subject(); @@ -55,11 +67,18 @@ export class LayerSettingsService { this.toggleLayerVisibility$ = this.toggleLayerVisibilitySubject.asObservable(); } + removePolygonFilterForLayer(layer: MapLayer) { + this.removeLayerFilterPolygon.next(layer); + } addPolygonFilterForLayer(layer: MapLayer) { this.addLayerFilterPolygon.next(layer); } + addPolygonToLayer(layer: MapLayer, polygon: Polygon){ + this.layerPolygonFilter.next([layer, polygon]); + } + setFitLayerToMap(layer: MapLayer) { this.fitLayerToMap.next(layer) } @@ -76,8 +95,8 @@ export class LayerSettingsService { this.layers.next(layers); } - visualizationConfigurationChanged(visualization: Visualization): void { - this.modifiedVisualization.next(visualization); + visualizationConfigurationChanged(config: MapLayerConfiguration): void { + this.modifiedConfig.next(config); } setCanRerenderLayers(canRerenderMap: boolean) { diff --git a/src/app/views/views.module.ts b/src/app/views/views.module.ts index a481188d..0f92d727 100644 --- a/src/app/views/views.module.ts +++ b/src/app/views/views.module.ts @@ -110,6 +110,7 @@ import {LabelComponent} from "./querying/gis/components/visualization/label/labe import {PointShapeComponent} from "./querying/gis/components/visualization/point-shape/point-shape.component"; import {NgxJsonViewerModule} from "ngx-json-viewer"; import {DataPreviewComponent} from "./querying/gis/components/configuration/data-preview/data-preview.component"; +import {FilterComponent} from "./querying/gis/components/configuration/filter/filter.component"; @NgModule({ @@ -223,7 +224,8 @@ import {DataPreviewComponent} from "./querying/gis/components/configuration/data EmptyComponent, LabelComponent, PointShapeComponent, - DataPreviewComponent + DataPreviewComponent, + FilterComponent ], exports: [ QueryEditor, From dcc223125403fef6362bb21e19c4e5e03b148d23 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Wed, 11 Dec 2024 01:57:35 +0100 Subject: [PATCH 32/60] Add button to rerender query layers + update layer change detection --- .../layers/map-layers.component.html | 7 ++++ .../layers/map-layers.component.scss | 2 +- .../components/layers/map-layers.component.ts | 35 +++++++++++++++---- .../querying/gis/models/MapLayer.model.ts | 11 ++++++ 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.html b/src/app/views/querying/gis/components/layers/map-layers.component.html index 54e4137c..cdbded41 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.html +++ b/src/app/views/querying/gis/components/layers/map-layers.component.html @@ -21,6 +21,13 @@
    + - + + + + + + + + + + +
    diff --git a/src/app/views/querying/gis/components/configuration/filter/filter.component.ts b/src/app/views/querying/gis/components/configuration/filter/filter.component.ts index d6ed59a3..4e3439e2 100644 --- a/src/app/views/querying/gis/components/configuration/filter/filter.component.ts +++ b/src/app/views/querying/gis/components/configuration/filter/filter.component.ts @@ -33,4 +33,11 @@ export class FilterComponent implements MapLayerConfigurationComponent { this.layerSettings.addPolygonFilterForLayer(this.config.layer); } } + + applyFilterPolygonToQuery() { + + + // const polygon = this.config.layer.tempPolygon; + // const planNode = this.config.layer.planNode; + } } diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.html b/src/app/views/querying/gis/components/layers/map-layers.component.html index 88a0bffb..22eba498 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.html +++ b/src/app/views/querying/gis/components/layers/map-layers.component.html @@ -73,7 +73,10 @@
    - + +
    + Add layer

    + +
    + +
    + diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index a04d36bd..534bd26d 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -1,10 +1,16 @@ import { - AfterViewInit, Component, effect, + AfterViewInit, + Component, + effect, ElementRef, HostListener, - inject, OnDestroy, + inject, + OnDestroy, OnInit, - Renderer2, signal, ViewChild, WritableSignal + Renderer2, + signal, + ViewChild, + WritableSignal } from '@angular/core'; import {LayerContext} from '../../models/LayerContext.model'; import {LayerSettingsService} from '../../services/layersettings.service'; @@ -13,24 +19,24 @@ import * as GeoJSON from 'geojson'; import {FeatureCollection} from 'geojson'; import {MapLayer} from '../../models/MapLayer.model'; import isEqual from 'lodash/isEqual'; -import { - CdkDragDrop, - moveItemInArray, -} from '@angular/cdk/drag-drop'; +import {CdkDragDrop, moveItemInArray,} from '@angular/cdk/drag-drop'; // noinspection ES6UnusedImports import {getSampleMapLayers} from '../../models/get-sample-maplayers'; import {CrudService} from '../../../../../services/crud.service'; import {WebSocket} from '../../../../../services/webSocket'; -import {Result} from '../../../../../components/data-view/models/result-set.model'; +import {RelationalResult, Result} from '../../../../../components/data-view/models/result-set.model'; import {WebuiSettingsService} from '../../../../../services/webui-settings.service'; import {Subscription} from 'rxjs'; -import {QueryRequest} from '../../../../../models/ui-request.model'; +import {DataModel, PolyAlgRequest, QueryRequest} from '../../../../../models/ui-request.model'; import {CombinedResult} from '../../../../../components/data-view/data-view.model'; import {CatalogService} from '../../../../../services/catalog.service'; import {QueryEditor} from '../../../console/components/code-editor/query-editor.component'; -import {AlgValidatorService} from '../../../../../components/polyalg/polyalg-viewer/alg-validator.service'; +import {AlgValidatorService, trimLines} from '../../../../../components/polyalg/polyalg-viewer/alg-validator.service'; import {SidebarNode} from '../../../../../models/sidebar-node.model'; import {InformationGroup, InformationPage} from '../../../../../models/information-page.model'; +import {AlgViewerComponent} from '../../../../../components/polyalg/polyalg-viewer/alg-viewer.component'; +import {geojsonToWKT} from '@terraformer/wkt'; + interface BaseLayer { name: string; @@ -74,6 +80,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { for (const informationObject of informationObjects) { if (informationObject.type === 'InformationPolyAlg') { queryLayer.jsonPolyAlg = informationObject.jsonPolyAlg; + queryLayer.planNode = JSON.parse(informationObject.jsonPolyAlg); } } } @@ -108,6 +115,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { public readonly _validator = inject(AlgValidatorService); @ViewChild(QueryEditor) queryEditor!: QueryEditor; + @ViewChild(AlgViewerComponent) algViewerComponent!: AlgViewerComponent; // Querying websocket: WebSocket; @@ -118,6 +126,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { private addDataToExistingLayer: MapLayer = null; private lastQueryAnalyzerId = null; private lastQueryAnalyzerPage = null; + private applyFilterToLayer: MapLayer = null; protected baseLayers: BaseLayer[] = [ { @@ -159,6 +168,40 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { protected readonly queryLanguages = ['CYPHER', 'SQL', 'MQL']; readonly activeNamespace: WritableSignal = signal(null); + runPolyPlan(layer: MapLayer) { + this.applyFilterToLayer = layer; + this.algViewerComponent.setPolyAlgPlan(layer.planNode, 'LOGICAL'); + } + + onPolyPlanChanged(polyPlan: string) { + if (!this.applyFilterToLayer || !polyPlan) { + return; + } + + console.log('onPolyPlanChanged', polyPlan, this.applyFilterToLayer); + + let plan = ''; + + if (this.applyFilterToLayer.language === 'mongo') { + const wkt = geojsonToWKT(this.applyFilterToLayer.tempPolygon); + plan = `DOC_FILTER[MQL_GEO_WITHIN( + geometry, + SRID=4326;${wkt}:DOCUMENT, -1.0E0:DOCUMENT)]( + ${polyPlan})`; + plan = trimLines(plan); + } + + plan = `DOC_FILTER[MQL_GEO_WITHIN(geometry, SRID=4326;POLYGON ((12.535400390625002 52.92215137976296, 13.458251953125002 51.15178610143037, 15.128173828125002 51.41291212935532, 14.930419921875002 53.553362785528094, 13.348388671875002 53.6185793648952, 12.535400390625002 52.92215137976296)):DOCUMENT, -1.0E0:DOCUMENT)]( + DOC_SCAN[doc.geocollection2] +)`; + + console.log('Run plan:', plan); + const request = new PolyAlgRequest(plan, DataModel.DOCUMENT, 'LOGICAL'); + request.noLimit = true; + const success = this.websocket.sendMessage(request); + console.log('success', success); + } + ngOnDestroy(): void { // This component is only destroyed once we navigate away from the Map-Based Query Mode. In this case, // we want to remove everything that belongs to this session. diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index 9f930477..1d1d4ebb 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -28,6 +28,7 @@ export class MapLayer { data: MapGeometryWithData[] = []; containsPoints = false; containsAreas = false; + containsData = false; // Query isQueryLayer = false; @@ -304,6 +305,9 @@ export class MapLayer { addData(data: MapGeometryWithData[]) { this.containsPoints = false; this.containsAreas = false; + if (data.length > 0){ + this.containsData = Object.keys(data[0].data).length !== 0; + } // Remove everything that does not have a geometry. data = data.filter((d) => d.geometry !== null); From 9a3d9c40eb56168a079622cc44355e7c2cccc25d Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Sun, 15 Dec 2024 22:55:14 +0100 Subject: [PATCH 47/60] GIS: Add geometryField to layer, to know which field to use in filter --- .../components/layers/map-layers.component.ts | 8 +-- .../querying/gis/models/MapLayer.model.ts | 68 +++++++++++-------- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index 534bd26d..89f992fb 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -185,15 +185,15 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { if (this.applyFilterToLayer.language === 'mongo') { const wkt = geojsonToWKT(this.applyFilterToLayer.tempPolygon); plan = `DOC_FILTER[MQL_GEO_WITHIN( - geometry, + ${this.applyFilterToLayer.geometryField}, SRID=4326;${wkt}:DOCUMENT, -1.0E0:DOCUMENT)]( ${polyPlan})`; plan = trimLines(plan); } - plan = `DOC_FILTER[MQL_GEO_WITHIN(geometry, SRID=4326;POLYGON ((12.535400390625002 52.92215137976296, 13.458251953125002 51.15178610143037, 15.128173828125002 51.41291212935532, 14.930419921875002 53.553362785528094, 13.348388671875002 53.6185793648952, 12.535400390625002 52.92215137976296)):DOCUMENT, -1.0E0:DOCUMENT)]( - DOC_SCAN[doc.geocollection2] -)`; +// plan = `DOC_FILTER[MQL_GEO_WITHIN(geometry, SRID=4326;POLYGON ((12.535400390625002 52.92215137976296, 13.458251953125002 51.15178610143037, 15.128173828125002 51.41291212935532, 14.930419921875002 53.553362785528094, 13.348388671875002 53.6185793648952, 12.535400390625002 52.92215137976296)):DOCUMENT, -1.0E0:DOCUMENT)]( +// DOC_SCAN[doc.geocollection2] +// )`; console.log('Run plan:', plan); const request = new PolyAlgRequest(plan, DataModel.DOCUMENT, 'LOGICAL'); diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index 1d1d4ebb..acfea5dd 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -33,6 +33,7 @@ export class MapLayer { // Query isQueryLayer = false; query = null; + geometryField = null; language = null; namespace = null; lastUpdated = ''; @@ -77,6 +78,7 @@ export class MapLayer { layer.namespace = result.namespace; layer.isQueryLayer = true; const mapData = []; + let geometryField = undefined; switch (result.dataModel) { case DataModel.DOCUMENT: @@ -85,7 +87,8 @@ export class MapLayer { const jsonObject = Object.fromEntries( Object.entries(JSON.parse(json)).map(([key, value]) => [key.toLowerCase(), value]) ); - const geometry = this.getGeometryFromData(jsonObject); + const [geometry, key] = this.getGeometryFromData(jsonObject); + geometryField = key; if (geometry) { const geometryWithData = new MapGeometryWithData(rowIndex, geometry, jsonObject); mapData.push(geometryWithData); @@ -113,7 +116,8 @@ export class MapLayer { } } - const geometry = this.getGeometryFromData(obj); + const [geometry, key] = this.getGeometryFromData(obj); + geometryField = key; if (geometry) { const geometryWithData = new MapGeometryWithData(rowIndex, geometry, obj); mapData.push(geometryWithData); @@ -144,7 +148,8 @@ export class MapLayer { } } - const geometry = this.getGeometryFromData(obj); + const [geometry, key] = this.getGeometryFromData(obj); + geometryField = key; if (geometry) { const geometryWithData = new MapGeometryWithData(rowIndex, geometry, obj); mapData.push(geometryWithData); @@ -157,45 +162,48 @@ export class MapLayer { } layer.addData(mapData); + layer.geometryField = geometryField; console.log('Created layer: ', layer); return layer; } - static getGeometryFromData(data: Record): Geometry | undefined { + static getGeometryFromData(data: Record): [Geometry, string] | undefined { // Detect GeoJSON objects for (const key in data) { if (data.hasOwnProperty(key) && this.isGeoJSON(data[key])) { - return data[key]; + return [data[key], key]; } } + // TODO: If we do this, we cannot filter the layer, because we do not have a single field + // that we can use in the logical plan to reference the coordinates. // Detect 2 columns that store latitude / longitude coordinates - const latLong = [ - ['lat', 'lon'], - ['latitude', 'longitude'], - ['lati', 'long'], - ]; - const isNumber = (value: any): boolean => { - return typeof value === 'number' && !isNaN(value); - }; - - for (const ll of latLong) { - const lat = ll[0]; - const lon = ll[1]; - - if ( - data.hasOwnProperty(lat) && - data.hasOwnProperty(lon) && - isNumber(data[lat]) && - isNumber(data[lon]) - ) { - return { - type: 'Point', - coordinates: [data[lon], data[lat]], - }; - } - } + // const latLong = [ + // ['lat', 'lon'], + // ['latitude', 'longitude'], + // ['lati', 'long'], + // ]; + // const isNumber = (value: any): boolean => { + // return typeof value === 'number' && !isNaN(value); + // }; + // + // for (const ll of latLong) { + // const lat = ll[0]; + // const lon = ll[1]; + // + // if ( + // data.hasOwnProperty(lat) && + // data.hasOwnProperty(lon) && + // isNumber(data[lat]) && + // isNumber(data[lon]) + // ) { + // return { + // type: 'Point', + // coordinates: [data[lon], data[lat]], + // }; + // } + // } // TODO: Detect heuristic, so that we can automatically detect the most common geometry types // - string in WKT format From 0a729cd44b757b76e0a482142c195d4d330b7262 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Mon, 16 Dec 2024 18:28:11 +0100 Subject: [PATCH 48/60] GIS: Unify rendering method + implement square render function + fix order between layers + UX improvements --- .../data-view/data-map/data-map.component.ts | 217 ++++++++---------- .../layers/map-layers.component.html | 14 +- .../layers/map-layers.component.scss | 4 + .../color-visualization-model.ts | 7 +- .../visualization/color/color.component.html | 6 +- .../point-shape-visualization.model.ts | 5 +- .../gis/models/MapGeometryWithData.model.ts | 2 + .../querying/gis/models/MapLayer.model.ts | 4 +- 8 files changed, 121 insertions(+), 138 deletions(-) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index 9edbc3ac..4ea67e25 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -44,14 +44,9 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView | d3.Selection | undefined; private g: d3.Selection | undefined; - private circles: - | d3.Selection - | undefined; - private paths: - | d3.Selection - | undefined; private pathGenerator!: GeoPath; private tooltip!: d3.Selection; + private renderedGeometries: d3.Selection; constructor(protected layerSettings: LayerSettingsService) { super(); @@ -222,6 +217,11 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView const transform = d3Geo.geoTransform({point: projectPoint}); this.pathGenerator = d3Geo.geoPath().projection(transform); + this.map.on('zoomstart', () => { + if (this.g) { + this.g.style('visibility', 'hidden'); + } + }); this.map.on('zoomend', () => { this.updateSvgPosition(); }); @@ -231,9 +231,6 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView } const bounds = this.map.getBounds(); const topLeft = this.map.latLngToLayerPoint(bounds.getNorthWest()); - const bottomRight = this.map.latLngToLayerPoint( - bounds.getSouthEast(), - ); this.svg .style('width', '999999px') .style('height', '999999px') @@ -266,31 +263,26 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView return; } - if (this.paths) { - this.paths.attr('d', (d) => this.pathGenerator(d.geometry)); - } - - if (this.circles) { - this.circles - .each((d) => { - const layerPoint = this.map.latLngToLayerPoint([ - d.getPoint()!.coordinates[1], - d.getPoint()!.coordinates[0], - ]); - d.cache['x'] = layerPoint.x; - d.cache['y'] = layerPoint.y; - }) - .attr('cx', (d) => d.cache['x']) - .attr('cy', (d) => d.cache['y']); - } + this.renderedGeometries + .filter((d) => d.type === 'path') + .attr('d', (d) => this.pathGenerator(d.geometry)); + this.renderedGeometries + .filter((d) => d.type === 'point') + .each((d) => { + const layerPoint = this.map.latLngToLayerPoint([ + d.getPoint().coordinates[1], + d.getPoint().coordinates[0], + ]); + d.cache['x'] = layerPoint.x; + d.cache['y'] = layerPoint.y; + }) + .attr('cx', (d) => d.cache['x']) + .attr('cy', (d) => d.cache['y']); const bounds = this.map.getBounds(); const topLeft = this.map.latLngToLayerPoint( bounds.getNorthWest(), ); - const bottomRight = this.map.latLngToLayerPoint( - bounds.getSouthEast(), - ); this.svg .style('width', '999999px') .style('height', '999999px') @@ -300,6 +292,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView 'transform', `translate(${-topLeft.x}, ${-topLeft.y})`, ); + this.g.style('visibility', 'visible'); } finally { this.isLoading = false; } @@ -318,34 +311,17 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView // Remove all previously added elements this.g.selectAll('*').remove(); - - const points: MapGeometryWithData[] = []; - const paths: MapGeometryWithData[] = []; - - // Add shapes from each layer to array + const geometries : MapGeometryWithData[] = []; for (const layer of this.layers.slice().reverse()) { - // Initialize all configs layer.pointShapeVisualization.init(layer.data); layer.areaShapeVisualization.init(layer.data); layer.colorVisualization.init(layer.data); - - points.push( - ...layer.data.filter( - (d) => d.geometry.type === 'Point', - ), - ); - paths.push( - ...layer.data.filter( - (d) => d.geometry.type !== 'Point', - ), - ); + geometries.push(...layer.data); } - // Render all points - // TODO: Circles are always on the bottom this way... - this.circles = this.createPoints(points); - this.paths = this.createPaths(paths); + this.renderedGeometries = this.renderGeometries(geometries); + console.log(`renderedGeometries.size()`, this.renderedGeometries.size()); // Set SVG position correctly this.updateSvgPosition(); @@ -363,85 +339,90 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView }, 0); } - createPoints(points: MapGeometryWithData[]) { + renderGeometries(geometries: (MapGeometryWithData)[]) { if (!this.g) { return; } const tt = this.tooltip; + const pathGenerator = this.pathGenerator; + const map = this.map; return this.g - .selectAll('circle') - .data(points) + .selectAll('.geometry') + .data(geometries) .enter() - .append('circle') - .attr('layer-id', (d) => d.layer!.uuid) - .attr('layer-index', (d) => d.layer!.index.toString()) - .attr('r', (d) => - d.layer!.pointShapeVisualization.getValueForAttribute('r', d), - ) - .attr('fill', (d) => - d.layer!.colorVisualization.getValueForAttribute('fill', d), - ) - .each((d) => { - const layerPoint = this.map.latLngToLayerPoint([ - d.getPoint().coordinates[1], - d.getPoint().coordinates[0], - ]); - d.cache['x'] = layerPoint.x; - d.cache['y'] = layerPoint.y; - }) - .attr('cx', (d) => d.cache['x']) - .attr('cy', (d) => d.cache['y']) - .style('pointer-events', 'auto') - .style('cursor', 'pointer') - .on('mouseover', function (event, d) { - tt.style('display', 'block').html(JSON.stringify(d.data, null, 1)); - }) - .on('mousemove', function (event) { - tt.style('top', event.pageY + 10 + 'px').style( - 'left', - event.pageX + 10 + 'px', - ); - }) - .on('mouseout', function () { - tt.style('display', 'none'); - }); - } - - createPaths(paths: MapGeometryWithData[]) { - if (!this.g) { - return; - } - - const tt = this.tooltip; + .append(function (d) { + console.log("d.type", d.type); + console.log("d.layer!.pointShapeVisualization.selectedMode", d.layer!.pointShapeVisualization.selectedMode); + if (d.type === 'point' && d.layer!.pointShapeVisualization.selectedMode === 'Circle'){ + return document.createElementNS('http://www.w3.org/2000/svg', 'circle') + } - return this.g - .selectAll('.paths') - .data(paths) - .enter() - .append('path') + // All other more complex shapes need to be created by using paths. + return document.createElementNS('http://www.w3.org/2000/svg', 'path') + }) + .attr('class', 'geometry') .attr('layer-id', (d) => d.layer!.uuid) .attr('layer-index', (d) => d.layer!.index.toString()) - .attr('d', (d) => this.pathGenerator(d.geometry)) - .attr('stroke-width', (d) => - d.layer!.areaShapeVisualization.getValueForAttribute( - 'stroke-width', - d, - ), - ) - .attr('stroke', (d) => - d.layer!.colorVisualization.getValueForAttribute('stroke', d), - ) - .attr('fill', (d) => - d.layer!.colorVisualization.getValueForAttribute('fill', d), - ) - .attr('fill-opacity', (d) => - d.layer!.colorVisualization.getValueForAttribute( - 'fill-opacity', - d, - ), - ) + .each(function (d) { + const element = d3.select(this); + switch(d.type){ + case "point": + const layerPoint = map.latLngToLayerPoint([ + d.getPoint().coordinates[1], + d.getPoint().coordinates[0], + ]); + const x = layerPoint.x; + const y = layerPoint.y; + const size = d.layer!.pointShapeVisualization.getValueForAttribute('r', d) as number; + const fill = d.layer!.colorVisualization.getValueForAttribute('fill', d) as string; + d.cache['x'] = x; + d.cache['y'] = y; + + if (d.layer!.pointShapeVisualization.selectedMode === "Icon"){ + // const icon = d.layer!.data[d.layer!.pointShapeVisualization.fieldName]; + // TODO: Lookup value of selected field to determine icon + const icon = 'square' + + switch (icon){ + case 'square': + element + .attr('d', (d) => { + const halfSize = size; + return `M ${x - halfSize} ${y - halfSize} + L ${x + halfSize} ${y - halfSize} + L ${x + halfSize} ${y + halfSize} + L ${x - halfSize} ${y + halfSize} Z`; + }) + .attr('fill', fill); + break + default: + throw new Error(`Update render function to include shape [${d.type}]`); + + } + } else { + // Shape is set to circle, create circle element. + element + .attr('cx', x) + .attr('cy', y) + .attr('r', size) + .attr('fill', fill); + } + + break; + case "path": + element + .attr('d', pathGenerator(d.geometry)) + .attr('stroke-width', d.layer!.areaShapeVisualization.getValueForAttribute('stroke-width', d)) + .attr('stroke', d.layer!.colorVisualization.getValueForAttribute('stroke', d)) + .attr('fill', d.layer!.colorVisualization.getValueForAttribute('fill', d)) + .attr('fill-opacity', d.layer!.colorVisualization.getValueForAttribute('fill-opacity', d)); + break; + default: + throw new Error(`Update render function to include shape [${d.type}]`); + } + }) .style('pointer-events', 'auto') .style('cursor', 'pointer') .on('mouseover', function (event, d) { diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.html b/src/app/views/querying/gis/components/layers/map-layers.component.html index 22eba498..2ee332c5 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.html +++ b/src/app/views/querying/gis/components/layers/map-layers.component.html @@ -58,18 +58,10 @@
    - Elements: {{ layer.data.length }} + {{ layer.query === null ? 'FILE' : 'QUERY' }}
    - {{ layer.name }} - - - - - - - - + {{ layer.name }}
    @@ -77,7 +69,7 @@
    ; - constructor(color: string) { + constructor(color: string, layer: MapLayer) { this.color = color; + this.layer = layer; } init(data: MapGeometryWithData[]): void { @@ -62,7 +65,7 @@ export class ColorVisualization implements Visualization, MapLayerConfiguration } copy(): Visualization { - const copy = new ColorVisualization(this.color); + const copy = new ColorVisualization(this.color, this.layer); copy.selectedMode = this.selectedMode; copy.fillOpacity = this.fillOpacity; copy.fieldName = this.fieldName; diff --git a/src/app/views/querying/gis/components/visualization/color/color.component.html b/src/app/views/querying/gis/components/visualization/color/color.component.html index 5a4e1174..f58388ba 100644 --- a/src/app/views/querying/gis/components/visualization/color/color.component.html +++ b/src/app/views/querying/gis/components/visualization/color/color.component.html @@ -24,9 +24,9 @@ /> - - Area Fill Opacity - + Opacity (Area) + = {}; cache: Record = {}; layer?: MapLayer = undefined; + type: 'point' | 'path'; constructor( index: number, @@ -21,6 +22,7 @@ export class MapGeometryWithData { ) { this.index = index; this.geometry = geometry; + this.type = this.isPoint() ? 'point' : 'path'; if (data) { this.data = data; diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index acfea5dd..5f001190 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -60,9 +60,9 @@ export class MapLayer { dataPreview: MapLayerConfiguration = new DataPreview(this); filterConfig: FilterConfig = new FilterConfig(this); - pointShapeVisualization: Visualization = new PointShapeVisualization(3); + pointShapeVisualization: PointShapeVisualization = new PointShapeVisualization(3); areaShapeVisualization: Visualization = new AreaShapeVisualization(1); - colorVisualization: Visualization = new ColorVisualization('red'); + colorVisualization: Visualization = new ColorVisualization('red', this); labelVisualization: Visualization = new LabelVisualization(); // Computed (Not used in copy) From 214a6ad03389608a3245aa2ce037efbdf4a8d420 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Mon, 16 Dec 2024 21:07:09 +0100 Subject: [PATCH 49/60] Update PointShapeConfig to render Triangle, Star or Square --- .../data-view/data-map/data-map.component.ts | 177 ++++++++++++------ .../visualization/color/color.component.html | 11 +- .../point-shape-visualization.model.ts | 10 +- .../point-shape/point-shape.component.html | 18 +- 4 files changed, 140 insertions(+), 76 deletions(-) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index 4ea67e25..cc276e4e 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -9,8 +9,9 @@ import {LayerSettingsService} from '../../../views/querying/gis/services/layerse import {MapLayer} from '../../../views/querying/gis/models/MapLayer.model'; import {MapGeometryWithData} from '../../../views/querying/gis/models/MapGeometryWithData.model'; import {CombinedResult} from '../data-view.model'; -import {LatLng} from "leaflet"; -import {Polygon, Position} from "geojson"; +import {LatLng} from 'leaflet'; +import {Polygon, Position} from 'geojson'; +import {cibYarn} from '@coreui/icons'; // tslint:disable:no-non-null-assertion @@ -32,7 +33,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView // leaflet-draw leafletDrawControl = null; - layerIdToPoylgon = new Map() + layerIdToPoylgon = new Map(); currentDrawingLayer: MapLayer = null; polygonTool = null; @@ -53,7 +54,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView effect(() => { if (!this.isInsideResults) { - return + return; } // Display the results. @@ -103,7 +104,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView this.subscriptions.add(this.layerSettings.fitLayerToMap$.subscribe((layer) => { if (layer) { - this.fitLayerToMap(layer) + this.fitLayerToMap(layer); } })); @@ -111,7 +112,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView } fitLayerToMap(layer: MapLayer) { - const bounds = layer.getBounds() + const bounds = layer.getBounds(); if (bounds.length > 0) { const latLngBounds = L.latLngBounds(bounds); if (latLngBounds.isValid()) { @@ -147,26 +148,26 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView leafletMap.on(L.Draw.Event.CREATED, (event) => { if (this.currentDrawingLayer === null) { - throw new Error("We are drawing, but there is no layer set?") + throw new Error('We are drawing, but there is no layer set?'); } const polygon: L.Polygon = event.layer; this.layerIdToPoylgon.set(this.currentDrawingLayer.uuid, polygon); - console.log("Polygon added for layer ", this.currentDrawingLayer.uuid, polygon) - leafletMap.removeControl(this.leafletDrawControl) - this.polygonTool.disable() + console.log('Polygon added for layer ', this.currentDrawingLayer.uuid, polygon); + leafletMap.removeControl(this.leafletDrawControl); + this.polygonTool.disable(); drawnItems.addLayer(polygon); // Send coordinates back to map-layers - let coordinates = polygon.getLatLngs() as LatLng[][]; - let positions: Position[][] = coordinates.map((ring) => + const coordinates = polygon.getLatLngs() as LatLng[][]; + const positions: Position[][] = coordinates.map((ring) => ring.map((c) => [c.lng, c.lat]) ); // Close the linear ring positions[0].push(positions[0][0]); const geoJsonPolygon: Polygon = { - type: "Polygon", + type: 'Polygon', coordinates: positions, - } + }; this.layerSettings.addPolygonToLayer(this.currentDrawingLayer, geoJsonPolygon); }); @@ -185,13 +186,10 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView if (mapLayer === null) { return; } - console.log("data-map remove layer filter polygon for ", mapLayer, this.layerIdToPoylgon); - console.trace(); if (!this.layerIdToPoylgon.has(mapLayer.uuid)) { return; } const polygon = this.layerIdToPoylgon.get(mapLayer.uuid); - console.log("Remove polygon", polygon); drawnItems.removeLayer(polygon); })); @@ -263,11 +261,13 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView return; } + // GeoJSON paths this.renderedGeometries .filter((d) => d.type === 'path') .attr('d', (d) => this.pathGenerator(d.geometry)); + // Circles this.renderedGeometries - .filter((d) => d.type === 'point') + .filter((d) => d.type === 'point' && d.layer!.pointShapeVisualization.selectedMode === 'Circle') .each((d) => { const layerPoint = this.map.latLngToLayerPoint([ d.getPoint().coordinates[1], @@ -278,6 +278,33 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView }) .attr('cx', (d) => d.cache['x']) .attr('cy', (d) => d.cache['y']); + // Manually created Paths: Squares, Triangles, Stars + const createTrianglePath = this.createTrianglePath; + const createStarPath = this.createStarPath; + const createSquarePath = this.createSquarePath; + + this.renderedGeometries + .filter((d) => d.type === 'point' && d.layer!.pointShapeVisualization.selectedMode !== 'Circle') + .each((d) => { + const layerPoint = this.map.latLngToLayerPoint([ + d.getPoint().coordinates[1], + d.getPoint().coordinates[0], + ]); + d.cache['x'] = layerPoint.x; + d.cache['y'] = layerPoint.y; + }) + .attr('d', (d) => { + switch (d.layer!.pointShapeVisualization.selectedMode) { + case 'Triangle': + return createTrianglePath(d.cache['x'], d.cache['y'], d.cache['size']); + case 'Star': + return createStarPath(d.cache['x'], d.cache['y'], d.cache['size']); + case 'Square': + return createSquarePath(d.cache['x'], d.cache['y'], d.cache['size']); + default: + throw new Error(`Update SVG-Repositioning function to include point shape [${d.layer!.pointShapeVisualization.selectedMode}]`); + } + }); const bounds = this.map.getBounds(); const topLeft = this.map.latLngToLayerPoint( @@ -300,7 +327,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView } renderLayersWithD3() { - console.log("Map: Render layers...") + console.log('Map: Render layers...'); this.showLoadingSpinner('Rendering layers'); setTimeout(() => { @@ -311,7 +338,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView // Remove all previously added elements this.g.selectAll('*').remove(); - const geometries : MapGeometryWithData[] = []; + const geometries: MapGeometryWithData[] = []; for (const layer of this.layers.slice().reverse()) { // Initialize all configs layer.pointShapeVisualization.init(layer.data); @@ -321,7 +348,6 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView } this.renderedGeometries = this.renderGeometries(geometries); - console.log(`renderedGeometries.size()`, this.renderedGeometries.size()); // Set SVG position correctly this.updateSvgPosition(); @@ -329,8 +355,8 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView // Only for the first layer we add: Adjust map to points from layer. // Otherwise: Maybe the user is already looking at the data they are interested in -> do not change // map position, because it could be unwanted. - if (this.layers.length == 1) { - this.fitLayerToMap(this.layers[0]) + if (this.layers.length === 1) { + this.fitLayerToMap(this.layers[0]); } } finally { @@ -347,28 +373,29 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView const tt = this.tooltip; const pathGenerator = this.pathGenerator; const map = this.map; + const createTrianglePath = this.createTrianglePath; + const createStarPath = this.createStarPath; + const createSquarePath = this.createSquarePath; return this.g .selectAll('.geometry') .data(geometries) .enter() .append(function (d) { - console.log("d.type", d.type); - console.log("d.layer!.pointShapeVisualization.selectedMode", d.layer!.pointShapeVisualization.selectedMode); - if (d.type === 'point' && d.layer!.pointShapeVisualization.selectedMode === 'Circle'){ - return document.createElementNS('http://www.w3.org/2000/svg', 'circle') + if (d.type === 'point' && d.layer!.pointShapeVisualization.selectedMode === 'Circle') { + return document.createElementNS('http://www.w3.org/2000/svg', 'circle'); } // All other more complex shapes need to be created by using paths. - return document.createElementNS('http://www.w3.org/2000/svg', 'path') + return document.createElementNS('http://www.w3.org/2000/svg', 'path'); }) .attr('class', 'geometry') .attr('layer-id', (d) => d.layer!.uuid) .attr('layer-index', (d) => d.layer!.index.toString()) .each(function (d) { const element = d3.select(this); - switch(d.type){ - case "point": + switch (d.type) { + case 'point': const layerPoint = map.latLngToLayerPoint([ d.getPoint().coordinates[1], d.getPoint().coordinates[0], @@ -377,41 +404,41 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView const y = layerPoint.y; const size = d.layer!.pointShapeVisualization.getValueForAttribute('r', d) as number; const fill = d.layer!.colorVisualization.getValueForAttribute('fill', d) as string; + d.cache['x'] = x; d.cache['y'] = y; + d.cache['size'] = size; + + switch (d.layer!.pointShapeVisualization.selectedMode) { + case 'Circle': + element + .attr('cx', x) + .attr('cy', y) + .attr('r', size) + .attr('fill', fill); + break; + case 'Square': + element + .attr('d', (d) => createSquarePath(x, y, size)) + .attr('fill', fill); + break; + case 'Triangle': + element + .attr('d', () => createTrianglePath(x, y, size)) + .attr('fill', d.layer!.colorVisualization.getValueForAttribute('fill', d)); + break; + case 'Star': + element + .attr('d', (d) => createStarPath(x, y, size)) + .attr('fill', d.layer!.colorVisualization.getValueForAttribute('fill', d)); + break; + default: + throw new Error(`Update render function to include shape [${d.type}]`); - if (d.layer!.pointShapeVisualization.selectedMode === "Icon"){ - // const icon = d.layer!.data[d.layer!.pointShapeVisualization.fieldName]; - // TODO: Lookup value of selected field to determine icon - const icon = 'square' - - switch (icon){ - case 'square': - element - .attr('d', (d) => { - const halfSize = size; - return `M ${x - halfSize} ${y - halfSize} - L ${x + halfSize} ${y - halfSize} - L ${x + halfSize} ${y + halfSize} - L ${x - halfSize} ${y + halfSize} Z`; - }) - .attr('fill', fill); - break - default: - throw new Error(`Update render function to include shape [${d.type}]`); - - } - } else { - // Shape is set to circle, create circle element. - element - .attr('cx', x) - .attr('cy', y) - .attr('r', size) - .attr('fill', fill); } break; - case "path": + case 'path': element .attr('d', pathGenerator(d.geometry)) .attr('stroke-width', d.layer!.areaShapeVisualization.getValueForAttribute('stroke-width', d)) @@ -439,6 +466,38 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView }); } + createTrianglePath(x, y, size) { + const side = size * 3; + const height = (Math.sqrt(3) / 2) * side; + return `M ${x} ${y - height / 2} + L ${x - side / 2} ${y + height / 2} + L ${x + side / 2} ${y + height / 2} Z`; + } + + createStarPath(x, y, size) { + const outerRadius = size * 2; + const innerRadius = size; + const spikes = 5; + + let path = ''; + const step = Math.PI / spikes; + for (let i = 0; i < 2 * spikes; i++) { + const radius = i % 2 === 0 ? outerRadius : innerRadius; + const xStar = x + Math.cos(i * step) * radius; + const yStar = y - Math.sin(i * step) * radius; + path += i === 0 ? `M ${xStar} ${yStar}` : `L ${xStar} ${yStar}`; + } + return `${path} Z`; + } + + createSquarePath(x, y, size) { + const halfSize = size; + return `M ${x - halfSize} ${y - halfSize} + L ${x + halfSize} ${y - halfSize} + L ${x + halfSize} ${y + halfSize} + L ${x - halfSize} ${y + halfSize} Z`; + } + toggleLayerVisibility(layer: MapLayer) { if (!this.g?.node()) { throw new Error('SVG g does not exist.'); diff --git a/src/app/views/querying/gis/components/visualization/color/color.component.html b/src/app/views/querying/gis/components/visualization/color/color.component.html index f58388ba..d7e9f796 100644 --- a/src/app/views/querying/gis/components/visualization/color/color.component.html +++ b/src/app/views/querying/gis/components/visualization/color/color.component.html @@ -46,10 +46,11 @@ /> - - - - + + + + + + diff --git a/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts b/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts index 8a3780bf..1ab7e403 100644 --- a/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts +++ b/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts @@ -7,10 +7,14 @@ export class PointShapeVisualization implements Visualization, MapLayerConfigura name = 'Point Shape'; configurationComponentType = PointShapeComponent; - modes: string[] = ['Circle', 'Icon']; + modes: string[] = ['Circle', 'Square', 'Triangle', 'Star']; selectedMode: string; size: number; - fieldName = ''; + + // TODO: Currently deactivated, because it required too much client-side logic. It is easier to just apply + // the filter when specifying the query, and then applying the selectedMode to the whole layer, instead + // of comparing the field here. + // fieldName = ''; constructor(size: number) { this.size = size; @@ -24,7 +28,7 @@ export class PointShapeVisualization implements Visualization, MapLayerConfigura copy(): PointShapeVisualization { const copy = new PointShapeVisualization(this.size); copy.selectedMode = this.selectedMode; - copy.fieldName = this.fieldName; + // copy.fieldName = this.fieldName; return copy; } diff --git a/src/app/views/querying/gis/components/visualization/point-shape/point-shape.component.html b/src/app/views/querying/gis/components/visualization/point-shape/point-shape.component.html index 63842066..4b80e577 100644 --- a/src/app/views/querying/gis/components/visualization/point-shape/point-shape.component.html +++ b/src/app/views/querying/gis/components/visualization/point-shape/point-shape.component.html @@ -24,14 +24,14 @@ - - Variable - - + + + + + + + + + From a80973a5c611ae151433f4f503a9dcaefe1ac7ae Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Mon, 16 Dec 2024 21:35:16 +0100 Subject: [PATCH 50/60] GIS: Fix rendering of square shape --- .../data-view/data-map/data-map.component.ts | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index cc276e4e..c0a117dd 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -36,6 +36,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView layerIdToPoylgon = new Map(); currentDrawingLayer: MapLayer = null; polygonTool = null; + isInitialRender = true; readonly MIN_ZOOM = 0; readonly MAX_ZOOM = 19; @@ -355,12 +356,14 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView // Only for the first layer we add: Adjust map to points from layer. // Otherwise: Maybe the user is already looking at the data they are interested in -> do not change // map position, because it could be unwanted. - if (this.layers.length === 1) { + // Only do this for the + if (this.isInitialRender && this.layers.length === 1) { this.fitLayerToMap(this.layers[0]); } } finally { this.isLoading = false; + this.isInitialRender = false; } }, 0); } @@ -393,7 +396,9 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView .attr('layer-id', (d) => d.layer!.uuid) .attr('layer-index', (d) => d.layer!.index.toString()) .each(function (d) { - const element = d3.select(this); + const element = d3.select(this) + const fill = d.layer!.colorVisualization.getValueForAttribute('fill', d) as string; + switch (d.type) { case 'point': const layerPoint = map.latLngToLayerPoint([ @@ -403,8 +408,6 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView const x = layerPoint.x; const y = layerPoint.y; const size = d.layer!.pointShapeVisualization.getValueForAttribute('r', d) as number; - const fill = d.layer!.colorVisualization.getValueForAttribute('fill', d) as string; - d.cache['x'] = x; d.cache['y'] = y; d.cache['size'] = size; @@ -425,25 +428,23 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView case 'Triangle': element .attr('d', () => createTrianglePath(x, y, size)) - .attr('fill', d.layer!.colorVisualization.getValueForAttribute('fill', d)); + .attr('fill', fill); break; case 'Star': element .attr('d', (d) => createStarPath(x, y, size)) - .attr('fill', d.layer!.colorVisualization.getValueForAttribute('fill', d)); + .attr('fill', fill); break; default: throw new Error(`Update render function to include shape [${d.type}]`); - } - break; case 'path': element .attr('d', pathGenerator(d.geometry)) .attr('stroke-width', d.layer!.areaShapeVisualization.getValueForAttribute('stroke-width', d)) .attr('stroke', d.layer!.colorVisualization.getValueForAttribute('stroke', d)) - .attr('fill', d.layer!.colorVisualization.getValueForAttribute('fill', d)) + .attr('fill', fill) .attr('fill-opacity', d.layer!.colorVisualization.getValueForAttribute('fill-opacity', d)); break; default: @@ -491,11 +492,13 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView } createSquarePath(x, y, size) { - const halfSize = size; - return `M ${x - halfSize} ${y - halfSize} - L ${x + halfSize} ${y - halfSize} - L ${x + halfSize} ${y + halfSize} - L ${x - halfSize} ${y + halfSize} Z`; + // No idea why this is necessary, but on the second render the size is suddenly no longer a number + // and gets concatenated instead of added, which leads to the square looking wrong on the map. + size = Number(size); + return `M ${x - size} ${y - size} + L ${x + size} ${y - size} + L ${x + size} ${y + size} + L ${x - size} ${y + size} Z`; } toggleLayerVisibility(layer: MapLayer) { From b69a57ae1f70fb60db1921acf8598fcaf7de0958 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Tue, 17 Dec 2024 18:17:56 +0100 Subject: [PATCH 51/60] GIS: Add cross shape --- .../data-view/data-map/data-map.component.ts | 17 ++++++++++++++++- .../point-shape-visualization.model.ts | 7 +++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index c0a117dd..ab71422f 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -283,6 +283,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView const createTrianglePath = this.createTrianglePath; const createStarPath = this.createStarPath; const createSquarePath = this.createSquarePath; + const createCrossPath = this.createCrossPath; this.renderedGeometries .filter((d) => d.type === 'point' && d.layer!.pointShapeVisualization.selectedMode !== 'Circle') @@ -302,6 +303,8 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView return createStarPath(d.cache['x'], d.cache['y'], d.cache['size']); case 'Square': return createSquarePath(d.cache['x'], d.cache['y'], d.cache['size']); + case 'Cross': + return createCrossPath(d.cache['x'], d.cache['y'], d.cache['size']); default: throw new Error(`Update SVG-Repositioning function to include point shape [${d.layer!.pointShapeVisualization.selectedMode}]`); } @@ -379,6 +382,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView const createTrianglePath = this.createTrianglePath; const createStarPath = this.createStarPath; const createSquarePath = this.createSquarePath; + const createCrossPath = this.createCrossPath; return this.g .selectAll('.geometry') @@ -396,7 +400,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView .attr('layer-id', (d) => d.layer!.uuid) .attr('layer-index', (d) => d.layer!.index.toString()) .each(function (d) { - const element = d3.select(this) + const element = d3.select(this); const fill = d.layer!.colorVisualization.getValueForAttribute('fill', d) as string; switch (d.type) { @@ -435,6 +439,11 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView .attr('d', (d) => createStarPath(x, y, size)) .attr('fill', fill); break; + case 'Cross': + element + .attr('d', (d) => createCrossPath(x, y, size)) + .attr('stroke', fill); + break; default: throw new Error(`Update render function to include shape [${d.type}]`); } @@ -501,6 +510,12 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView L ${x - size} ${y + size} Z`; } + createCrossPath(x, y, size) { + const half = Number(size); + return `M ${x - half} ${y - half} L ${x + half} ${y + half} + M ${x - half} ${y + half} L ${x + half} ${y - half}`; + } + toggleLayerVisibility(layer: MapLayer) { if (!this.g?.node()) { throw new Error('SVG g does not exist.'); diff --git a/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts b/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts index 1ab7e403..e13a0c1e 100644 --- a/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts +++ b/src/app/views/querying/gis/components/visualization/point-shape-visualization.model.ts @@ -3,12 +3,15 @@ import { MapGeometryWithData } from '../../models/MapGeometryWithData.model'; import { PointShapeComponent } from './point-shape/point-shape.component'; import {MapLayerConfiguration} from '../../models/MapLayerConfiguration.interface'; +const modes = ['Circle', 'Square', 'Triangle', 'Star', 'Cross'] as const; // readonly tuple +type Mode = typeof modes[number]; // Union type: 'Circle' | 'Square' | 'Triangle' | 'Star' | 'Cross' + export class PointShapeVisualization implements Visualization, MapLayerConfiguration { name = 'Point Shape'; configurationComponentType = PointShapeComponent; - modes: string[] = ['Circle', 'Square', 'Triangle', 'Star']; - selectedMode: string; + modes = modes; + selectedMode: Mode; size: number; // TODO: Currently deactivated, because it required too much client-side logic. It is easier to just apply From fbccc8332628c48987a373aecc7f47b9a347889e Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Tue, 17 Dec 2024 18:18:48 +0100 Subject: [PATCH 52/60] GIS: Only allow one query (rerun layer / filter layer) at a time --- .../gis/components/layers/map-layers.component.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index 89f992fb..b1adce0d 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -169,7 +169,13 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { readonly activeNamespace: WritableSignal = signal(null); runPolyPlan(layer: MapLayer) { + if (this.applyFilterToLayer || this.addDataToExistingLayer) { + console.log('Another query is already in progress. Wait for it to finish.'); + return; + } + this.applyFilterToLayer = layer; + this.addDataToExistingLayer = layer; this.algViewerComponent.setPolyAlgPlan(layer.planNode, 'LOGICAL'); } @@ -369,6 +375,11 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { } rerunQuery(layer: MapLayer) { + if (this.applyFilterToLayer || this.addDataToExistingLayer) { + console.log('Another query is already in progress. Do not update this.addDataToExistingLayer.'); + return; + } + // Data will be overwritten once the results are in this.addDataToExistingLayer = layer; this.submitQuery(layer.query, layer.language, layer.namespace); From f5e5e1a3d4a905f3984b83ea9789dc18e3a438d6 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Tue, 17 Dec 2024 18:58:06 +0100 Subject: [PATCH 53/60] GIS: Apply filter geometry to layer now works per layer + updates instantly + bug fixed where old layer was still referenced --- .../data-view/data-map/data-map.component.ts | 5 +- .../filter/filter.component.html | 12 +--- .../configuration/filter/filter.component.ts | 11 ++-- .../layers/map-layers.component.html | 3 - .../components/layers/map-layers.component.ts | 63 +++++++++---------- .../querying/gis/models/MapLayer.model.ts | 23 ++----- 6 files changed, 44 insertions(+), 73 deletions(-) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index ab71422f..5afb107f 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -331,7 +331,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView } renderLayersWithD3() { - console.log('Map: Render layers...'); + console.log('Map: Render layers...', this.layers); this.showLoadingSpinner('Rendering layers'); setTimeout(() => { @@ -350,7 +350,8 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView layer.colorVisualization.init(layer.data); geometries.push(...layer.data); } - + + console.log('Render geometries: ', geometries); this.renderedGeometries = this.renderGeometries(geometries); // Set SVG position correctly diff --git a/src/app/views/querying/gis/components/configuration/filter/filter.component.html b/src/app/views/querying/gis/components/configuration/filter/filter.component.html index 5468feac..57078c76 100644 --- a/src/app/views/querying/gis/components/configuration/filter/filter.component.html +++ b/src/app/views/querying/gis/components/configuration/filter/filter.component.html @@ -1,13 +1,3 @@
    - - - - - - - - - - - +
    diff --git a/src/app/views/querying/gis/components/configuration/filter/filter.component.ts b/src/app/views/querying/gis/components/configuration/filter/filter.component.ts index 4e3439e2..b9de5e41 100644 --- a/src/app/views/querying/gis/components/configuration/filter/filter.component.ts +++ b/src/app/views/querying/gis/components/configuration/filter/filter.component.ts @@ -12,11 +12,13 @@ import {FilterConfig} from '../FilterConfig'; }) export class FilterComponent implements MapLayerConfigurationComponent { + isAddMode: boolean; + constructor( @Inject('config') protected config: FilterConfig, private layerSettings: LayerSettingsService, ) { - // + this.updateIsAddMode(); } configChanged() { @@ -34,10 +36,7 @@ export class FilterComponent implements MapLayerConfigurationComponent { } } - applyFilterPolygonToQuery() { - - - // const polygon = this.config.layer.tempPolygon; - // const planNode = this.config.layer.planNode; + updateIsAddMode(){ + this.isAddMode = this.config.filterPolygon === null; } } diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.html b/src/app/views/querying/gis/components/layers/map-layers.component.html index 2ee332c5..3a7e751c 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.html +++ b/src/app/views/querying/gis/components/layers/map-layers.component.html @@ -65,9 +65,6 @@
    -
    - -
    diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index b1adce0d..12eb41e5 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -79,7 +79,6 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { const informationObjects = Object.values(group.informationObjects); for (const informationObject of informationObjects) { if (informationObject.type === 'InformationPolyAlg') { - queryLayer.jsonPolyAlg = informationObject.jsonPolyAlg; queryLayer.planNode = JSON.parse(informationObject.jsonPolyAlg); } } @@ -92,16 +91,30 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { console.log('this.addDataToExistingLayer', this.addDataToExistingLayer); if (this.addDataToExistingLayer) { - this.addDataToExistingLayer.data = queryLayer.data; - this.addDataToExistingLayer.query = queryLayer.query; + this.addDataToExistingLayer.addData(queryLayer.data, true); // This timestamp will trigger the change detection. this.addDataToExistingLayer.lastUpdated = queryLayer.lastUpdated; - this.addDataToExistingLayer = null; - this.checkCanRerender(); + + // Reset references to layer, so that the user can start the next query. + if (this.applyFilterToLayer){ + // Restores the state in the filter configuration section + this.applyFilterToLayer.filterConfig.removePolygon(); + // Restores the state on the map component + this.layerSettings.removePolygonFilterForLayer(this.applyFilterToLayer); + // Restores the state here. + this.applyFilterToLayer = null; + } else { + // Only update the query if it was written in a query language the query console + // understands. + this.addDataToExistingLayer.query = queryLayer.query; + this.addDataToExistingLayer = null; + } + + // Instantly trigger rerender. + this.updateLayers(this.layers); } else { this.addLayerInternal(queryLayer); } - localStorage.setItem(this.LOCAL_STORAGE_LAST_QUERY_KEY, combinedResult.query); this.isAddLayerModalVisible = false; } } @@ -122,7 +135,6 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { results: WritableSignal[]> = signal([]); readonly language: WritableSignal = signal('sql'); private subscriptions = new Subscription(); - private readonly LOCAL_STORAGE_LAST_QUERY_KEY = 'last_query_gis'; private addDataToExistingLayer: MapLayer = null; private lastQueryAnalyzerId = null; private lastQueryAnalyzerPage = null; @@ -184,28 +196,20 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { return; } - console.log('onPolyPlanChanged', polyPlan, this.applyFilterToLayer); - let plan = ''; if (this.applyFilterToLayer.language === 'mongo') { - const wkt = geojsonToWKT(this.applyFilterToLayer.tempPolygon); - plan = `DOC_FILTER[MQL_GEO_WITHIN( - ${this.applyFilterToLayer.geometryField}, - SRID=4326;${wkt}:DOCUMENT, -1.0E0:DOCUMENT)]( - ${polyPlan})`; + const wkt = geojsonToWKT(this.applyFilterToLayer.filterConfig.filterPolygon); + + // TODO: Get SRID from layer. + plan = `DOC_FILTER[MQL_GEO_WITHIN(${this.applyFilterToLayer.geometryField}, 'SRID=4326;${wkt}':DOCUMENT, -1:DOCUMENT)](${polyPlan})`; plan = trimLines(plan); } -// plan = `DOC_FILTER[MQL_GEO_WITHIN(geometry, SRID=4326;POLYGON ((12.535400390625002 52.92215137976296, 13.458251953125002 51.15178610143037, 15.128173828125002 51.41291212935532, 14.930419921875002 53.553362785528094, 13.348388671875002 53.6185793648952, 12.535400390625002 52.92215137976296)):DOCUMENT, -1.0E0:DOCUMENT)]( -// DOC_SCAN[doc.geocollection2] -// )`; - console.log('Run plan:', plan); const request = new PolyAlgRequest(plan, DataModel.DOCUMENT, 'LOGICAL'); request.noLimit = true; - const success = this.websocket.sendMessage(request); - console.log('success', success); + this.websocket.sendMessage(request); } ngOnDestroy(): void { @@ -221,7 +225,6 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { private initWebsocket() { const sub = this.websocket.onMessage().subscribe({ next: msg => { - console.log('msg: ', msg); if (Array.isArray(msg) && msg[0].hasOwnProperty('routerLink')) { const sidebarNodesTemp: SidebarNode[] = msg; const logicalQueryPlanNode = sidebarNodesTemp.filter(n => n.name === 'Logical Query Plan')[0] || null; @@ -230,9 +233,12 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { this.lastQueryAnalyzerId = split[0]; this.lastQueryAnalyzerPage = split[1]; } - } - if (Array.isArray(msg) && ((msg[0].hasOwnProperty('data') || msg[0].hasOwnProperty('affectedTuples') || msg[0].hasOwnProperty('error')))) { // array of ResultSet + } else if (Array.isArray(msg) && ((msg[0].hasOwnProperty('data') || msg[0].hasOwnProperty('affectedTuples') || msg[0].hasOwnProperty('error')))) { // array of ResultSet this.results.set([]>msg); + } else + if (msg.hasOwnProperty('data') || msg.hasOwnProperty('affectedTuples') || msg.hasOwnProperty('error')){ + // PolyRequest + this.results.set([msg]); } }, error: err => { @@ -247,10 +253,6 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { ngAfterViewInit(): void { this.startPollingHeight(); - const lastQuery = localStorage.getItem(this.LOCAL_STORAGE_LAST_QUERY_KEY); - if (lastQuery) { - this.queryEditor.setCode(lastQuery); - } } ngOnInit(): void { @@ -287,6 +289,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { } const [layer, polygon] = layerAndPolygon; layer.filterConfig.addPolygon(polygon); + this.runPolyPlan(layer); })); // this.updateLayers(getSampleMapLayers()); @@ -409,7 +412,6 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { v.index = i + 1; return v; }); - layer.planValidator = this._validator; this.updateLayers(newLayers); } @@ -431,11 +433,6 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { return this._crud.anyQuery(this.websocket, request); } - filterLayer(layer: MapLayer) { - console.log('Filter layer', layer); - this.layerSettings.addPolygonFilterForLayer(layer); - } - async addLayer() { this.addLayerDialogErrorMessage = ''; diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index 5f001190..92feef7d 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -39,24 +39,7 @@ export class MapLayer { lastUpdated = ''; // Query Filter - planValidator: AlgValidatorService = null; planNode: PlanNode = null; - jsonPolyAlg: string = null; - - // Polygon around Berlin - tempPolygon: Polygon = { - type: 'Polygon', - coordinates: [ - [ - [12.535400390625002, 52.92215137976296], - [13.458251953125002, 51.15178610143037], - [15.128173828125002, 51.41291212935532], - [14.930419921875002, 53.553362785528094], - [13.348388671875002, 53.6185793648952], - [12.535400390625002, 52.92215137976296] - ] - ] - }; dataPreview: MapLayerConfiguration = new DataPreview(this); filterConfig: FilterConfig = new FilterConfig(this); @@ -310,7 +293,11 @@ export class MapLayer { return copy; } - addData(data: MapGeometryWithData[]) { + addData(data: MapGeometryWithData[], overwrite=false) { + if (overwrite){ + this.data = []; + } + this.containsPoints = false; this.containsAreas = false; if (data.length > 0){ From 6aff57189ef852b228dbdddc4e772bcb4edcf8ff Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Tue, 17 Dec 2024 20:34:49 +0100 Subject: [PATCH 54/60] GIS: Add note that filter is not stored inside query --- .../gis/components/configuration/filter/filter.component.css | 4 +++- .../gis/components/configuration/filter/filter.component.html | 1 + .../querying/gis/components/layers/map-layers.component.scss | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/views/querying/gis/components/configuration/filter/filter.component.css b/src/app/views/querying/gis/components/configuration/filter/filter.component.css index 3e275ac2..2fdd913a 100644 --- a/src/app/views/querying/gis/components/configuration/filter/filter.component.css +++ b/src/app/views/querying/gis/components/configuration/filter/filter.component.css @@ -4,4 +4,6 @@ div { gap: 1rem; } - +.info { + color: #a5a5a5; +} \ No newline at end of file diff --git a/src/app/views/querying/gis/components/configuration/filter/filter.component.html b/src/app/views/querying/gis/components/configuration/filter/filter.component.html index 57078c76..e40286ba 100644 --- a/src/app/views/querying/gis/components/configuration/filter/filter.component.html +++ b/src/app/views/querying/gis/components/configuration/filter/filter.component.html @@ -1,3 +1,4 @@
    + Info: The filter is not stored inside the query. Rerunning the query will not apply the filter again.
    diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.scss b/src/app/views/querying/gis/components/layers/map-layers.component.scss index eb6eeed6..e1c27a1d 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.scss +++ b/src/app/views/querying/gis/components/layers/map-layers.component.scss @@ -234,7 +234,7 @@ gap: 1rem; & > span { - color: #bdbdbd; + color: #a5a5a5; } } From e06fd08c672b4fcbb1b9de53903ef0af51bfeea7 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Tue, 17 Dec 2024 21:17:17 +0100 Subject: [PATCH 55/60] GIS: Map now empty after second query is run where no geometric data is detected --- .../data-view/data-map/data-map.component.ts | 10 +++++++++- .../views/querying/gis/models/MapLayer.model.ts | 17 ++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index 5afb107f..60bc4ef5 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -67,7 +67,15 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView // The CombinedResult will be given to the LayerSettings, if the user clicks on the button to show the // results in the full GIS query mode. this.previewResult = result; - this.layerSettings.setLayers([MapLayer.from(result)]); + + // Always update layers, because even if the current query did not result in any geometries that we can + // show on the map, we have to at least remove the old results. + const layers = []; + const resultsLayer = MapLayer.from(result); + if (resultsLayer.data.length > 0){ + layers.push(resultsLayer); + } + this.layerSettings.setLayers(layers); }); } diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index 92feef7d..01fc6a22 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -71,8 +71,8 @@ export class MapLayer { Object.entries(JSON.parse(json)).map(([key, value]) => [key.toLowerCase(), value]) ); const [geometry, key] = this.getGeometryFromData(jsonObject); - geometryField = key; if (geometry) { + geometryField = key; const geometryWithData = new MapGeometryWithData(rowIndex, geometry, jsonObject); mapData.push(geometryWithData); } @@ -100,8 +100,8 @@ export class MapLayer { } const [geometry, key] = this.getGeometryFromData(obj); - geometryField = key; if (geometry) { + geometryField = key; const geometryWithData = new MapGeometryWithData(rowIndex, geometry, obj); mapData.push(geometryWithData); } @@ -132,8 +132,8 @@ export class MapLayer { } const [geometry, key] = this.getGeometryFromData(obj); - geometryField = key; if (geometry) { + geometryField = key; const geometryWithData = new MapGeometryWithData(rowIndex, geometry, obj); mapData.push(geometryWithData); } @@ -144,9 +144,12 @@ export class MapLayer { throw Error(`Cannot convert CombinedResult to MapLayer. Unknown document model: ${result.dataModel}`); } - layer.addData(mapData); - layer.geometryField = geometryField; - console.log('Created layer: ', layer); + if (mapData.length > 0){ + layer.addData(mapData); + layer.geometryField = geometryField; + } + + console.log(`Created layer with ${layer.data.length}: data points:`, layer); return layer; } @@ -191,7 +194,7 @@ export class MapLayer { // TODO: Detect heuristic, so that we can automatically detect the most common geometry types // - string in WKT format - return undefined; + return [undefined, undefined]; } From a512064ac8e805f4a83a14d10cba494fd1b2a33e Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Tue, 17 Dec 2024 22:57:18 +0100 Subject: [PATCH 56/60] GIS: Disable enable/disable functions for polygon tool, user must click on "Polygon Tool" themselves. Maybe activate the Polygon Tool by clicking the button that appears in JavaScript? --- .../data-view/data-map/data-map.component.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index 60bc4ef5..f5c53f3f 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -35,7 +35,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView leafletDrawControl = null; layerIdToPoylgon = new Map(); currentDrawingLayer: MapLayer = null; - polygonTool = null; + // polygonTool = null; isInitialRender = true; readonly MIN_ZOOM = 0; @@ -163,7 +163,7 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView this.layerIdToPoylgon.set(this.currentDrawingLayer.uuid, polygon); console.log('Polygon added for layer ', this.currentDrawingLayer.uuid, polygon); leafletMap.removeControl(this.leafletDrawControl); - this.polygonTool.disable(); + // this.polygonTool.disable(); drawnItems.addLayer(polygon); // Send coordinates back to map-layers @@ -186,9 +186,9 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView } this.currentDrawingLayer = mapLayer; leafletMap.addControl(this.leafletDrawControl); - const polygonTool = new L.Draw.Polygon(leafletMap, this.leafletDrawControl.options.draw.polygon); - polygonTool.enable(); - this.polygonTool = polygonTool; + // const polygonTool = new L.Draw.Polygon(leafletMap, this.leafletDrawControl.options.draw.polygon); + // polygonTool.enable(); + // this.polygonTool = polygonTool; })); this.subscriptions.add(this.layerSettings.removeLayerFilterPolygon$.subscribe((mapLayer) => { From 7b1a0a6bf29a52a8aa84d0f4bad4d6811f1da7e8 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Wed, 18 Dec 2024 17:22:24 +0100 Subject: [PATCH 57/60] GIS: Extract distance and add comment --- .../querying/gis/components/layers/map-layers.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index 12eb41e5..ef862956 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -202,7 +202,10 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { const wkt = geojsonToWKT(this.applyFilterToLayer.filterConfig.filterPolygon); // TODO: Get SRID from layer. - plan = `DOC_FILTER[MQL_GEO_WITHIN(${this.applyFilterToLayer.geometryField}, 'SRID=4326;${wkt}':DOCUMENT, -1:DOCUMENT)](${polyPlan})`; + // TODO: class const + // Do not use distance for MQL_GEO_WITHIN + const distance = -1; + plan = `DOC_FILTER[MQL_GEO_WITHIN(${this.applyFilterToLayer.geometryField}, 'SRID=4326;${wkt}':DOCUMENT, ${distance}:FLOAT)](${polyPlan})`; plan = trimLines(plan); } From 3b98ee79fa1ee2242cb2811615a3f7e93b2e1622 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Wed, 18 Dec 2024 17:32:18 +0100 Subject: [PATCH 58/60] GIS: Correctly parse relational column with type double --- src/app/views/querying/gis/models/MapLayer.model.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index 01fc6a22..e7793396 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -94,7 +94,10 @@ export class MapLayer { obj[key] = parseInt(value, 10); } else if (header.dataType.startsWith('DECIMAL')) { obj[key] = parseFloat(value); - } else { + } else if (header.dataType.startsWith('DOUBLE')) { + obj[key] = parseFloat(value); + } + else { obj[key] = value; } } From 49a55856bdd2db1ab5820912f82b5b1ce28546e4 Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Wed, 18 Dec 2024 22:27:13 +0100 Subject: [PATCH 59/60] GIS: Refactor Add Drawing Mode - Only one layer at a time can be inside the drawing mode. This simplifies our architecture, because otherwise we would have to execute multiple queries and keep track which results belong to which layer. - Make it possible to apply the filter multiple times in a row without breaking. - Semantics should now be easier to understand. --- .../data-view/data-map/data-map.component.ts | 12 +++--- .../components/configuration/FilterConfig.ts | 21 ++------- .../configuration/filter/filter.component.css | 11 ++++- .../filter/filter.component.html | 16 +++++-- .../configuration/filter/filter.component.ts | 21 ++------- .../components/layers/map-layers.component.ts | 43 +++++++++++++++---- .../gis/services/layersettings.service.ts | 26 +++++------ 7 files changed, 85 insertions(+), 65 deletions(-) diff --git a/src/app/components/data-view/data-map/data-map.component.ts b/src/app/components/data-view/data-map/data-map.component.ts index f5c53f3f..12491f7d 100644 --- a/src/app/components/data-view/data-map/data-map.component.ts +++ b/src/app/components/data-view/data-map/data-map.component.ts @@ -180,21 +180,23 @@ export class DataMapComponent extends DataTemplateComponent implements AfterView this.layerSettings.addPolygonToLayer(this.currentDrawingLayer, geoJsonPolygon); }); - this.subscriptions.add(this.layerSettings.addLayerFilterPolygon$.subscribe((mapLayer) => { + this.subscriptions.add(this.layerSettings.layerEnableDrawingMode$.subscribe((mapLayer) => { if (mapLayer === null) { return; } this.currentDrawingLayer = mapLayer; leafletMap.addControl(this.leafletDrawControl); - // const polygonTool = new L.Draw.Polygon(leafletMap, this.leafletDrawControl.options.draw.polygon); - // polygonTool.enable(); - // this.polygonTool = polygonTool; })); - this.subscriptions.add(this.layerSettings.removeLayerFilterPolygon$.subscribe((mapLayer) => { + this.subscriptions.add(this.layerSettings.layerDisableDrawingMode$.subscribe((mapLayer) => { if (mapLayer === null) { return; } + + // This is important if the users disables the drawing mode. + leafletMap.removeControl(this.leafletDrawControl); + + // Remove shape from the map if it exists. if (!this.layerIdToPoylgon.has(mapLayer.uuid)) { return; } diff --git a/src/app/views/querying/gis/components/configuration/FilterConfig.ts b/src/app/views/querying/gis/components/configuration/FilterConfig.ts index d5c88a47..1b4ee6c9 100644 --- a/src/app/views/querying/gis/components/configuration/FilterConfig.ts +++ b/src/app/views/querying/gis/components/configuration/FilterConfig.ts @@ -5,30 +5,15 @@ import {MapLayer} from '../../models/MapLayer.model'; export class FilterConfig implements MapLayerConfiguration { configurationComponentType = FilterComponent; - filterPolygon: Polygon = null; - filterPolygonText = ''; layer: MapLayer; + isDrawingModeActive = false; + polygon: Polygon; constructor(layer: MapLayer) { this.layer = layer; } copy(): MapLayerConfiguration { - const copy = new FilterConfig(this.layer); - copy.filterPolygon = this.filterPolygon; - copy.filterPolygonText = this.filterPolygonText; - return copy; - } - - addPolygon(polygon: Polygon) { - this.filterPolygon = polygon; - this.filterPolygonText = 'Polygon'; - console.log('Polygon added', polygon); - } - - removePolygon() { - this.filterPolygon = null; - this.filterPolygonText = ''; - console.log('Polygon removed'); + return new FilterConfig(this.layer); } } diff --git a/src/app/views/querying/gis/components/configuration/filter/filter.component.css b/src/app/views/querying/gis/components/configuration/filter/filter.component.css index 2fdd913a..e3e5b6d8 100644 --- a/src/app/views/querying/gis/components/configuration/filter/filter.component.css +++ b/src/app/views/querying/gis/components/configuration/filter/filter.component.css @@ -1,9 +1,18 @@ -div { +.flex { display: flex; flex-direction: column; gap: 1rem; } +label { + margin-bottom: 0.25rem; +} + +button { + /* So that the button width does not change when the labels are changed. */ + width: 210px; +} + .info { color: #a5a5a5; } \ No newline at end of file diff --git a/src/app/views/querying/gis/components/configuration/filter/filter.component.html b/src/app/views/querying/gis/components/configuration/filter/filter.component.html index e40286ba..e99b6cf9 100644 --- a/src/app/views/querying/gis/components/configuration/filter/filter.component.html +++ b/src/app/views/querying/gis/components/configuration/filter/filter.component.html @@ -1,4 +1,14 @@ -
    - - Info: The filter is not stored inside the query. Rerunning the query will not apply the filter again. +
    +
    + + +
    + Info: Once the drawing mode is enabled, + you can use the Polygon Tool on the Map to draw a polygon. + When done, the filter is automatically applied to the query.. +
    diff --git a/src/app/views/querying/gis/components/configuration/filter/filter.component.ts b/src/app/views/querying/gis/components/configuration/filter/filter.component.ts index b9de5e41..de019cfe 100644 --- a/src/app/views/querying/gis/components/configuration/filter/filter.component.ts +++ b/src/app/views/querying/gis/components/configuration/filter/filter.component.ts @@ -12,31 +12,18 @@ import {FilterConfig} from '../FilterConfig'; }) export class FilterComponent implements MapLayerConfigurationComponent { - isAddMode: boolean; - constructor( @Inject('config') protected config: FilterConfig, private layerSettings: LayerSettingsService, ) { - this.updateIsAddMode(); } - configChanged() { - this.layerSettings.visualizationConfigurationChanged(this.config); + enableDrawingMode(){ + this.layerSettings.enableDrawingModeForLayer(this.config.layer); } - drawOrRemovePolygon() { - if (this.config.filterPolygon) { - console.log('drawOrRemovePolygon REMOVE'); - this.config.removePolygon(); - this.layerSettings.removePolygonFilterForLayer(this.config.layer); - } else { - console.log('drawOrRemovePolygon ADD'); - this.layerSettings.addPolygonFilterForLayer(this.config.layer); - } + disableDrawingMode(){ + this.layerSettings.disableDrawingModeForLayer(this.config.layer); } - updateIsAddMode(){ - this.isAddMode = this.config.filterPolygon === null; - } } diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index ef862956..c39fd387 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -96,20 +96,24 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { this.addDataToExistingLayer.lastUpdated = queryLayer.lastUpdated; // Reset references to layer, so that the user can start the next query. - if (this.applyFilterToLayer){ + if (this.applyFilterToLayer) { // Restores the state in the filter configuration section - this.applyFilterToLayer.filterConfig.removePolygon(); + this.applyFilterToLayer.filterConfig.polygon = null; // Restores the state on the map component - this.layerSettings.removePolygonFilterForLayer(this.applyFilterToLayer); + this.layerSettings.disableDrawingModeForLayer(this.applyFilterToLayer); // Restores the state here. this.applyFilterToLayer = null; + // Remove the PolyPlan from the AlgViewer, so that when setting the same plan for another + // filter operation, the changed event is fired again. + this.algViewerComponent.setPolyAlgPlan(null, 'LOGICAL'); } else { // Only update the query if it was written in a query language the query console // understands. this.addDataToExistingLayer.query = queryLayer.query; - this.addDataToExistingLayer = null; } + this.addDataToExistingLayer = null; + // Instantly trigger rerender. this.updateLayers(this.layers); } else { @@ -118,6 +122,11 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { this.isAddLayerModalVisible = false; } } + + }, { + // Necessary to set the polyPlan of the AlgViewer to null after + // the result from the query arrived. + allowSignalWrites: true }); } @@ -182,24 +191,27 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { runPolyPlan(layer: MapLayer) { if (this.applyFilterToLayer || this.addDataToExistingLayer) { - console.log('Another query is already in progress. Wait for it to finish.'); + console.log('Another query is already in progress. Wait for it to finish.', this.applyFilterToLayer, this.addDataToExistingLayer); return; } this.applyFilterToLayer = layer; this.addDataToExistingLayer = layer; + console.log('runPolyPlan layer.planNode=', layer.planNode); this.algViewerComponent.setPolyAlgPlan(layer.planNode, 'LOGICAL'); } onPolyPlanChanged(polyPlan: string) { + console.log('onPolyPlanChanged', polyPlan); if (!this.applyFilterToLayer || !polyPlan) { return; } + console.log('onPolyPlanChanged this.applyFilterToLayer', this.applyFilterToLayer); let plan = ''; if (this.applyFilterToLayer.language === 'mongo') { - const wkt = geojsonToWKT(this.applyFilterToLayer.filterConfig.filterPolygon); + const wkt = geojsonToWKT(this.applyFilterToLayer.filterConfig.polygon); // TODO: Get SRID from layer. // TODO: class const @@ -238,8 +250,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { } } else if (Array.isArray(msg) && ((msg[0].hasOwnProperty('data') || msg[0].hasOwnProperty('affectedTuples') || msg[0].hasOwnProperty('error')))) { // array of ResultSet this.results.set([]>msg); - } else - if (msg.hasOwnProperty('data') || msg.hasOwnProperty('affectedTuples') || msg.hasOwnProperty('error')){ + } else if (msg.hasOwnProperty('data') || msg.hasOwnProperty('affectedTuples') || msg.hasOwnProperty('error')) { // PolyRequest this.results.set([msg]); } @@ -291,10 +302,24 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { return; } const [layer, polygon] = layerAndPolygon; - layer.filterConfig.addPolygon(polygon); + layer.filterConfig.polygon = polygon; this.runPolyPlan(layer); })); + this.subscriptions.add(this.layerSettings.layerEnableDrawingMode$.subscribe((mapLayer) => { + if (mapLayer === null) { + return; + } + + // Currently, the drawing mode is exclusive to a single layer. Disable for all other layers. + for (const layer of this.layers) { + if (layer.uuid === mapLayer.uuid) { + continue; + } + layer.filterConfig.isDrawingModeActive = false; + } + })); + // this.updateLayers(getSampleMapLayers()); } diff --git a/src/app/views/querying/gis/services/layersettings.service.ts b/src/app/views/querying/gis/services/layersettings.service.ts index 43d25791..703eb7a4 100644 --- a/src/app/views/querying/gis/services/layersettings.service.ts +++ b/src/app/views/querying/gis/services/layersettings.service.ts @@ -12,10 +12,10 @@ import {Polygon} from 'geojson'; }) export class LayerSettingsService { // Filter - private addLayerFilterPolygon: BehaviorSubject; - addLayerFilterPolygon$: Observable; - private removeLayerFilterPolygon: BehaviorSubject; - removeLayerFilterPolygon$: Observable; + private layerEnableDrawingMode: BehaviorSubject; + layerEnableDrawingMode$: Observable; + private layerDisableDrawingMode: BehaviorSubject; + layerDisableDrawingMode$: Observable; private layerPolygonFilter: BehaviorSubject<[MapLayer, Polygon]>; layerPolygonFilter$: Observable<[MapLayer, Polygon]>; private fitLayerToMap: BehaviorSubject; @@ -43,10 +43,10 @@ export class LayerSettingsService { // To make sure that information from one session does not spill over the next session, we recreate all // reactive variables, so that they don't hold any old values. Otherwise, the next time we subscribe, we will // receive the most recent value, which could be from an old session. - this.addLayerFilterPolygon = new BehaviorSubject(null); - this.addLayerFilterPolygon$ = this.addLayerFilterPolygon.asObservable(); - this.removeLayerFilterPolygon = new BehaviorSubject(null); - this.removeLayerFilterPolygon$ = this.removeLayerFilterPolygon.asObservable(); + this.layerEnableDrawingMode = new BehaviorSubject(null); + this.layerEnableDrawingMode$ = this.layerEnableDrawingMode.asObservable(); + this.layerDisableDrawingMode = new BehaviorSubject(null); + this.layerDisableDrawingMode$ = this.layerDisableDrawingMode.asObservable(); this.layerPolygonFilter = new BehaviorSubject<[MapLayer, Polygon]>(null); this.layerPolygonFilter$ = this.layerPolygonFilter.asObservable(); this.fitLayerToMap = new BehaviorSubject(null); @@ -67,12 +67,14 @@ export class LayerSettingsService { this.toggleLayerVisibility$ = this.toggleLayerVisibilitySubject.asObservable(); } - removePolygonFilterForLayer(layer: MapLayer) { - this.removeLayerFilterPolygon.next(layer); + disableDrawingModeForLayer(layer: MapLayer) { + layer.filterConfig.isDrawingModeActive = false; + this.layerDisableDrawingMode.next(layer); } - addPolygonFilterForLayer(layer: MapLayer) { - this.addLayerFilterPolygon.next(layer); + enableDrawingModeForLayer(layer: MapLayer) { + layer.filterConfig.isDrawingModeActive = true; + this.layerEnableDrawingMode.next(layer); } addPolygonToLayer(layer: MapLayer, polygon: Polygon){ From 7661ae3845daf044d62e83aede63e28d5d354cce Mon Sep 17 00:00:00 2001 From: Rafael Biehler Date: Thu, 19 Dec 2024 12:48:48 +0100 Subject: [PATCH 60/60] GIS: Edit Layer + Remove layer now rerender immediately Removing layers is important, because if the client has performance problems, those can be fixed by removing a layer with a lot of shapes. Otherwise this would have only been possible by adding a new layer, which causes a rerender, or rerunning the query for an existing layer. --- .../configuration/filter/filter.component.css | 15 ++-- .../filter/filter.component.html | 14 +++- .../configuration/filter/filter.component.ts | 7 +- .../layers/map-layers.component.html | 20 +++--- .../components/layers/map-layers.component.ts | 70 +++++++++++++++---- .../querying/gis/models/MapLayer.model.ts | 9 ++- .../gis/services/layersettings.service.ts | 11 ++- 7 files changed, 106 insertions(+), 40 deletions(-) diff --git a/src/app/views/querying/gis/components/configuration/filter/filter.component.css b/src/app/views/querying/gis/components/configuration/filter/filter.component.css index e3e5b6d8..af3d568f 100644 --- a/src/app/views/querying/gis/components/configuration/filter/filter.component.css +++ b/src/app/views/querying/gis/components/configuration/filter/filter.component.css @@ -4,15 +4,18 @@ gap: 1rem; } -label { - margin-bottom: 0.25rem; -} +.spatial-filter-section { + display: flex; + flex-direction: column; + gap: 0.25rem; -button { - /* So that the button width does not change when the labels are changed. */ - width: 210px; + & > button { + /* So that the button width does not change when the labels are changed. */ + width: 210px; + } } .info { + display: block; color: #a5a5a5; } \ No newline at end of file diff --git a/src/app/views/querying/gis/components/configuration/filter/filter.component.html b/src/app/views/querying/gis/components/configuration/filter/filter.component.html index e99b6cf9..35e90711 100644 --- a/src/app/views/querying/gis/components/configuration/filter/filter.component.html +++ b/src/app/views/querying/gis/components/configuration/filter/filter.component.html @@ -1,14 +1,22 @@
    -
    +
    -
    - Info: Once the drawing mode is enabled, + Info: Once the drawing mode is enabled, you can use the Polygon Tool on the Map to draw a polygon. When done, the filter is automatically applied to the query.. +
    + +
    + +
    +
    diff --git a/src/app/views/querying/gis/components/configuration/filter/filter.component.ts b/src/app/views/querying/gis/components/configuration/filter/filter.component.ts index de019cfe..aa073aa8 100644 --- a/src/app/views/querying/gis/components/configuration/filter/filter.component.ts +++ b/src/app/views/querying/gis/components/configuration/filter/filter.component.ts @@ -18,12 +18,15 @@ export class FilterComponent implements MapLayerConfigurationComponent { ) { } - enableDrawingMode(){ + enableDrawingMode() { this.layerSettings.enableDrawingModeForLayer(this.config.layer); } - disableDrawingMode(){ + disableDrawingMode() { this.layerSettings.disableDrawingModeForLayer(this.config.layer); } + editQuery() { + this.layerSettings.editQuery(this.config.layer); + } } diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.html b/src/app/views/querying/gis/components/layers/map-layers.component.html index 3a7e751c..8457709e 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.html +++ b/src/app/views/querying/gis/components/layers/map-layers.component.html @@ -12,8 +12,7 @@ cdkDropListSortingDisabled="false" >
    - {{ layer.index }} @@ -61,7 +60,8 @@ {{ layer.query === null ? 'FILE' : 'QUERY' }}
    - {{ layer.name }} + {{ layer.isQueryLayer ? layer.query : layer.name }}
    @@ -69,7 +69,7 @@ [title]="'Data' + ' (' + layer.data.length + ')'" [config]="layer.dataPreview"> -
    Add layer
    +
    {{ editQueryForMapLayer ? 'Edit Query' : 'Add Layer' }}
    -
    +
    @@ -189,7 +189,11 @@
    Add layer
    - + diff --git a/src/app/views/querying/gis/components/layers/map-layers.component.ts b/src/app/views/querying/gis/components/layers/map-layers.component.ts index c39fd387..d7b4566c 100644 --- a/src/app/views/querying/gis/components/layers/map-layers.component.ts +++ b/src/app/views/querying/gis/components/layers/map-layers.component.ts @@ -24,7 +24,7 @@ import {CdkDragDrop, moveItemInArray,} from '@angular/cdk/drag-drop'; import {getSampleMapLayers} from '../../models/get-sample-maplayers'; import {CrudService} from '../../../../../services/crud.service'; import {WebSocket} from '../../../../../services/webSocket'; -import {RelationalResult, Result} from '../../../../../components/data-view/models/result-set.model'; +import {Result} from '../../../../../components/data-view/models/result-set.model'; import {WebuiSettingsService} from '../../../../../services/webui-settings.service'; import {Subscription} from 'rxjs'; import {DataModel, PolyAlgRequest, QueryRequest} from '../../../../../models/ui-request.model'; @@ -49,6 +49,7 @@ interface BaseLayer { styleUrl: './map-layers.component.scss', }) export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { + protected editQueryForMapLayer: MapLayer; constructor( protected layerSettings: LayerSettingsService, @@ -65,10 +66,12 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { const res = this.results(); if (res.length > 0) { const combinedResult = CombinedResult.from(res[0]); + if (combinedResult.error) { this.addLayerDialogErrorMessage = `There was an error executing the query. Error: ${combinedResult.error}`; } else { const queryLayer = MapLayer.from(combinedResult); + console.log('queryLayer.query', queryLayer.query); if (this.lastQueryAnalyzerId && this.lastQueryAnalyzerPage) { this._crud.getAnalyzerPage(this.lastQueryAnalyzerId, this.lastQueryAnalyzerPage).subscribe({ @@ -109,6 +112,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { } else { // Only update the query if it was written in a query language the query console // understands. + console.log('Update query'); this.addDataToExistingLayer.query = queryLayer.query; } @@ -186,7 +190,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { private lastHeight = ''; protected readonly Object = Object; protected readonly LayerContext = LayerContext; - protected readonly queryLanguages = ['CYPHER', 'SQL', 'MQL']; + protected readonly queryLanguages = ['cypher', 'sql', 'mql']; readonly activeNamespace: WritableSignal = signal(null); runPolyPlan(layer: MapLayer) { @@ -320,6 +324,28 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { } })); + this.subscriptions.add(this.layerSettings.editQueryForMapLayer$.subscribe((mapLayer) => { + if (mapLayer === null) { + return; + } + if (!mapLayer.isQueryLayer) { + return; + } + + this.editQueryForMapLayer = mapLayer; + + // Show the Add Layer Modal, that we also use to edit the query. + if (!this.isAddLayerModalVisible) { + console.log('EDIT MODE', this.editQueryForMapLayer); + this.queryEditor.setCode(mapLayer.query); + const language = mapLayer.language === 'mongo' ? 'mql' : mapLayer.language; + this.language.set(language); + this.activeNamespace.set(mapLayer.namespace); + this.addLayerMode = LayerContext.Query; + this.isAddLayerModalVisible = true; + } + })); + // this.updateLayers(getSampleMapLayers()); } @@ -420,12 +446,9 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { this.layerSettings.setFitLayerToMap(layer); } - removeLayer(layer: MapLayer) { - layer.isRemoved = true; - if (layer.isActive) { - this.toggleLayerVisibility(layer); - this.updateLayerUi(); - } + removeLayer(layerToRemove: MapLayer) { + const newLayers = this.layers.filter(layer => layer !== layerToRemove); + this.updateLayers(newLayers); } onBaseLayerChange(selectedLayer: BaseLayer): void { @@ -435,7 +458,7 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { addLayerInternal(layer: MapLayer) { const newLayers = [ layer, - ...this.layers.filter((l) => !l.isRemoved), + ...this.layers, ].map((v, i) => { v.index = i + 1; return v; @@ -517,7 +540,14 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { } toggleAddLayerModalVisibility() { - this.isAddLayerModalVisible = !this.isAddLayerModalVisible; + if (!this.isAddLayerModalVisible){ + // Open modal in new layer mode + this.editQueryForMapLayer = null; + this.isAddLayerModalVisible = true; + } else { + // Close the modal + this.isAddLayerModalVisible = false; + } } addLayerModalVisibilityChanged(event: any) { @@ -527,12 +557,10 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { updateLayerUi() { // Layers which are first in the array are rendered first, and will be drawn over by other layers. // Layers: BOTTOM -> TOP - const visibleLayers = this.layers.filter((d) => !d.isRemoved); - for (let i = 0; i < visibleLayers.length; i++) { - visibleLayers[visibleLayers.length - 1 - i].index = i + 1; + for (let i = 0; i < this.layers.length; i++) { + this.layers[this.layers.length - 1 - i].index = i + 1; } - this.anyLayersVisible = - this.layers.filter((d) => !d.isRemoved).length > 0; + this.anyLayersVisible = this.layers.length > 0; } export() { @@ -574,4 +602,16 @@ export class MapLayersComponent implements OnInit, AfterViewInit, OnDestroy { a.click(); URL.revokeObjectURL(url); } + + editQuery() { + if (!this.editQueryForMapLayer) { + return; + } + this.editQueryForMapLayer.query = this.queryEditor.getCode(); + this.editQueryForMapLayer.language = this.language(); + this.editQueryForMapLayer.namespace = this.activeNamespace(); + this.isAddLayerModalVisible = false; + this.rerunQuery(this.editQueryForMapLayer); + this.editQueryForMapLayer = null; + } } diff --git a/src/app/views/querying/gis/models/MapLayer.model.ts b/src/app/views/querying/gis/models/MapLayer.model.ts index e7793396..4495d557 100644 --- a/src/app/views/querying/gis/models/MapLayer.model.ts +++ b/src/app/views/querying/gis/models/MapLayer.model.ts @@ -32,10 +32,10 @@ export class MapLayer { // Query isQueryLayer = false; - query = null; - geometryField = null; - language = null; - namespace = null; + query = ''; + geometryField = ''; + language = ''; + namespace = ''; lastUpdated = ''; // Query Filter @@ -50,7 +50,6 @@ export class MapLayer { // Computed (Not used in copy) isActive = true; - isRemoved = false; index = -1; static from(result: CombinedResult): MapLayer { diff --git a/src/app/views/querying/gis/services/layersettings.service.ts b/src/app/views/querying/gis/services/layersettings.service.ts index 703eb7a4..fbae3d50 100644 --- a/src/app/views/querying/gis/services/layersettings.service.ts +++ b/src/app/views/querying/gis/services/layersettings.service.ts @@ -35,6 +35,9 @@ export class LayerSettingsService { private toggleLayerVisibilitySubject: Subject; toggleLayerVisibility$: Observable; + private editQueryForMapLayer: Subject; + editQueryForMapLayer$: Observable; + constructor() { this.reset(); } @@ -65,6 +68,12 @@ export class LayerSettingsService { this.rerenderButtonClicked$ = this.rerenderButtonClickedSubject.asObservable(); this.toggleLayerVisibilitySubject = new Subject(); this.toggleLayerVisibility$ = this.toggleLayerVisibilitySubject.asObservable(); + this.editQueryForMapLayer = new Subject(); + this.editQueryForMapLayer$ = this.editQueryForMapLayer.asObservable(); + } + + editQuery(layer: MapLayer) { + this.editQueryForMapLayer.next(layer); } disableDrawingModeForLayer(layer: MapLayer) { @@ -77,7 +86,7 @@ export class LayerSettingsService { this.layerEnableDrawingMode.next(layer); } - addPolygonToLayer(layer: MapLayer, polygon: Polygon){ + addPolygonToLayer(layer: MapLayer, polygon: Polygon) { this.layerPolygonFilter.next([layer, polygon]); }