Skip to content

Commit

Permalink
feat(workspace): can now detect geoservice properties (wms...) to add…
Browse files Browse the repository at this point in the history
…/remove layer from workspace

* feat(regexService): config define regex list/object

* feat(geoProperties): method to detect if a feature contains geoService url and column (wms, arcgis...)

* feat(workspace): can now detect geoservice properties (wms...) to add/remove layer from workspace

* refactor

* typo

* typo

* refactor(geo-properties): prevent getcapabilities

* feat(button): button to add layers from workspace

* lint

* lint

* fix(core): empty intercepted error

* fix(add layer): undefined event

* wip

* refactor(feature-details): revert changes

* wip

* wip

* lint

* perf(geo-properties): fix some performance issues

* wip

---------

Co-authored-by: Pierre-Etienne Lord <pe_lord@pm.me>
Co-authored-by: Pierre-Étienne Lord <pe_lord@yahoo.ca>
Co-authored-by: idisavi <idriss.savadogo@transports.gouv.qc.ca>
  • Loading branch information
4 people authored Jun 5, 2023
1 parent 3998dcc commit f49984f
Show file tree
Hide file tree
Showing 13 changed files with 467 additions and 19 deletions.
4 changes: 4 additions & 0 deletions packages/geo/src/lib/feature/shared/feature.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ export interface FeatureStoreLoadingStrategyOptions
motion?: FeatureMotion;
}

export interface FeatureStorePropertyTypeStrategyOptions
extends FeatureStoreStrategyOptions {
map: IgoMap
}
export interface FeatureStoreInMapExtentStrategyOptions
extends FeatureStoreStrategyOptions {}

Expand Down
225 changes: 225 additions & 0 deletions packages/geo/src/lib/feature/shared/strategies/geo-properties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { EntityStoreStrategy } from '@igo2/common';
import { CapabilitiesService } from '../../../datasource/shared/capabilities.service';
import { FeatureStore } from '../store';
import { Feature, FeatureStorePropertyTypeStrategyOptions } from '../feature.interfaces';
import { Subscription, debounceTime, pairwise } from 'rxjs';
import { PropertyTypeDetectorService } from '../../../utils/propertyTypeDetector/propertyTypeDetector.service';
import { ObjectUtils } from '@igo2/utils';
import { generateIdFromSourceOptions } from '../../../utils/id-generator';
import { IgoMap } from '../../../map/shared/map';
import { Layer } from '../../../layer/shared/layers/layer';
import { GeoServiceDefinition } from '../../../utils';

/**
* This strategy maintain the store features updated to detect geoproperties
* (wms/arcgis... layer and url).
* The features's state inside the map are tagged haveGeoServiceProperties = true;
*/
export class GeoPropertiesStrategy extends EntityStoreStrategy {

/**
* Subscription to the store's OL source changes
*/
private stores$$ = new Map<FeatureStore, string>();
private states$$: Subscription[] = [];
private empty$$: Subscription;

/**
* The map the layer is bound to
*/
private readonly map: IgoMap;

constructor(
protected options: FeatureStorePropertyTypeStrategyOptions,
private propertyTypeDetectorService: PropertyTypeDetectorService,
private capabilitiesService: CapabilitiesService) {
super(options);
this.map = options.map;
}

/**
* Bind this strategy to a store and start watching for Ol source changes
* @param store Feature store
*/
bindStore(store: FeatureStore) {
super.bindStore(store);
if (this.active === true) {
this.watchStore(store);
}
}

/**
* Unbind this strategy from a store and stop watching for Ol source changes
* @param store Feature store
*/
unbindStore(store: FeatureStore) {
super.unbindStore(store);
if (this.active === true) {
this.unwatchStore(store);
}
}

/**
* Start watching all stores already bound to that strategy at once.
* @internal
*/
protected doActivate() {
this.stores.forEach((store: FeatureStore) => this.watchStore(store));
}

/**
* Stop watching all stores bound to that strategy
* @internal
*/
protected doDeactivate() {
this.unwatchAll();
}

/**
* Watch for a store's OL source changes
* @param store Feature store
*/
private watchStore(store: FeatureStore) {
if (this.stores$$.has(store)) {
return;
}

this.updateEntitiesPropertiesState(store);
this.states$$.push(
this.map.layers$.pipe(
debounceTime(750),
pairwise())
.subscribe(([prevLayers, currentLayers]) => {
let prevLayersId;
if (prevLayers) {
prevLayersId = prevLayers.map(l => l.id);
}
const layers = currentLayers.filter(l => !prevLayersId.includes(l.id));
this.updateEntitiesPropertiesState(store, layers);
}));
this.states$$.push(
store.entities$
.pipe(debounceTime(750))
.subscribe((a) => {
this.updateEntitiesPropertiesState(store);
}));

}

private updateEntitiesPropertiesState(store: FeatureStore, layers?: Layer[]) {
const layersId = this.map.layers.map(l => l.id);
let entities: Feature[] = [];
if (layers) {
entities = store.entities$.value;
} else {
const allSV = store.stateView.all();
entities = allSV.length ? store.stateView.all().filter(s => !s.state.geoService).map(e => e.entity) : store.entities$.value;
}
const sampling = entities.length >= 250 ? 250 : entities.length;
const firstN = entities.slice(0, sampling);
let allKeys = [];
firstN.map(e => {
allKeys = allKeys.concat(Object.keys(e.properties || {}));
});
allKeys = [...new Set(allKeys)];
const distinctValues = {};
allKeys.map(k => {
distinctValues[k] = [...new Set(entities.map(item => item.properties[k]))];
});
const containGeoServices = {};
Object.entries(distinctValues).forEach((entry: [string, []]) => {
const [key, values] = entry;
const valuedAreGeoservices = values.filter(value => this.propertyTypeDetectorService.isGeoService(value));
if (valuedAreGeoservices?.length) {
containGeoServices[key] = valuedAreGeoservices;
}
});
interface GeoServiceAssociation { url: string, layerNameProperty: string, urlProperty: string, geoService: GeoServiceDefinition }
const geoServiceAssociations: GeoServiceAssociation[] = [];
Object.entries(containGeoServices).forEach((entry: [string, []]) => {
const [key, values] = entry;
values.map(value => {
const geoService = this.propertyTypeDetectorService.getGeoService(value, allKeys);
const propertiesForLayerName = allKeys.filter(p => geoService.propertiesForLayerName.includes(p));
// providing the the first matching regex;
const propertyForLayerName = propertiesForLayerName.length ? propertiesForLayerName[0] : undefined;
if (propertyForLayerName) {
geoServiceAssociations.push({ url: value, urlProperty: key, layerNameProperty: propertyForLayerName, geoService });

}
});
});
const geoServiceStates: { geoServiceAssociation: GeoServiceAssociation, state: any }[] = [];
geoServiceAssociations.map(geoServiceAssociation => {
const url = geoServiceAssociation.url;
const type = geoServiceAssociation.geoService.type || 'wms';
distinctValues[geoServiceAssociation.layerNameProperty].map(layerName => {
let appliedLayerName = layerName;
let arcgisLayerName = undefined;
if (['arcgisrest', 'imagearcgisrest', 'tilearcgisrest'].includes(type)) {
appliedLayerName = undefined;
arcgisLayerName = layerName;
}
const so = ObjectUtils.removeUndefined({
sourceOptions: {
type: type || 'wms',
url,
optionsFromCapabilities: true,
optionsFromApi: true,
params: {
LAYERS: appliedLayerName,
LAYER: arcgisLayerName
}
}
});
const potentialLayerId = generateIdFromSourceOptions(so.sourceOptions);
geoServiceStates.push({
geoServiceAssociation,
state:
{
added: layersId.find(l => l === potentialLayerId) !== undefined,
haveGeoServiceProperties: true,
type,
url,
layerName
}
});
});
});
geoServiceStates.map(geoServiceState => {
const urlProperty = geoServiceState.geoServiceAssociation.urlProperty;
const layerNameProperty = geoServiceState.geoServiceAssociation.layerNameProperty;

const urlValue = geoServiceState.state.url;
const layerNameValue = geoServiceState.state.layerName;
const entitiesToProcess = entities.filter(e =>
e.properties[urlProperty] === urlValue &&
e.properties[layerNameProperty] === layerNameValue
);
const ns = {
geoService: geoServiceState.state
};
store.state.updateMany(entitiesToProcess, ns, true);
});
}

/**
* Stop watching for a store's OL source changes
* @param store Feature store
*/
private unwatchStore(store: FeatureStore) {
const key = this.stores$$.get(store);
if (key !== undefined) {
this.stores$$.delete(store);
}
}

/**
* Stop watching for OL source changes in all stores.
*/
private unwatchAll() {
this.stores$$.clear();
this.states$$.map(state => state.unsubscribe());
if (this.empty$$) { this.empty$$.unsubscribe(); }
}
}
1 change: 1 addition & 0 deletions packages/geo/src/lib/feature/shared/strategies/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './in-map-extent';
export * from './in-map-resolution';
export * from './geo-properties';
export * from './loading';
export * from './loading-layer';
export * from './selection';
Expand Down
14 changes: 11 additions & 3 deletions packages/geo/src/lib/map/shared/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import olLayer from 'ol/layer/Layer';
import olSource from 'ol/source/Source';

import proj4 from 'proj4';
import { BehaviorSubject, skipWhile, Subject } from 'rxjs';
import { BehaviorSubject, pairwise, skipWhile, Subject } from 'rxjs';

import { SubjectStatus } from '@igo2/utils';

Expand Down Expand Up @@ -141,11 +141,19 @@ export class IgoMap {
if (this.geolocationController) {
this.geolocationController.updateGeolocationOptions(this.mapViewOptions);
}
this.layers$.subscribe((layers) => {
this.layers$
.pipe(pairwise())
.subscribe(([prevLayers, currentLayers]) => {
let prevLayersId;
if (prevLayers){
prevLayersId = prevLayers.map(l => l.id);
}
const layers = currentLayers.filter(l => !prevLayersId.includes(l.id));

for (const layer of layers) {
if (layer.options.linkedLayers) {
layer.ol.once('postrender', () => {
initLayerSyncFromRootParentLayers(this, this.layers);
initLayerSyncFromRootParentLayers(this, layers);
});
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/geo/src/lib/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './googleLinks';
export * from './id-generator';
export * from './osmLinks';
export * from './propertyTypeDetector';
2 changes: 2 additions & 0 deletions packages/geo/src/lib/utils/propertyTypeDetector/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './propertyTypeDetector.service';
export * from './propertyTypeDetector.interface';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface GeoServiceDefinition {
url: string;
type:
| 'wms'
| 'wfs'
| 'vector'
| 'wmts'
| 'xyz'
| 'osm'
| 'tiledebug'
| 'carto'
| 'arcgisrest'
| 'imagearcgisrest'
| 'tilearcgisrest'
| 'websocket'
| 'mvt'
| 'cluster';
propertiesForLayerName: string[]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Injectable } from '@angular/core';
import { RegexService } from '@igo2/core';
import { GeoServiceDefinition } from './propertyTypeDetector.interface';

@Injectable({
providedIn: 'root'
})
export class PropertyTypeDetectorService {

public geoServiceRegexes: GeoServiceDefinition[];

constructor(
private regexService: RegexService
) {
this.geoServiceRegexes = this.getGeoServiceRegexes();
}

getPropertyType(value) {
return typeof value;
}

private isUrl(value): boolean {
const regex = /^https?:\/\//;
return regex.test(value.toString());
}

isGeoService(value): boolean {
let isGeoService = false;
if (!this.isUrl) {
return;
}
for (const geoServiceRegex of this.geoServiceRegexes) {
const domainRegex = new RegExp(geoServiceRegex.url);
if (domainRegex.test(value)) {
isGeoService = true;
break;
}
}
return isGeoService;
}

getGeoService(url: string, availableProperties: string[]): GeoServiceDefinition {
if (!this.isGeoService(url)) {
return;
}
let matchingGeoservice: GeoServiceDefinition;
for (const geoServiceRegex of this.geoServiceRegexes) {
const domainRegex = new RegExp(geoServiceRegex.url);
if (domainRegex.test(url)) {
// providing the the first matching regex;
const matchingProperties = availableProperties.filter(p => geoServiceRegex.propertiesForLayerName.includes(p));
matchingGeoservice = matchingProperties ? geoServiceRegex: undefined;
if (matchingGeoservice) { break; }
}
}
return matchingGeoservice;
}


private getGeoServiceRegexes(): GeoServiceDefinition[] {
return this.regexService.get('geoservice') as GeoServiceDefinition[] | [];
}

}
Loading

0 comments on commit f49984f

Please sign in to comment.