diff --git a/packages/common/src/lib/entity/shared/entity.interfaces.ts b/packages/common/src/lib/entity/shared/entity.interfaces.ts index 9d5fbfd5ea..b62c9d72f6 100644 --- a/packages/common/src/lib/entity/shared/entity.interfaces.ts +++ b/packages/common/src/lib/entity/shared/entity.interfaces.ts @@ -29,6 +29,8 @@ export interface EntityStateManagerOptions { store?: EntityStore; } +export interface EntityStoreStrategyOptions {} + export interface EntityTransactionOptions { getKey?: (entity: object) => EntityKey; } diff --git a/packages/common/src/lib/entity/shared/index.ts b/packages/common/src/lib/entity/shared/index.ts index 374cea84a6..2613ca8984 100644 --- a/packages/common/src/lib/entity/shared/index.ts +++ b/packages/common/src/lib/entity/shared/index.ts @@ -6,3 +6,5 @@ export * from './state'; export * from './watcher'; export * from './transaction'; export * from './view'; + +export * from './strategies'; diff --git a/packages/common/src/lib/entity/shared/store.ts b/packages/common/src/lib/entity/shared/store.ts index 6920a48b8b..e2f564c6fd 100644 --- a/packages/common/src/lib/entity/shared/store.ts +++ b/packages/common/src/lib/entity/shared/store.ts @@ -4,13 +4,14 @@ import { EntityStateManager } from './state'; import { EntityView } from './view'; import { EntityKey, EntityState, EntityRecord, EntityStoreOptions } from './entity.interfaces'; import { getEntityId, getEntityProperty } from './entity.utils'; +import { EntityStoreStrategy } from './strategies/strategy'; /** * An entity store class holds any number of entities * as well as their state. It can be observed, filtered and sorted and * provides methods to insert, update or delete entities. */ -export class EntityStore { +export class EntityStore { /** * Observable of the raw entities @@ -66,6 +67,11 @@ export class EntityStore get pristine(): boolean { return this._pristine; } private _pristine: boolean = true; + /** + * Strategies + */ + private strategies: EntityStoreStrategy[] = []; + constructor(entities: E[], options: EntityStoreOptions = {}) { this.getKey = options.getKey ? options.getKey : getEntityId; this.getProperty = options.getProperty ? options.getProperty : getEntityProperty; @@ -196,6 +202,76 @@ export class EntityStore this.next(); } + /** + * Add a strategy to this store + * @param strategy Entity store strategy + * @returns Entity store + */ + addStrategy(strategy: EntityStoreStrategy, activate: boolean = false): EntityStore { + const existingStrategy = this.strategies.find((_strategy: EntityStoreStrategy) => { + return strategy.constructor === _strategy.constructor; + }); + if (existingStrategy !== undefined) { + throw new Error('A strategy of this type already exists on that EntityStore.'); + } + + this.strategies.push(strategy); + strategy.bindStore(this); + + if (activate === true) { + strategy.activate(); + } + + return this; + } + + /** + * Remove a strategy from this store + * @param strategy Entity store strategy + * @returns Entity store + */ + removeStrategy(strategy: EntityStoreStrategy): EntityStore { + const index = this.strategies.indexOf(strategy); + if (index >= 0) { + this.strategies.splice(index, 1); + strategy.unbindStore(this); + } + return this; + } + + /** + * Return strategies of a given type + * @param type Entity store strategy class + * @returns Strategies + */ + getStrategyOfType(type: typeof EntityStoreStrategy): EntityStoreStrategy { + return this.strategies.find((strategy: EntityStoreStrategy) => { + return strategy instanceof type; + }); + } + + /** + * Activate strategies of a given type + * @param type Entity store strategy class + */ + activateStrategyOfType(type: typeof EntityStoreStrategy) { + const strategy = this.getStrategyOfType(type); + if (strategy !== undefined) { + strategy.activate(); + } + } + + /** + * Deactivate strategies of a given type + * @param type Entity store strategy class + */ + deactivateStrategyOfType(type: typeof EntityStoreStrategy) { + const strategy = this.getStrategyOfType(type); + if (strategy !== undefined) { + strategy.deactivate(); + } + } + /** * Generate a complete index of all the entities * @param entities Entities diff --git a/packages/common/src/lib/entity/shared/strategies/filter-selection.ts b/packages/common/src/lib/entity/shared/strategies/filter-selection.ts new file mode 100644 index 0000000000..879827d59e --- /dev/null +++ b/packages/common/src/lib/entity/shared/strategies/filter-selection.ts @@ -0,0 +1,94 @@ +import { EntityRecord } from '../entity.interfaces'; +import { EntityStore } from '../store'; +import { EntityStoreStrategy } from './strategy'; + +/** + * When active, this strategy filters a store's stateView to return + * selected entities only. + */ +export class EntityStoreFilterSelectionStrategy extends EntityStoreStrategy { + + /** + * Store / filter ids map + */ + private filters: Map = new Map(); + + /** + * Bind this strategy to a store and start filtering it + * @param store Entity store + */ + bindStore(store: EntityStore) { + super.bindStore(store); + if (this.active === true) { + this.filterStore(store); + } + } + + /** + * Unbind this strategy from a store and stop filtering it + * @param store Entity store + */ + unbindStore(store: EntityStore) { + super.unbindStore(store); + if (this.active === true) { + this.unfilterStore(store); + } + } + + /** + * Start filtering all stores + * @internal + */ + protected doActivate() { + this.filterAll(); + } + + /** + * Stop filtering all stores + * @internal + */ + protected doDeactivate() { + this.unfilterAll(); + } + + /** + * Filter all stores + */ + private filterAll() { + this.stores.forEach((store: EntityStore) => this.filterStore(store)); + } + + /** + * Unfilter all stores + */ + private unfilterAll() { + this.stores.forEach((store: EntityStore) => this.unfilterStore(store)); + } + + /** + * Filter a store and add it to the filters map + */ + private filterStore(store: EntityStore) { + if (this.filters.has(store)) { + return; + } + + const filter = (record: EntityRecord) => { + return record.state.selected === true; + }; + this.filters.set(store, store.stateView.addFilter(filter)); + } + + /** + * Unfilter a store and delete it from the filters map + */ + private unfilterStore(store: EntityStore) { + const filterId = this.filters.get(store); + if (filterId === undefined) { + return; + } + + store.stateView.removeFilter(filterId); + this.filters.delete(store); + } +} diff --git a/packages/common/src/lib/entity/shared/strategies/index.ts b/packages/common/src/lib/entity/shared/strategies/index.ts new file mode 100644 index 0000000000..534dcb1f71 --- /dev/null +++ b/packages/common/src/lib/entity/shared/strategies/index.ts @@ -0,0 +1,2 @@ +export * from './strategy'; +export * from './filter-selection'; diff --git a/packages/geo/src/lib/feature/shared/strategies/strategy.ts b/packages/common/src/lib/entity/shared/strategies/strategy.ts similarity index 58% rename from packages/geo/src/lib/feature/shared/strategies/strategy.ts rename to packages/common/src/lib/entity/shared/strategies/strategy.ts index c957ec7b34..fa8d7d0c59 100644 --- a/packages/geo/src/lib/feature/shared/strategies/strategy.ts +++ b/packages/common/src/lib/entity/shared/strategies/strategy.ts @@ -1,37 +1,35 @@ -import { FeatureStoreStrategyOptions } from '../feature.interfaces'; -import { FeatureStore } from '../store'; +import { BehaviorSubject } from 'rxjs'; + +import { EntityStoreStrategyOptions } from '../entity.interfaces'; +import { EntityStore } from '../store'; /** - * Strategies or responsible of synchronizing a feature store and a layer. - * A strategy can be shared among multiple stores. Sharing a strategy - * is a good idea when multiple strategies would have on cancelling effect - * on each other. + * Entity store strategies. They can do pretty much anything during a store's + * lifetime. For example, they may act as triggers when something happens. + * Sharing a strategy is a good idea when multiple strategies would have + * on cancelling effect on each other. * * At creation, strategy is inactive and needs to be manually activated. */ -export class FeatureStoreStrategy { +export class EntityStoreStrategy { /** * Feature store * @internal */ - protected stores: FeatureStore[] = []; + protected stores: EntityStore[] = []; /** * Whether this strategy is active * @internal */ - protected active = false; + get active(): boolean { return this.active$.value; } + readonly active$: BehaviorSubject = new BehaviorSubject(false); - constructor(protected options: FeatureStoreStrategyOptions = {}) { + constructor(protected options: EntityStoreStrategyOptions = {}) { this.options = options; } - /** - * Whether this strategy is active - */ - isActive(): boolean { return this.active; } - /** * Activate the strategy. If it's already active, it'll be deactivated * and activated again. @@ -40,7 +38,7 @@ export class FeatureStoreStrategy { if (this.active === true) { this.doDeactivate(); } - this.active = true; + this.active$.next(true); this.doActivate(); } @@ -49,7 +47,7 @@ export class FeatureStoreStrategy { * and activated again. */ deactivate() { - this.active = false; + this.active$.next(false); this.doDeactivate(); } @@ -57,7 +55,7 @@ export class FeatureStoreStrategy { * Bind this strategy to a store * @param store Feature store */ - bindStore(store: FeatureStore) { + bindStore(store: EntityStore) { if (this.stores.indexOf(store) < 0) { this.stores.push(store); } @@ -67,7 +65,7 @@ export class FeatureStoreStrategy { * Unbind this strategy from store * @param store Feature store */ - unbindStore(store: FeatureStore) { + unbindStore(store: EntityStore) { const index = this.stores.indexOf(store); if (index >= 0) { this.stores.splice(index, 1); diff --git a/packages/common/src/lib/workspace/shared/workspace.ts b/packages/common/src/lib/workspace/shared/workspace.ts index 7af7d5d9ff..531fea9845 100644 --- a/packages/common/src/lib/workspace/shared/workspace.ts +++ b/packages/common/src/lib/workspace/shared/workspace.ts @@ -1,5 +1,4 @@ import { Subscription, BehaviorSubject, Subject } from 'rxjs'; -import { debounceTime } from 'rxjs/operators'; import { ActionStore } from '../../action'; import { Widget } from '../../widget'; @@ -34,11 +33,6 @@ export class Workspace { */ private entities$$: Subscription; - /** - * Whether this workspace is active - */ - private active: boolean = false; - /** * State change that trigger an update of the actions availability */ @@ -87,9 +81,11 @@ export class Workspace { constructor(protected options: WorkspaceOptions) {} /** - * Whether this workspace is active + * Whether this strategy is active + * @internal */ - isActive(): boolean { return this.active; } + get active(): boolean { return this.active$.value; } + readonly active$: BehaviorSubject = new BehaviorSubject(false); /** * Activate the workspace. By doing that, the workspace will observe @@ -100,7 +96,7 @@ export class Workspace { if (this.active === true) { this.deactivate(); } - this.active = true; + this.active$.next(true); if (this.entityStore !== undefined) { this.entities$$ = this.entityStore.stateView.all$() @@ -114,7 +110,7 @@ export class Workspace { * Deactivate the workspace. Unsubcribe to the selected entity. */ deactivate() { - this.active = false; + this.active$.next(false); this.deactivateWidget(); if (this.entities$$ !== undefined) { diff --git a/packages/geo/src/lib/feature/shared/feature.interfaces.ts b/packages/geo/src/lib/feature/shared/feature.interfaces.ts index ccbde14015..6cd13b6fdc 100644 --- a/packages/geo/src/lib/feature/shared/feature.interfaces.ts +++ b/packages/geo/src/lib/feature/shared/feature.interfaces.ts @@ -2,7 +2,7 @@ import { FormGroup } from '@angular/forms'; import { GeoJsonGeometryTypes } from 'geojson'; -import { EntityKey, EntityStoreOptions } from '@igo2/common'; +import { EntityKey, EntityStoreOptions, EntityStoreStrategyOptions } from '@igo2/common'; import { VectorLayer } from '../../layer'; import { IgoMap } from '../../map'; @@ -41,7 +41,7 @@ export interface FeatureStoreOptions extends EntityStoreOptions { layer?: VectorLayer; } -export interface FeatureStoreStrategyOptions { +export interface FeatureStoreStrategyOptions extends EntityStoreStrategyOptions { // When the store moves features into view, the view extent, which is also the features extent, // is scaled by those factors, effectively resulting in a decentered view or a more zoomed in/out view. // These factors are applied to the top, right, bottom and left directions, in that order. diff --git a/packages/geo/src/lib/feature/shared/store.ts b/packages/geo/src/lib/feature/shared/store.ts index 4d89c027a6..3525de3782 100644 --- a/packages/geo/src/lib/feature/shared/store.ts +++ b/packages/geo/src/lib/feature/shared/store.ts @@ -13,7 +13,6 @@ import { IgoMap } from '../../map'; import { FeatureMotion } from './feature.enums'; import { Feature, FeatureStoreOptions } from './feature.interfaces'; import { computeOlFeaturesDiff, featureFromOl, featureToOl, moveToOlFeatures } from './feature.utils'; -import { FeatureStoreStrategy } from './strategies/strategy'; /** * The class is a specialized version of an EntityStore that stores @@ -22,12 +21,6 @@ import { FeatureStoreStrategy } from './strategies/strategy'; */ export class FeatureStore extends EntityStore { - /** - * Feature store strategies responsible of synchronizing the store - * and the layer - */ - strategies: FeatureStoreStrategy[] = []; - /** * Vector layer to display the features on */ @@ -60,76 +53,6 @@ export class FeatureStore extends EntityStore { return this; } - /** - * Add a strategy to this store - * @param strategy Feature store strategy - * @returns Feature store - */ - addStrategy(strategy: FeatureStoreStrategy, activate: boolean = false): FeatureStore { - const existingStrategy = this.strategies.find((_strategy: FeatureStoreStrategy) => { - return strategy.constructor === _strategy.constructor; - }); - if (existingStrategy !== undefined) { - throw new Error('A strategy of this type already exists on that FeatureStore.'); - } - - this.strategies.push(strategy); - strategy.bindStore(this); - - if (activate === true) { - strategy.activate(); - } - - return this; - } - - /** - * Remove a strategy from this store - * @param strategy Feature store strategy - * @returns Feature store - */ - removeStrategy(strategy: FeatureStoreStrategy): FeatureStore { - const index = this.strategies.indexOf(strategy); - if (index >= 0) { - this.strategies.splice(index, 1); - strategy.unbindStore(this); - } - return this; - } - - /** - * Return strategies of a given type - * @param type Feature store strategy class - * @returns Strategies - */ - getStrategyOfType(type: typeof FeatureStoreStrategy): FeatureStoreStrategy { - return this.strategies.find((strategy: FeatureStoreStrategy) => { - return strategy instanceof type; - }); - } - - /** - * Activate strategies of a given type - * @param type Feature store strategy class - */ - activateStrategyOfType(type: typeof FeatureStoreStrategy) { - const strategy = this.getStrategyOfType(type); - if (strategy !== undefined) { - strategy.activate(); - } - } - - /** - * Deactivate strategies of a given type - * @param type Feature store strategy class - */ - deactivateStrategyOfType(type: typeof FeatureStoreStrategy) { - const strategy = this.getStrategyOfType(type); - if (strategy !== undefined) { - strategy.deactivate(); - } - } - /** * Set the layer's features and perform a motion to make them visible. Strategies * make extensive use of that method. diff --git a/packages/geo/src/lib/feature/shared/strategies/index.ts b/packages/geo/src/lib/feature/shared/strategies/index.ts index 871c770d17..229fe518b6 100644 --- a/packages/geo/src/lib/feature/shared/strategies/index.ts +++ b/packages/geo/src/lib/feature/shared/strategies/index.ts @@ -1,4 +1,3 @@ export * from './loading'; export * from './loading-layer'; export * from './selection'; -export * from './strategy'; diff --git a/packages/geo/src/lib/feature/shared/strategies/loading-layer.ts b/packages/geo/src/lib/feature/shared/strategies/loading-layer.ts index 6e7aab87d7..8a0f83412a 100644 --- a/packages/geo/src/lib/feature/shared/strategies/loading-layer.ts +++ b/packages/geo/src/lib/feature/shared/strategies/loading-layer.ts @@ -1,9 +1,10 @@ import { unByKey } from 'ol/Observable'; import { OlEvent } from 'ol/events/Event'; +import { EntityStoreStrategy } from '@igo2/common'; + import { FeatureStore } from '../store'; import { FeatureStoreLoadingLayerStrategyOptions } from '../feature.interfaces'; -import { FeatureStoreStrategy } from './strategy'; /** * This strategy loads a layer's features into it's store counterpart. @@ -13,7 +14,7 @@ import { FeatureStoreStrategy } from './strategy'; * Important: In it's current state, this strategy is to meant to be combined * with a standard Loading strategy and it would probably cause recursion issues. */ -export class FeatureStoreLoadingLayerStrategy extends FeatureStoreStrategy { +export class FeatureStoreLoadingLayerStrategy extends EntityStoreStrategy { /** * Subscription to the store's OL source changes @@ -30,7 +31,7 @@ export class FeatureStoreLoadingLayerStrategy extends FeatureStoreStrategy { */ bindStore(store: FeatureStore) { super.bindStore(store); - if (this.isActive() === true) { + if (this.active === true) { this.watchStore(store); } } @@ -41,7 +42,7 @@ export class FeatureStoreLoadingLayerStrategy extends FeatureStoreStrategy { */ unbindStore(store: FeatureStore) { super.unbindStore(store); - if (this.isActive() === true) { + if (this.active === true) { this.unwatchStore(store); } } diff --git a/packages/geo/src/lib/feature/shared/strategies/loading.ts b/packages/geo/src/lib/feature/shared/strategies/loading.ts index e3b5f28bc6..59a5ed5307 100644 --- a/packages/geo/src/lib/feature/shared/strategies/loading.ts +++ b/packages/geo/src/lib/feature/shared/strategies/loading.ts @@ -1,9 +1,10 @@ import { Subscription } from 'rxjs'; +import { EntityStoreStrategy } from '@igo2/common'; + import { FeatureMotion } from '../feature.enums'; import { Feature, FeatureStoreLoadingStrategyOptions } from '../feature.interfaces'; import { FeatureStore } from '../store'; -import { FeatureStoreStrategy } from './strategy'; /** * This strategy loads a store's features into it's layer counterpart. @@ -13,7 +14,7 @@ import { FeatureStoreStrategy } from './strategy'; * Important: This strategy observes filtered entities, not raw entities. This * is not configurable yet. */ -export class FeatureStoreLoadingStrategy extends FeatureStoreStrategy { +export class FeatureStoreLoadingStrategy extends EntityStoreStrategy { /** * Subscription to the store's features @@ -30,7 +31,7 @@ export class FeatureStoreLoadingStrategy extends FeatureStoreStrategy { */ bindStore(store: FeatureStore) { super.bindStore(store); - if (this.isActive() === true) { + if (this.active === true) { this.watchStore(store); } } @@ -41,7 +42,7 @@ export class FeatureStoreLoadingStrategy extends FeatureStoreStrategy { */ unbindStore(store: FeatureStore) { super.unbindStore(store); - if (this.isActive() === true) { + if (this.active === true) { this.unwatchStore(store); } } diff --git a/packages/geo/src/lib/feature/shared/strategies/selection.ts b/packages/geo/src/lib/feature/shared/strategies/selection.ts index 03d1f57e1c..1bae53bae0 100644 --- a/packages/geo/src/lib/feature/shared/strategies/selection.ts +++ b/packages/geo/src/lib/feature/shared/strategies/selection.ts @@ -8,7 +8,7 @@ import { unByKey } from 'ol/Observable'; import { Subscription, combineLatest } from 'rxjs'; import { map, debounceTime, skip } from 'rxjs/operators'; -import { EntityKey, EntityRecord } from '@igo2/common'; +import { EntityKey, EntityRecord, EntityStoreStrategy } from '@igo2/common'; import { FeatureDataSource } from '../../../datasource'; import { VectorLayer } from '../../../layer'; @@ -16,7 +16,6 @@ import { IgoMap, ctrlKeyDown } from '../../../map'; import { Feature, FeatureStoreSelectionStrategyOptions } from '../feature.interfaces'; import { FeatureStore } from '../store'; -import { FeatureStoreStrategy } from './strategy'; import { FeatureMotion } from '../feature.enums'; class OlDragSelectInteraction extends OlDragBoxInteraction { @@ -35,7 +34,7 @@ class OlDragSelectInteraction extends OlDragBoxInteraction { * would trigger the strategy of each layer and they would cancel * each other as well as move the map view around needlessly. */ -export class FeatureStoreSelectionStrategy extends FeatureStoreStrategy { +export class FeatureStoreSelectionStrategy extends EntityStoreStrategy { /** * Listener to the map click event that allows selecting a feature @@ -76,7 +75,7 @@ export class FeatureStoreSelectionStrategy extends FeatureStoreStrategy { */ bindStore(store: FeatureStore) { super.bindStore(store); - if (this.isActive() === true) { + if (this.active === true) { // Force reactivation this.activate(); } @@ -89,7 +88,7 @@ export class FeatureStoreSelectionStrategy extends FeatureStoreStrategy { */ unbindStore(store: FeatureStore) { super.unbindStore(store); - if (this.isActive() === true) { + if (this.active === true) { // Force reactivation this.activate(); }