Refactor style cache

This commit is contained in:
Willem Dantuma 2020-02-12 20:38:14 +01:00
parent b83aca7969
commit 6379b64351
9 changed files with 258 additions and 224 deletions

View File

@ -6,7 +6,7 @@
}, },
"dependencies": { "dependencies": {
"ngx-openlayers": "1.0.0-next.13", "ngx-openlayers": "1.0.0-next.13",
"ol": "^6.0.0" "ol": "6.1.1"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/core": "^8.2.0", "@angular/core": "^8.2.0",

View File

@ -4,8 +4,7 @@ import { IMapState } from '../models/map.state';
import { IItemLayer } from '../models/item.layer'; import { IItemLayer } from '../models/item.layer';
import { IQueryState } from '../models/query.state'; import { IQueryState } from '../models/query.state';
import { IItem } from '@farmmaps/common'; import { IItem } from '@farmmaps/common';
import { Feature } from 'ol'; import { Feature,Style } from 'ol';
import { Extent } from 'ol/extent';
export const SETSTATE = '[Map] SetState'; export const SETSTATE = '[Map] SetState';
export const SETMAPSTATE = '[Map] MapState'; export const SETMAPSTATE = '[Map] MapState';
@ -35,6 +34,7 @@ export const SELECTBASELAYER = '[Map] SelectBaseLayers';
export const SELECTOVERLAYLAYER = '[Map] SelectOverlayLayers'; export const SELECTOVERLAYLAYER = '[Map] SelectOverlayLayers';
export const ZOOMTOEXTENT = '[Map] ZoomToExtent'; export const ZOOMTOEXTENT = '[Map] ZoomToExtent';
export const DOQUERY = '[Map] DoQuery'; export const DOQUERY = '[Map] DoQuery';
export const SETSTYLE = '[Map] SetStyle';
export class SetState implements Action { export class SetState implements Action {
readonly type = SETSTATE; readonly type = SETSTATE;
@ -204,6 +204,12 @@ export class DoQuery implements Action {
constructor(public query:IQueryState) { } constructor(public query:IQueryState) { }
} }
export class SetStyle implements Action {
readonly type = SETSTYLE;
constructor(public itemType:string,public style:Style) { }
}
export type Actions = SetMapState export type Actions = SetMapState
| Init | Init
| SetParent | SetParent
@ -231,5 +237,6 @@ export type Actions = SetMapState
| ZoomToExtent | ZoomToExtent
| SetState | SetState
| SetViewExtent | SetViewExtent
| DoQuery; | DoQuery
| SetStyle;

View File

@ -58,6 +58,7 @@ import { MapRoutingModule } from './common-map-routing.module';
import { LegendComponent } from './components/legend/legend.component'; import { LegendComponent } from './components/legend/legend.component';
import { LayerVectorImageComponent } from './components/aol/layer-vector-image/layer-vector-image.component'; import { LayerVectorImageComponent } from './components/aol/layer-vector-image/layer-vector-image.component';
import { StateSerializerService } from './services/state-serializer.service'; import { StateSerializerService } from './services/state-serializer.service';
import {FeatureIconService} from './services/feature-icon.service';
import { GeolocationService } from './services/geolocation.service'; import { GeolocationService } from './services/geolocation.service';
import {DeviceOrientationService} from './services/device-orientation.service'; import {DeviceOrientationService} from './services/device-orientation.service';
import { WidgetStatusComponent } from './components/widget-status/widget-status.component'; import { WidgetStatusComponent } from './components/widget-status/widget-status.component';
@ -131,6 +132,7 @@ export {
AbstractItemListItemComponent, AbstractItemListItemComponent,
AbstractItemListComponent, AbstractItemListComponent,
StateSerializerService, StateSerializerService,
FeatureIconService,
GeolocationService, GeolocationService,
DeviceOrientationService, DeviceOrientationService,
IMapState, IMapState,
@ -249,6 +251,7 @@ export class AppCommonMapModule {
ngModule: AppCommonMapModule, ngModule: AppCommonMapModule,
providers: [ providers: [
StateSerializerService, StateSerializerService,
FeatureIconService,
GeolocationService, GeolocationService,
DeviceOrientationService, DeviceOrientationService,
{ provide: AbstractFeatureListComponent, useClass: FeatureListCroppingschemeComponent, multi: true }, { provide: AbstractFeatureListComponent, useClass: FeatureListCroppingschemeComponent, multi: true },

View File

@ -1,207 +1,146 @@
import { Component, Host, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges, forwardRef, Inject, InjectionToken } from '@angular/core'; import { Component, Host, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges, forwardRef, Inject, InjectionToken } from '@angular/core';
import { LayerVectorComponent, SourceVectorComponent, MapComponent } from 'ngx-openlayers'; import { LayerVectorComponent, SourceVectorComponent, MapComponent } from 'ngx-openlayers';
import { ItemService,ItemTypeService,IItem, IItemType } from '@farmmaps/common'; import { ItemService,ItemTypeService,IItem, IItemType } from '@farmmaps/common';
import { Feature } from 'ol'; import { Feature } from 'ol';
import { Point } from 'ol/geom'; import { Point } from 'ol/geom';
import { MapBrowserEvent } from 'ol'; import { MapBrowserEvent } from 'ol';
import * as style from 'ol/style'; import * as style from 'ol/style';
import * as color from 'ol/color'; import * as color from 'ol/color';
import * as loadingstrategy from 'ol/loadingstrategy'; import * as loadingstrategy from 'ol/loadingstrategy';
import * as condition from 'ol/events/condition'; import * as condition from 'ol/events/condition';
import * as extent from 'ol/extent'; import * as extent from 'ol/extent';
import {Vector,Cluster} from 'ol/source'; import {Vector,Cluster} from 'ol/source';
import {Layer} from 'ol/layer'; import {Layer} from 'ol/layer';
import {GeoJSON} from 'ol/format'; import {GeoJSON} from 'ol/format';
import {Select} from 'ol/interaction'; import {Select} from 'ol/interaction';
import {IStyleCache} from '../../../models/style.cache';
@Component({ import {FeatureIconService} from '../../../services/feature-icon.service';
selector: 'fm-map-item-source-vector',
template: `<ng-content></ng-content>`, @Component({
providers: [ selector: 'fm-map-item-source-vector',
{ provide: SourceVectorComponent , useExisting: forwardRef(() => ItemVectorSourceComponent) } template: `<ng-content></ng-content>`,
] providers: [
}) { provide: SourceVectorComponent , useExisting: forwardRef(() => ItemVectorSourceComponent) }
export class ItemVectorSourceComponent extends SourceVectorComponent implements OnInit, OnChanges { ]
instance: Vector; })
private _format: GeoJSON; export class ItemVectorSourceComponent extends SourceVectorComponent implements OnInit, OnChanges {
private _select: Select; instance: Vector;
private _hoverSelect: Select; private _format: GeoJSON;
private _iconScale: number = 0.05; private _select: Select;
@Input() features: Array<Feature>; private _hoverSelect: Select;
@Input() selectedFeature: Feature; private _iconScale: number = 0.05;
@Input() selectedItem: IItem; @Input() features: Array<Feature>;
@Output() onFeaturesSelected: EventEmitter<Feature> = new EventEmitter<Feature>(); @Input() selectedFeature: Feature;
private styleCache = { @Input() selectedItem: IItem;
'file': new style.Style({ @Output() onFeaturesSelected: EventEmitter<Feature> = new EventEmitter<Feature>();
image: new style.Icon({ private styleCache:IStyleCache = {};
anchor: [0.5, 1],
scale: 0.05, constructor(@Host() private layer: LayerVectorComponent, private itemService: ItemService, @Host() private map: MapComponent, private itemTypeService: ItemTypeService,private featureIconService$:FeatureIconService) {
src: this.getIconImageDataUrl("fa fa-file-o") super(layer);
}), this._format = new GeoJSON();
stroke: new style.Stroke({ }
color: 'red',
width: 1 geometry(feature: Feature) {
}), let view = this.map.instance.getView();
fill: new style.Fill({ let resolution = view.getResolution();
color: 'rgba(0, 0, 255, 0.1)' var geometry = feature.getGeometry();
}), let e = geometry.getExtent();
geometry: (feature) => this.geometry(feature) //var size = Math.max((e[2] - e[0]) / resolution, (e[3] - e[1]) / resolution);
}), if (resolution > 12) {
'selected': new style.Style({ geometry = new Point(extent.getCenter(e));
image: new style.Icon({ }
anchor: [0.5, 1], return geometry;
scale: 0.08, }
src: this.getIconImageDataUrl(null)
}), ngOnInit() {
stroke: new style.Stroke({ this.strategy = loadingstrategy.bbox;
color: 'red', this.format = new GeoJSON();
width: 3 this._select = new Select({
}), style: (feature) => {
fill: new style.Fill({ return this.styleCache['selected'];
color: 'rgba(0, 0, 255, 0.1)' },
}), hitTolerance: 10,
geometry: (feature) => this.geometry(feature) layers: [this.layer.instance as Layer]
}) });
}; this._hoverSelect = new Select({
style: (feature) => {
constructor(@Host() private layer: LayerVectorComponent, private itemService: ItemService, @Host() private map: MapComponent, private itemTypeService: ItemTypeService) { return this.styleCache['selected'];
super(layer); },
this._format = new GeoJSON(); hitTolerance: 10,
} condition: (e: MapBrowserEvent) => {
return e.type == 'pointermove';
geometry(feature: Feature) { },
let view = this.map.instance.getView(); layers: [this.layer.instance as Layer]
let resolution = view.getResolution(); });
var geometry = feature.getGeometry(); this.map.instance.addInteraction(this._select);
let e = geometry.getExtent(); this.map.instance.addInteraction(this._hoverSelect);
//var size = Math.max((e[2] - e[0]) / resolution, (e[3] - e[1]) / resolution); this._select.on('select', (e) => {
if (resolution > 12) { if (e.selected.length > 0 && e.selected[0]) {
geometry = new Point(extent.getCenter(e)); this.onFeaturesSelected.emit(e.selected[0]);
} } else {
return geometry; this.onFeaturesSelected.emit(null);
} }
});
getIconImageDataUrl(iconClass:string, backgroundColor: string = "#c80a6e",color:string = "#ffffff"): string { this.instance = new Vector(this);
var canvas = document.createElement('canvas'); this.host.instance.setSource(this.instance);
canvas.width = 365;
canvas.height = 560; this.host.instance.setStyle((feature) => {
var ctx = canvas.getContext('2d'); var key = feature.get('itemType') + (this.selectedItem?"_I":"");
ctx.lineWidth = 6; if (!this.styleCache[key]) {
ctx.fillStyle = backgroundColor; if (this.itemTypeService.itemTypes[key]) {
ctx.strokeStyle = "#000000"; let itemType = this.itemTypeService.itemTypes[key];
var path = new Path2D("m182.9 551.7c0 0.1 0.2 0.3 0.2 0.3s175.2-269 175.2-357.4c0-130.1-88.8-186.7-175.4-186.9-86.6 0.2-175.4 56.8-175.4 186.9 0 88.4 175.3 357.4 175.3 357.4z"); let fillColor = color.asArray(itemType.iconColor);
ctx.fill(path) fillColor[3] = this.selectedItem?0:0.5;
this.styleCache[key] = new style.Style({
var iconCharacter = ""; image: itemType.icon ? new style.Icon({
if (iconClass != null) { anchor: [0.5, 1],
var element = document.createElement("i"); scale: 0.05,
element.style.display = "none"; src: this.featureIconService$.getIconImageDataUrl(itemType.icon)
element.className = iconClass; }):null,
document.body.appendChild(element); stroke: new style.Stroke({
iconCharacter = getComputedStyle(element, "::before").content.replace(/"/g, ''); color: 'red',
let iconFont = "200px " +getComputedStyle(element, "::before").fontFamily width: 1
document.body.removeChild(element); }),
ctx.strokeStyle = color; fill: new style.Fill({
ctx.fillStyle = color; color: fillColor
ctx.lineWidth = 15; }),
ctx.font = iconFont; });
var ts = ctx.measureText(iconCharacter); } else {
ctx.fillText(iconCharacter, 182.9 - (ts.width / 2), 250); key = 'file';
ctx.strokeText(iconCharacter, 182.9 - (ts.width / 2), 250); }
} }
var styleEntry = this.styleCache[key];
return canvas.toDataURL(); styleEntry.geometry = (feature) => this.geometry(feature);
} return styleEntry;
});
ngOnInit() { }
this.strategy = loadingstrategy.bbox;
this.format = new GeoJSON(); ngOnChanges(changes: SimpleChanges) {
this._select = new Select({ if (changes["features"] && this.instance) {
style: (feature) => { this.instance.clear(true);
return this.styleCache['selected']; this._select.getFeatures().clear();
}, this.instance.addFeatures(changes["features"].currentValue);
hitTolerance: 10, }
layers: [this.layer.instance as Layer]
}); if (changes["selectedFeature"] && this.instance) {
this._hoverSelect = new Select({ var features = this._select.getFeatures();
style: (feature) => { var feature = changes["selectedFeature"].currentValue
return this.styleCache['selected']; //this.instance.clear(false);
}, //this.instance.addFeatures(features.getArray());
hitTolerance: 10, features.clear();
condition: (e: MapBrowserEvent) => { if (feature) {
return e.type == 'pointermove'; //this.instance.removeFeature(feature);
}, features.push(feature)
layers: [this.layer.instance as Layer] }
}); }
this.map.instance.addInteraction(this._select); if (changes["selectedItem"] && this.instance) {
this.map.instance.addInteraction(this._hoverSelect); var item = changes["selectedItem"].currentValue
this._select.on('select', (e) => { if (item) {
if (e.selected.length > 0 && e.selected[0]) { this.map.instance.removeInteraction(this._hoverSelect);
this.onFeaturesSelected.emit(e.selected[0]); } else {
} else { this.map.instance.addInteraction(this._hoverSelect);
this.onFeaturesSelected.emit(null); }
} }
}); }
this.instance = new Vector(this); }
this.host.instance.setSource(this.instance);
this.host.instance.setStyle((feature) => {
var key = feature.get('itemType') + (this.selectedItem?"_I":"");
if (!this.styleCache[key]) {
if (this.itemTypeService.itemTypes[key]) {
let itemType = this.itemTypeService.itemTypes[key];
let fillColor = color.asArray(itemType.iconColor);
fillColor[3] = this.selectedItem?0:0.5;
this.styleCache[key] = new style.Style({
image: itemType.icon ? new style.Icon({
anchor: [0.5, 1],
scale: 0.05,
src: this.getIconImageDataUrl(itemType.icon)
}):null,
stroke: new style.Stroke({
color: 'red',
width: 1
}),
fill: new style.Fill({
color: fillColor
}),
geometry: (feature) => this.geometry(feature)
});
} else {
key = 'file';
}
}
var styleEntry = this.styleCache[key];
return styleEntry;
});
}
ngOnChanges(changes: SimpleChanges) {
if (changes["features"] && this.instance) {
this.instance.clear(true);
this._select.getFeatures().clear();
this.instance.addFeatures(changes["features"].currentValue);
}
if (changes["selectedFeature"] && this.instance) {
var features = this._select.getFeatures();
var feature = changes["selectedFeature"].currentValue
//this.instance.clear(false);
//this.instance.addFeatures(features.getArray());
features.clear();
if (feature) {
//this.instance.removeFeature(feature);
features.push(feature)
}
}
if (changes["selectedItem"] && this.instance) {
var item = changes["selectedItem"].currentValue
if (item) {
this.map.instance.removeInteraction(this._hoverSelect);
} else {
this.map.instance.addInteraction(this._hoverSelect);
}
}
}
}

View File

@ -19,10 +19,15 @@ import {commonReducers} from '@farmmaps/common';
import {commonActions} from '@farmmaps/common'; import {commonActions} from '@farmmaps/common';
import { IListItem, IItem } from '@farmmaps/common'; import { IItem } from '@farmmaps/common';
import { FolderService, ItemService } from '@farmmaps/common'; import { FolderService, ItemService } from '@farmmaps/common';
import { tassign } from 'tassign'; import { tassign } from 'tassign';
import {FeatureIconService} from '../services/feature-icon.service';
import * as style from 'ol/style';
@Injectable() @Injectable()
export class MapEffects { export class MapEffects {
private _geojsonFormat: GeoJSON; private _geojsonFormat: GeoJSON;
@ -44,10 +49,41 @@ export class MapEffects {
ofType(mapActions.INIT), ofType(mapActions.INIT),
withLatestFrom(this.store$.select(commonReducers.selectGetRootItems)), withLatestFrom(this.store$.select(commonReducers.selectGetRootItems)),
switchMap(([action, rootItems]) => { switchMap(([action, rootItems]) => {
let actions=[];
for (let rootItem of rootItems) { for (let rootItem of rootItems) {
if (rootItem.itemType == "UPLOADS_FOLDER") return of(new mapActions.SetParent(rootItem.code)); if (rootItem.itemType == "UPLOADS_FOLDER") actions.push(new mapActions.SetParent(rootItem.code));
} }
return []; // initialize default feature styles
actions.push(new mapActions.SetStyle('file',new style.Style({
image: new style.Icon({
anchor: [0.5, 1],
scale: 0.05,
src: this.featureIconService$.getIconImageDataUrl("fa fa-file-o")
}),
stroke: new style.Stroke({
color: 'red',
width: 1
}),
fill: new style.Fill({
color: 'rgba(0, 0, 255, 0.1)'
})
})));
actions.push(new mapActions.SetStyle('selected',new style.Style({
image: new style.Icon({
anchor: [0.5, 1],
scale: 0.08,
src: this.featureIconService$.getIconImageDataUrl(null)
}),
stroke: new style.Stroke({
color: 'red',
width: 3
}),
fill: new style.Fill({
color: 'rgba(0, 0, 255, 0.1)'
})
})));
return actions;
} }
)); ));
@ -231,7 +267,7 @@ export class MapEffects {
return of(newAction); return of(newAction);
})); }));
constructor(private actions$: Actions, private store$: Store<mapReducers.State>, private folderService$: FolderService, private itemService$: ItemService) { constructor(private actions$: Actions, private store$: Store<mapReducers.State>, private folderService$: FolderService, private itemService$: ItemService,private featureIconService$:FeatureIconService) {
this._geojsonFormat = new GeoJSON(); this._geojsonFormat = new GeoJSON();
this._wktFormat = new WKT(); this._wktFormat = new WKT();
} }

View File

@ -0,0 +1,5 @@
import {Style} from 'ol';
export interface IStyleCache{
[id: string]: Style;
};

View File

@ -4,6 +4,7 @@ import { IItemLayer,ItemLayer} from '../models/item.layer';
import { IMapState} from '../models/map.state'; import { IMapState} from '../models/map.state';
import { IQueryState} from '../models/query.state'; import { IQueryState} from '../models/query.state';
import { IPeriodState} from '../models/period.state'; import { IPeriodState} from '../models/period.state';
import { IStyleCache} from '../models/style.cache';
import * as mapActions from '../actions/map.actions'; import * as mapActions from '../actions/map.actions';
import {commonActions} from '@farmmaps/common'; import {commonActions} from '@farmmaps/common';
import { createSelector, createFeatureSelector } from '@ngrx/store'; import { createSelector, createFeatureSelector } from '@ngrx/store';
@ -51,7 +52,8 @@ export interface State {
selectedItemLayer: IItemLayer, selectedItemLayer: IItemLayer,
projection: string, projection: string,
selectedBaseLayer: IItemLayer, selectedBaseLayer: IItemLayer,
selectedOverlayLayer: IItemLayer selectedOverlayLayer: IItemLayer,
styleCache:IStyleCache
} }
export const initialState: State = { export const initialState: State = {
@ -84,7 +86,8 @@ export const initialState: State = {
projection: "EPSG:3857", projection: "EPSG:3857",
selectedBaseLayer: null, selectedBaseLayer: null,
selectedOverlayLayer: null, selectedOverlayLayer: null,
selectedItemLayer: null selectedItemLayer: null,
styleCache: {}
} }
export function reducer(state = initialState, action: mapActions.Actions | commonActions.Actions | RouterNavigationAction): State { export function reducer(state = initialState, action: mapActions.Actions | commonActions.Actions | RouterNavigationAction): State {
@ -310,6 +313,12 @@ export function reducer(state = initialState, action: mapActions.Actions | commo
return tassign(state, {}); return tassign(state, {});
} }
} }
case mapActions.SETSTYLE:{
let a = action as mapActions.SetStyle;
let styles = state.styleCache;
styles[a.itemType] = a.style;
return tassign(state,{styleCache:styles});
}
default: { default: {
return state; return state;
} }

View File

@ -0,0 +1,41 @@
import { Injectable} from '@angular/core';
import { Feature } from 'ol';
import { Point } from 'ol/geom';
import * as extent from 'ol/extent';
@Injectable()
export class FeatureIconService {
getIconImageDataUrl(iconClass:string, backgroundColor: string = "#c80a6e",color:string = "#ffffff"): string {
var canvas = document.createElement('canvas');
canvas.width = 365;
canvas.height = 560;
var ctx = canvas.getContext('2d');
ctx.lineWidth = 6;
ctx.fillStyle = backgroundColor;
ctx.strokeStyle = "#000000";
var path = new Path2D("m182.9 551.7c0 0.1 0.2 0.3 0.2 0.3s175.2-269 175.2-357.4c0-130.1-88.8-186.7-175.4-186.9-86.6 0.2-175.4 56.8-175.4 186.9 0 88.4 175.3 357.4 175.3 357.4z");
ctx.fill(path)
var iconCharacter = "";
if (iconClass != null) {
var element = document.createElement("i");
element.style.display = "none";
element.className = iconClass;
document.body.appendChild(element);
iconCharacter = getComputedStyle(element, "::before").content.replace(/"/g, '');
let iconFont = "200px " +getComputedStyle(element, "::before").fontFamily
document.body.removeChild(element);
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = 15;
ctx.font = iconFont;
var ts = ctx.measureText(iconCharacter);
ctx.fillText(iconCharacter, 182.9 - (ts.width / 2), 250);
ctx.strokeText(iconCharacter, 182.9 - (ts.width / 2), 250);
}
return canvas.toDataURL();
}
}

View File

@ -4,9 +4,7 @@ import {IItem} from '../models/item'
import {AppConfig} from '../shared/app.config'; import {AppConfig} from '../shared/app.config';
import {HttpClient, HttpXhrBackend} from '@angular/common/http'; import {HttpClient, HttpXhrBackend} from '@angular/common/http';
@Injectable({ @Injectable()
providedIn: 'root',
})
export class ItemTypeService { export class ItemTypeService {
public itemTypes: IItemTypes; public itemTypes: IItemTypes;
private httpClient: HttpClient; private httpClient: HttpClient;
@ -15,10 +13,6 @@ export class ItemTypeService {
this.httpClient = new HttpClient(xhrBackend); this.httpClient = new HttpClient(xhrBackend);
} }
// itemService.getItemTypes().subscribe((itemTypes) => {
// this.itemTypes = itemTypes;
// });
getIcon(itemType: string) { getIcon(itemType: string) {
var icon = "fa fa-file-o"; var icon = "fa fa-file-o";
if (this.itemTypes[itemType]) icon = this.itemTypes[itemType].icon; if (this.itemTypes[itemType]) icon = this.itemTypes[itemType].icon;