diff --git a/apps/server-asset-sg/src/features/asset-old/asset.service.ts b/apps/server-asset-sg/src/features/asset-old/asset.service.ts
index fb19ccfc..8d2b7d53 100644
--- a/apps/server-asset-sg/src/features/asset-old/asset.service.ts
+++ b/apps/server-asset-sg/src/features/asset-old/asset.service.ts
@@ -44,13 +44,24 @@ export class AssetService {
}
async getAllStudies() {
- const rawData: { studyId: string; isPoint: boolean; centroidGeomText: string }[] =
- await this.prismaService.$queryRawUnsafe(
- 'select study_id as "studyId", is_point as "isPoint", centroid_geom_text as "centroidGeomText" from public.all_study'
- );
+ interface RawStudy {
+ studyId: string;
+ assetId: number;
+ isPoint: boolean;
+ centroidGeomText: string;
+ }
+ const rawData: RawStudy[] = await this.prismaService.$queryRawUnsafe(`
+ SELECT
+ study_id AS "studyId",
+ asset_id AS "assetId",
+ is_point AS "isPoint",
+ centroid_geom_text AS "centroidGeomText"
+ FROM public.all_study
+ `);
return rawData.map((study) => ({
studyId: study.studyId,
+ assetId: study.assetId,
isPoint: study.isPoint,
centroid: study.centroidGeomText.replace('POINT(', '').replace(')', ''),
}));
diff --git a/libs/asset-viewer/src/lib/components/asset-viewer-page/asset-viewer-page.component.html b/libs/asset-viewer/src/lib/components/asset-viewer-page/asset-viewer-page.component.html
index 9ba628fa..486e1c63 100644
--- a/libs/asset-viewer/src/lib/components/asset-viewer-page/asset-viewer-page.component.html
+++ b/libs/asset-viewer/src/lib/components/asset-viewer-page/asset-viewer-page.component.html
@@ -1,7 +1,6 @@
diff --git a/libs/asset-viewer/src/lib/components/map/map-controller.ts b/libs/asset-viewer/src/lib/components/map/map-controller.ts
index 11dc9f76..a747f06d 100644
--- a/libs/asset-viewer/src/lib/components/map/map-controller.ts
+++ b/libs/asset-viewer/src/lib/components/map/map-controller.ts
@@ -3,6 +3,7 @@ import { isNotUndefined } from '@asset-sg/core';
import { AssetEditDetail, getCoordsFromStudy, Study } from '@asset-sg/shared';
import { Control } from 'ol/control';
import { Coordinate } from 'ol/coordinate';
+import { containsExtent } from 'ol/extent';
import Feature from 'ol/Feature';
import { Geometry, LineString, Point, Polygon } from 'ol/geom';
import { fromExtent as polygonFromExtent } from 'ol/geom/Polygon';
@@ -21,15 +22,30 @@ export class MapController {
private readonly map: OlMap;
readonly layers: MapLayers;
-
readonly sources: MapLayerSources;
+ readonly assetsClick$: Observable;
+ readonly assetsHover$: Observable;
+
+ /**
+ * The id of all visible assets, mapped to their {@link AssetEditDetail} object.
+ * @private
+ */
private readonly assetsById = new Map();
- private readonly assetsByStudyId = new Map();
+
+ /**
+ * The IDs of all available studies, mapped to the id of the asset that they belong to.
+ * @private
+ */
+ private readonly assetIdsByStudyId = new Map();
+
+ /**
+ * The currently selected asset.
+ * @private
+ */
+ private activeAsset: AssetEditDetail | null = null;
private readonly _isInitialized$ = new BehaviorSubject(false);
- readonly assetsClick$: Observable;
- readonly assetsHover$: Observable;
constructor(element: HTMLElement) {
const view = new View({
@@ -44,6 +60,7 @@ export class MapController {
this.map = new OlMap({
target: element,
+ controls: [],
layers: [
this.layers.raster,
this.layers.heatmap,
@@ -87,12 +104,14 @@ export class MapController {
}
setStudies(studies: AllStudyDTO[]): void {
+ this.assetIdsByStudyId.clear();
const studyFeatures: Feature[] = Array(studies.length);
const heatmapFeatures: Feature[] = Array(studies.length);
for (let i = 0; i < studies.length; i++) {
const study = studies[i];
const geometry = new Point(olCoordsFromLV95(study.centroid));
+ this.assetIdsByStudyId.set(study.studyId, study.assetId);
const heatmapFeature = new Feature(geometry);
heatmapFeature.setId(study.studyId);
@@ -116,7 +135,6 @@ export class MapController {
setAssets(assets: AssetEditDetail[]): void {
this.assetsById.clear();
- this.assetsByStudyId.clear();
const features: Feature[] = [];
const studies: Study[] = [];
@@ -124,7 +142,7 @@ export class MapController {
const asset = assets[i];
this.assetsById.set(asset.assetId, asset);
for (const assetStudy of asset.studies) {
- const study = { studyId: assetStudy.studyId, geom: wktToGeoJSON(assetStudy.geomText) };
+ const study: Study = { studyId: assetStudy.studyId, geom: wktToGeoJSON(assetStudy.geomText) };
features.push(
makeStudyFeature(study, {
point: featureStyles.bigPoint,
@@ -132,36 +150,29 @@ export class MapController {
lineString: featureStyles.lineString,
})
);
-
const studyFeature = this.sources.studies.getFeatureById(study.studyId);
if (studyFeature != null) {
- studyFeature.set('previousStyle', studyFeature.getStyle());
- studyFeature.setStyle(featureStyles.hidden);
+ this.hideFeature(studyFeature);
}
-
- this.assetsByStudyId.set(study.studyId, asset);
+ studies.push(study);
}
}
window.requestAnimationFrame(() => {
this.sources.assets.clear();
this.sources.assets.addFeatures(features);
+ this.sources.picker.clear();
zoomToStudies(this.map, studies);
});
}
clearAssets(): void {
this.assetsById.clear();
- this.assetsByStudyId.clear();
window.requestAnimationFrame(() => {
this.sources.assets.clear();
this.sources.polygon.clear();
+ this.sources.picker.clear();
this.sources.studies.forEachFeature((feature) => {
- const previousStyle = feature.get('previousStyle');
- if (previousStyle == null) {
- return;
- }
- feature.setStyle(previousStyle);
- feature.unset('previousStyle');
+ this.unhideFeature(feature);
});
});
}
@@ -180,6 +191,7 @@ export class MapController {
lineString: featureStyles.lineStringAssetHighlighted,
});
});
+
this.sources.picker.clear();
this.sources.picker.addFeatures(features);
}
@@ -189,6 +201,9 @@ export class MapController {
}
setActiveAsset(asset: AssetEditDetail): void {
+ this.resetActiveAssetStyle();
+ this.activeAsset = asset;
+
this.sources.activeAsset.clear();
this.layers.assets.setOpacity(0.5);
this.layers.studies.setOpacity(0.5);
@@ -208,6 +223,11 @@ export class MapController {
lineString: featureStyles.lineStringAsset,
});
features.push(feature);
+
+ const studyFeature = this.sources.studies.getFeatureById(study.studyId);
+ if (studyFeature != null) {
+ this.hideFeature(studyFeature);
+ }
}
window.requestAnimationFrame(() => {
@@ -217,6 +237,8 @@ export class MapController {
}
clearActiveAsset(): void {
+ this.resetActiveAssetStyle();
+ this.activeAsset = null;
this.sources.activeAsset.clear();
this.layers.assets.setOpacity(1);
this.layers.studies.setOpacity(1);
@@ -267,14 +289,52 @@ export class MapController {
}) as MapLayer;
}
+ /**
+ * Creates an observable that emits the ids of assets whose geometries have been clicked.
+ *
+ * - If a study point has been clicked, that study's assetId is emitted as the only clicked element.
+ * - Otherwise, the assetIds of all overlapping studies hit by the click are emitted.
+ *
+ * @private
+ */
private makeAssetsClick$(): Observable {
return fromEventPattern>(
(h) => this.map.on('click', h),
(h) => this.map.un('click', h)
).pipe(
- // Extract the ids of the assets that have been clicked.
- map((event) => {
- const assetIds: number[] = [];
+ // Check if the click has hit a study point, and use only that point if so.
+ switchMap(async (event) => {
+ let assetId: number | null = null;
+ this.map.forEachFeatureAtPixel(
+ event.pixel,
+ (feature): void => {
+ if (assetId != null) {
+ return;
+ }
+ const featureId = feature.getId();
+ if (featureId == null) {
+ return;
+ }
+ const currentAssetId = this.assetIdsByStudyId.get(`${featureId}`);
+ if (currentAssetId != null) {
+ assetId = currentAssetId;
+ }
+ },
+ {
+ layerFilter: (layer) => layer === this.layers.studies,
+ }
+ );
+ return [event, assetId] as const;
+ }),
+
+ map(([event, assetIdFromStudy]) => {
+ // Use the study point's asset if one has been clicked.
+ if (assetIdFromStudy != null) {
+ return [assetIdFromStudy];
+ }
+
+ // Otherwise, extract the assetIds of all overlapping study geometries that have been clicked.
+ const assetIds = new Set();
this.map.forEachFeatureAtPixel(
event.pixel,
(feature): void => {
@@ -282,16 +342,16 @@ export class MapController {
if (featureId == null) {
return;
}
- const asset = this.assetsByStudyId.get(`${featureId}`);
- if (asset != null) {
- assetIds.push(asset.assetId);
+ const assetId = this.assetIdsByStudyId.get(`${featureId}`);
+ if (assetId != null) {
+ assetIds.add(assetId);
}
},
{
layerFilter: (layer) => layer === this.layers.assets,
}
);
- return assetIds;
+ return [...assetIds];
})
);
}
@@ -305,21 +365,57 @@ export class MapController {
// Extract the ids of the assets that have been hovered.
map((features) => {
+ const viewExtent = this.map.getView().calculateExtent(this.map.getSize());
const assetIds: number[] = [];
for (const feature of features) {
const featureId = feature.getId();
if (featureId == null) {
continue;
}
- const asset = this.assetsByStudyId.get(`${featureId}`);
- if (asset != null) {
- assetIds.push(asset.assetId);
+
+ // Ignore geometries that fill up the entire view.
+ const geometry = feature.getGeometry();
+ if (geometry != null && containsExtent(geometry.getExtent(), viewExtent)) {
+ continue;
+ }
+
+ const assetId = this.assetIdsByStudyId.get(`${featureId}`);
+ if (assetId != null) {
+ assetIds.push(assetId);
}
}
return assetIds;
})
);
}
+
+ private resetActiveAssetStyle(): void {
+ // If we have no active asset, or if the active asset is also contained in the currently visible assets,
+ // then we either can't or don't need to show the asset's study points.
+ if (this.activeAsset == null || this.assetsById.has(this.activeAsset.assetId)) {
+ return;
+ }
+ for (const study of this.activeAsset.studies) {
+ const feature = this.sources.studies.getFeatureById(study.studyId);
+ if (feature != null) {
+ this.unhideFeature(feature);
+ }
+ }
+ }
+
+ private hideFeature(feature: Feature): void {
+ feature.set('previousStyle', feature.getStyle());
+ feature.setStyle(featureStyles.hidden);
+ }
+
+ private unhideFeature(feature: Feature): void {
+ const previousStyle = feature.get('previousStyle');
+ if (previousStyle == null) {
+ return;
+ }
+ feature.setStyle(previousStyle);
+ feature.unset('previousStyle');
+ }
}
interface MapLayers {
@@ -398,12 +494,17 @@ const makeStudyFeature = (study: Study, styles: { point: Style; polygon: Style;
return [new Point(olCoordsFromLV95(study.geom.coord)), styles.point];
case 'LineString':
return [new LineString(study.geom.coords.map(olCoordsFromLV95)), styles.lineString];
- case 'Polygon':
- return [new Polygon([study.geom.coords.map(olCoordsFromLV95)]), styles.polygon];
+ case 'Polygon': {
+ const polygon = new Polygon([study.geom.coords.map(olCoordsFromLV95)]);
+ const style = styles.polygon.clone();
+ style.setZIndex((style.getZIndex() ?? 0) + 1 / polygon.getArea());
+ return [polygon, style];
+ }
}
})();
const feature = new Feature({ geometry });
+
feature.setId(study.studyId);
feature.setStyle(style);
feature.setProperties({ 'swisstopo.type': 'StudyGeometry' });
@@ -419,6 +520,7 @@ const zoomToStudies = (map: OlMap, studies: Study[]): void => {
const size = map.getSize();
const oldCenter = view.getCenter();
const oldZoom = view.getZoom();
+
if (size == null) {
return;
}
diff --git a/libs/asset-viewer/src/lib/models/all-study-dto.ts b/libs/asset-viewer/src/lib/models/all-study-dto.ts
index ab9f72e5..8641309b 100644
--- a/libs/asset-viewer/src/lib/models/all-study-dto.ts
+++ b/libs/asset-viewer/src/lib/models/all-study-dto.ts
@@ -5,12 +5,14 @@ import { Equals, assert } from 'tsafe';
export const AllStudyDTO = D.struct({
studyId: D.string,
+ assetId: D.number,
isPoint: D.boolean,
centroid: LV95,
});
export const eqAllStudyDTO = TEq.struct({
studyId: TEq.string,
+ assetId: TEq.number,
isPoint: TEq.boolean,
centroid: eqLV95,
});
diff --git a/libs/client-shared/src/lib/utils/map.ts b/libs/client-shared/src/lib/utils/map.ts
index 122d3e3a..c4287a80 100644
--- a/libs/client-shared/src/lib/utils/map.ts
+++ b/libs/client-shared/src/lib/utils/map.ts
@@ -80,17 +80,16 @@ export const makeRhombusImage = (radius: number) =>
radius,
angle: 0,
fill: new Fill({ color: '#194ed0' }),
- stroke: new Stroke({ color: 'black' }),
+ stroke: new Stroke({ color: '#194ed0' }),
});
export const featureStyles = {
hidden: new Style(undefined),
-
point: new Style({
image: new Circle({
- radius: 4,
+ radius: 10,
fill: new Fill({ color: '#194ed0' }),
- stroke: new Stroke({ color: 'black' }),
+ stroke: new Stroke({ color: '#194ed0' }),
}),
}),
rhombus: new Style({
@@ -102,6 +101,7 @@ export const featureStyles = {
stroke: new Stroke({ color: 'red', width: 2.5 }),
fill: new Fill({ color: 'transparent' }),
}),
+ zIndex: 3,
}),
bigPointAsset: new Style({
image: new Circle({
@@ -128,6 +128,7 @@ export const featureStyles = {
polygon: new Style({
stroke: new Stroke({ color: 'red', width: 2.5 }),
fill: new Fill({ color: 'transparent' }),
+ zIndex: 1,
}),
polygonAsset: new Style({
stroke: new Stroke({ color: 'red', width: 3 }),
@@ -148,6 +149,7 @@ export const featureStyles = {
lineString: new Style({
stroke: new Stroke({ color: 'red', width: 3 }),
fill: new Fill({ color: 'transparent' }),
+ zIndex: 2,
}),
lineStringAsset: new Style({
stroke: new Stroke({ color: 'red', width: 3 }),
diff --git a/libs/shared/src/lib/models/all-study.ts b/libs/shared/src/lib/models/all-study.ts
index 4c61a145..d185fe43 100644
--- a/libs/shared/src/lib/models/all-study.ts
+++ b/libs/shared/src/lib/models/all-study.ts
@@ -4,6 +4,7 @@ import { LV95FromSpaceSeparatedString } from './lv95';
export const AllStudyDTOFromAPI = D.struct({
studyId: D.string,
+ assetId: D.number,
isPoint: D.boolean,
centroid: LV95FromSpaceSeparatedString,
});