Skip to content

Commit

Permalink
feat(strategy): move strategies from feature to entity store and add …
Browse files Browse the repository at this point in the history
…filter selection strategy (#500)

* feat(entity table): connect entity tables to the state view

* style(*): remove now useless entity watcher

* feat(entity selector): fix issue with empty value

* wip(strategy): move strategies from feature to entity store and add filter selection strategy

* lint(*)
  • Loading branch information
cbourget authored and mbarbeau committed Nov 5, 2019
1 parent 1d8252b commit 2a83591
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 123 deletions.
2 changes: 2 additions & 0 deletions packages/common/src/lib/entity/shared/entity.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export interface EntityStateManagerOptions {
store?: EntityStore<object>;
}

export interface EntityStoreStrategyOptions {}

export interface EntityTransactionOptions {
getKey?: (entity: object) => EntityKey;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/common/src/lib/entity/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export * from './state';
export * from './watcher';
export * from './transaction';
export * from './view';

export * from './strategies';
78 changes: 77 additions & 1 deletion packages/common/src/lib/entity/shared/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<E extends object, S extends EntityState = EntityState> {
export class EntityStore<E extends object = object, S extends EntityState = EntityState> {

/**
* Observable of the raw entities
Expand Down Expand Up @@ -66,6 +67,11 @@ export class EntityStore<E extends object, S extends EntityState = EntityState>
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;
Expand Down Expand Up @@ -196,6 +202,76 @@ export class EntityStore<E extends object, S extends EntityState = EntityState>
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<EntityStore, string> = 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<object>) => {
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);
}
}
2 changes: 2 additions & 0 deletions packages/common/src/lib/entity/shared/strategies/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './strategy';
export * from './filter-selection';
Original file line number Diff line number Diff line change
@@ -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<boolean> = 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.
Expand All @@ -40,7 +38,7 @@ export class FeatureStoreStrategy {
if (this.active === true) {
this.doDeactivate();
}
this.active = true;
this.active$.next(true);
this.doActivate();
}

Expand All @@ -49,15 +47,15 @@ export class FeatureStoreStrategy {
* and activated again.
*/
deactivate() {
this.active = false;
this.active$.next(false);
this.doDeactivate();
}

/**
* 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);
}
Expand All @@ -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);
Expand Down
16 changes: 6 additions & 10 deletions packages/common/src/lib/workspace/shared/workspace.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Subscription, BehaviorSubject, Subject } from 'rxjs';
import { debounceTime } from 'rxjs/operators';

import { ActionStore } from '../../action';
import { Widget } from '../../widget';
Expand Down Expand Up @@ -34,11 +33,6 @@ export class Workspace<E extends object = object> {
*/
private entities$$: Subscription;

/**
* Whether this workspace is active
*/
private active: boolean = false;

/**
* State change that trigger an update of the actions availability
*/
Expand Down Expand Up @@ -87,9 +81,11 @@ export class Workspace<E extends object = object> {
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<boolean> = new BehaviorSubject(false);

/**
* Activate the workspace. By doing that, the workspace will observe
Expand All @@ -100,7 +96,7 @@ export class Workspace<E extends object = object> {
if (this.active === true) {
this.deactivate();
}
this.active = true;
this.active$.next(true);

if (this.entityStore !== undefined) {
this.entities$$ = this.entityStore.stateView.all$()
Expand All @@ -114,7 +110,7 @@ export class Workspace<E extends object = object> {
* Deactivate the workspace. Unsubcribe to the selected entity.
*/
deactivate() {
this.active = false;
this.active$.next(false);
this.deactivateWidget();

if (this.entities$$ !== undefined) {
Expand Down
4 changes: 2 additions & 2 deletions packages/geo/src/lib/feature/shared/feature.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 2a83591

Please sign in to comment.