diff --git a/projects/common-map/package.json b/projects/common-map/package.json index e204ec4..dddff35 100644 --- a/projects/common-map/package.json +++ b/projects/common-map/package.json @@ -6,7 +6,7 @@ }, "dependencies": { "ngx-openlayers": "1.0.0-next.13", - "ol": "^6.0.0" + "ol": "6.1.1" }, "peerDependencies": { "@angular/core": "^8.2.0", diff --git a/projects/common-map/src/fm-map/actions/map.actions.ts b/projects/common-map/src/fm-map/actions/map.actions.ts index fb4b0ab..b573a25 100644 --- a/projects/common-map/src/fm-map/actions/map.actions.ts +++ b/projects/common-map/src/fm-map/actions/map.actions.ts @@ -4,8 +4,7 @@ import { IMapState } from '../models/map.state'; import { IItemLayer } from '../models/item.layer'; import { IQueryState } from '../models/query.state'; import { IItem } from '@farmmaps/common'; -import { Feature } from 'ol'; -import { Extent } from 'ol/extent'; +import { Feature,Style } from 'ol'; export const SETSTATE = '[Map] SetState'; export const SETMAPSTATE = '[Map] MapState'; @@ -35,6 +34,7 @@ export const SELECTBASELAYER = '[Map] SelectBaseLayers'; export const SELECTOVERLAYLAYER = '[Map] SelectOverlayLayers'; export const ZOOMTOEXTENT = '[Map] ZoomToExtent'; export const DOQUERY = '[Map] DoQuery'; +export const SETSTYLE = '[Map] SetStyle'; export class SetState implements Action { readonly type = SETSTATE; @@ -204,6 +204,12 @@ export class DoQuery implements Action { constructor(public query:IQueryState) { } } +export class SetStyle implements Action { + readonly type = SETSTYLE; + + constructor(public itemType:string,public style:Style) { } +} + export type Actions = SetMapState | Init | SetParent @@ -231,5 +237,6 @@ export type Actions = SetMapState | ZoomToExtent | SetState | SetViewExtent - | DoQuery; + | DoQuery + | SetStyle; diff --git a/projects/common-map/src/fm-map/common-map.module.ts b/projects/common-map/src/fm-map/common-map.module.ts index b35a061..78d0635 100644 --- a/projects/common-map/src/fm-map/common-map.module.ts +++ b/projects/common-map/src/fm-map/common-map.module.ts @@ -58,6 +58,7 @@ import { MapRoutingModule } from './common-map-routing.module'; import { LegendComponent } from './components/legend/legend.component'; import { LayerVectorImageComponent } from './components/aol/layer-vector-image/layer-vector-image.component'; import { StateSerializerService } from './services/state-serializer.service'; +import {FeatureIconService} from './services/feature-icon.service'; import { GeolocationService } from './services/geolocation.service'; import {DeviceOrientationService} from './services/device-orientation.service'; import { WidgetStatusComponent } from './components/widget-status/widget-status.component'; @@ -131,6 +132,7 @@ export { AbstractItemListItemComponent, AbstractItemListComponent, StateSerializerService, + FeatureIconService, GeolocationService, DeviceOrientationService, IMapState, @@ -249,6 +251,7 @@ export class AppCommonMapModule { ngModule: AppCommonMapModule, providers: [ StateSerializerService, + FeatureIconService, GeolocationService, DeviceOrientationService, { provide: AbstractFeatureListComponent, useClass: FeatureListCroppingschemeComponent, multi: true }, diff --git a/projects/common-map/src/fm-map/components/aol/item-vector-source/item-vector-source.component.ts b/projects/common-map/src/fm-map/components/aol/item-vector-source/item-vector-source.component.ts index 4c43acb..08518ab 100644 --- a/projects/common-map/src/fm-map/components/aol/item-vector-source/item-vector-source.component.ts +++ b/projects/common-map/src/fm-map/components/aol/item-vector-source/item-vector-source.component.ts @@ -1,207 +1,146 @@ -import { Component, Host, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges, forwardRef, Inject, InjectionToken } from '@angular/core'; -import { LayerVectorComponent, SourceVectorComponent, MapComponent } from 'ngx-openlayers'; -import { ItemService,ItemTypeService,IItem, IItemType } from '@farmmaps/common'; - -import { Feature } from 'ol'; -import { Point } from 'ol/geom'; -import { MapBrowserEvent } from 'ol'; -import * as style from 'ol/style'; -import * as color from 'ol/color'; -import * as loadingstrategy from 'ol/loadingstrategy'; -import * as condition from 'ol/events/condition'; -import * as extent from 'ol/extent'; -import {Vector,Cluster} from 'ol/source'; -import {Layer} from 'ol/layer'; -import {GeoJSON} from 'ol/format'; -import {Select} from 'ol/interaction'; - -@Component({ - selector: 'fm-map-item-source-vector', - template: ``, - providers: [ - { provide: SourceVectorComponent , useExisting: forwardRef(() => ItemVectorSourceComponent) } - ] -}) -export class ItemVectorSourceComponent extends SourceVectorComponent implements OnInit, OnChanges { - instance: Vector; - private _format: GeoJSON; - private _select: Select; - private _hoverSelect: Select; - private _iconScale: number = 0.05; - @Input() features: Array; - @Input() selectedFeature: Feature; - @Input() selectedItem: IItem; - @Output() onFeaturesSelected: EventEmitter = new EventEmitter(); - private styleCache = { - 'file': new style.Style({ - image: new style.Icon({ - anchor: [0.5, 1], - scale: 0.05, - src: this.getIconImageDataUrl("fa fa-file-o") - }), - stroke: new style.Stroke({ - color: 'red', - width: 1 - }), - fill: new style.Fill({ - color: 'rgba(0, 0, 255, 0.1)' - }), - geometry: (feature) => this.geometry(feature) - }), - 'selected': new style.Style({ - image: new style.Icon({ - anchor: [0.5, 1], - scale: 0.08, - src: this.getIconImageDataUrl(null) - }), - stroke: new style.Stroke({ - color: 'red', - width: 3 - }), - fill: new style.Fill({ - color: 'rgba(0, 0, 255, 0.1)' - }), - geometry: (feature) => this.geometry(feature) - }) - }; - - constructor(@Host() private layer: LayerVectorComponent, private itemService: ItemService, @Host() private map: MapComponent, private itemTypeService: ItemTypeService) { - super(layer); - this._format = new GeoJSON(); - } - - geometry(feature: Feature) { - let view = this.map.instance.getView(); - let resolution = view.getResolution(); - var geometry = feature.getGeometry(); - let e = geometry.getExtent(); - //var size = Math.max((e[2] - e[0]) / resolution, (e[3] - e[1]) / resolution); - if (resolution > 12) { - geometry = new Point(extent.getCenter(e)); - } - return geometry; - } - - 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(); - } - - ngOnInit() { - this.strategy = loadingstrategy.bbox; - this.format = new GeoJSON(); - this._select = new Select({ - style: (feature) => { - return this.styleCache['selected']; - }, - hitTolerance: 10, - layers: [this.layer.instance as Layer] - }); - this._hoverSelect = new Select({ - style: (feature) => { - return this.styleCache['selected']; - }, - hitTolerance: 10, - condition: (e: MapBrowserEvent) => { - return e.type == 'pointermove'; - }, - layers: [this.layer.instance as Layer] - }); - this.map.instance.addInteraction(this._select); - this.map.instance.addInteraction(this._hoverSelect); - this._select.on('select', (e) => { - if (e.selected.length > 0 && e.selected[0]) { - this.onFeaturesSelected.emit(e.selected[0]); - } else { - 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); - } - } - } -} +import { Component, Host, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges, forwardRef, Inject, InjectionToken } from '@angular/core'; +import { LayerVectorComponent, SourceVectorComponent, MapComponent } from 'ngx-openlayers'; +import { ItemService,ItemTypeService,IItem, IItemType } from '@farmmaps/common'; + +import { Feature } from 'ol'; +import { Point } from 'ol/geom'; +import { MapBrowserEvent } from 'ol'; +import * as style from 'ol/style'; +import * as color from 'ol/color'; +import * as loadingstrategy from 'ol/loadingstrategy'; +import * as condition from 'ol/events/condition'; +import * as extent from 'ol/extent'; +import {Vector,Cluster} from 'ol/source'; +import {Layer} from 'ol/layer'; +import {GeoJSON} from 'ol/format'; +import {Select} from 'ol/interaction'; +import {IStyleCache} from '../../../models/style.cache'; +import {FeatureIconService} from '../../../services/feature-icon.service'; + +@Component({ + selector: 'fm-map-item-source-vector', + template: ``, + providers: [ + { provide: SourceVectorComponent , useExisting: forwardRef(() => ItemVectorSourceComponent) } + ] +}) +export class ItemVectorSourceComponent extends SourceVectorComponent implements OnInit, OnChanges { + instance: Vector; + private _format: GeoJSON; + private _select: Select; + private _hoverSelect: Select; + private _iconScale: number = 0.05; + @Input() features: Array; + @Input() selectedFeature: Feature; + @Input() selectedItem: IItem; + @Output() onFeaturesSelected: EventEmitter = new EventEmitter(); + private styleCache:IStyleCache = {}; + + constructor(@Host() private layer: LayerVectorComponent, private itemService: ItemService, @Host() private map: MapComponent, private itemTypeService: ItemTypeService,private featureIconService$:FeatureIconService) { + super(layer); + this._format = new GeoJSON(); + } + + geometry(feature: Feature) { + let view = this.map.instance.getView(); + let resolution = view.getResolution(); + var geometry = feature.getGeometry(); + let e = geometry.getExtent(); + //var size = Math.max((e[2] - e[0]) / resolution, (e[3] - e[1]) / resolution); + if (resolution > 12) { + geometry = new Point(extent.getCenter(e)); + } + return geometry; + } + + ngOnInit() { + this.strategy = loadingstrategy.bbox; + this.format = new GeoJSON(); + this._select = new Select({ + style: (feature) => { + return this.styleCache['selected']; + }, + hitTolerance: 10, + layers: [this.layer.instance as Layer] + }); + this._hoverSelect = new Select({ + style: (feature) => { + return this.styleCache['selected']; + }, + hitTolerance: 10, + condition: (e: MapBrowserEvent) => { + return e.type == 'pointermove'; + }, + layers: [this.layer.instance as Layer] + }); + this.map.instance.addInteraction(this._select); + this.map.instance.addInteraction(this._hoverSelect); + this._select.on('select', (e) => { + if (e.selected.length > 0 && e.selected[0]) { + this.onFeaturesSelected.emit(e.selected[0]); + } else { + 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.featureIconService$.getIconImageDataUrl(itemType.icon) + }):null, + stroke: new style.Stroke({ + color: 'red', + width: 1 + }), + fill: new style.Fill({ + color: fillColor + }), + }); + } else { + key = 'file'; + } + } + var styleEntry = this.styleCache[key]; + styleEntry.geometry = (feature) => this.geometry(feature); + 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); + } + } + } +} diff --git a/projects/common-map/src/fm-map/effects/map.effects.ts b/projects/common-map/src/fm-map/effects/map.effects.ts index cf35add..1c6facf 100644 --- a/projects/common-map/src/fm-map/effects/map.effects.ts +++ b/projects/common-map/src/fm-map/effects/map.effects.ts @@ -19,10 +19,15 @@ import {commonReducers} 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 { tassign } from 'tassign'; +import {FeatureIconService} from '../services/feature-icon.service'; + +import * as style from 'ol/style'; + + @Injectable() export class MapEffects { private _geojsonFormat: GeoJSON; @@ -44,10 +49,41 @@ export class MapEffects { ofType(mapActions.INIT), withLatestFrom(this.store$.select(commonReducers.selectGetRootItems)), switchMap(([action, rootItems]) => { + let actions=[]; 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); })); - constructor(private actions$: Actions, private store$: Store, private folderService$: FolderService, private itemService$: ItemService) { + constructor(private actions$: Actions, private store$: Store, private folderService$: FolderService, private itemService$: ItemService,private featureIconService$:FeatureIconService) { this._geojsonFormat = new GeoJSON(); this._wktFormat = new WKT(); } diff --git a/projects/common-map/src/fm-map/models/style.cache.ts b/projects/common-map/src/fm-map/models/style.cache.ts new file mode 100644 index 0000000..96e5b9c --- /dev/null +++ b/projects/common-map/src/fm-map/models/style.cache.ts @@ -0,0 +1,5 @@ +import {Style} from 'ol'; + +export interface IStyleCache{ + [id: string]: Style; +}; \ No newline at end of file diff --git a/projects/common-map/src/fm-map/reducers/map.reducer.ts b/projects/common-map/src/fm-map/reducers/map.reducer.ts index 9374631..6641f22 100644 --- a/projects/common-map/src/fm-map/reducers/map.reducer.ts +++ b/projects/common-map/src/fm-map/reducers/map.reducer.ts @@ -4,6 +4,7 @@ import { IItemLayer,ItemLayer} from '../models/item.layer'; import { IMapState} from '../models/map.state'; import { IQueryState} from '../models/query.state'; import { IPeriodState} from '../models/period.state'; +import { IStyleCache} from '../models/style.cache'; import * as mapActions from '../actions/map.actions'; import {commonActions} from '@farmmaps/common'; import { createSelector, createFeatureSelector } from '@ngrx/store'; @@ -51,7 +52,8 @@ export interface State { selectedItemLayer: IItemLayer, projection: string, selectedBaseLayer: IItemLayer, - selectedOverlayLayer: IItemLayer + selectedOverlayLayer: IItemLayer, + styleCache:IStyleCache } export const initialState: State = { @@ -84,7 +86,8 @@ export const initialState: State = { projection: "EPSG:3857", selectedBaseLayer: null, selectedOverlayLayer: null, - selectedItemLayer: null + selectedItemLayer: null, + styleCache: {} } 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, {}); } } + case mapActions.SETSTYLE:{ + let a = action as mapActions.SetStyle; + let styles = state.styleCache; + styles[a.itemType] = a.style; + return tassign(state,{styleCache:styles}); + } default: { return state; } diff --git a/projects/common-map/src/fm-map/services/feature-icon.service.ts b/projects/common-map/src/fm-map/services/feature-icon.service.ts new file mode 100644 index 0000000..70a5a17 --- /dev/null +++ b/projects/common-map/src/fm-map/services/feature-icon.service.ts @@ -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(); + } +} \ No newline at end of file diff --git a/projects/common/src/fm/services/itemtype.service.ts b/projects/common/src/fm/services/itemtype.service.ts index 426ea45..f81f1ad 100644 --- a/projects/common/src/fm/services/itemtype.service.ts +++ b/projects/common/src/fm/services/itemtype.service.ts @@ -4,9 +4,7 @@ import {IItem} from '../models/item' import {AppConfig} from '../shared/app.config'; import {HttpClient, HttpXhrBackend} from '@angular/common/http'; -@Injectable({ - providedIn: 'root', -}) +@Injectable() export class ItemTypeService { public itemTypes: IItemTypes; private httpClient: HttpClient; @@ -15,10 +13,6 @@ export class ItemTypeService { this.httpClient = new HttpClient(xhrBackend); } - // itemService.getItemTypes().subscribe((itemTypes) => { - // this.itemTypes = itemTypes; - // }); - getIcon(itemType: string) { var icon = "fa fa-file-o"; if (this.itemTypes[itemType]) icon = this.itemTypes[itemType].icon;