19 Commits
Author SHA1 Message Date
Fabian Jeglinski 48e3db86ac refactor/rename gane to lm 2020-02-16 13:27:12 +01:00
Fabian Jeglinski a458a64969 add tooltip 2020-02-16 13:16:48 +01:00
Fabian Jeglinski f5c00cfa93 add styling improvements; add geojson export for as zip file; 2020-02-16 13:06:52 +01:00
Fabian Jeglinski 539643caf4 add bug to todo 2020-02-15 23:16:55 +01:00
Fabian Jeglinski e4e4c3a955 add prevColorCache for color picker component 2020-02-15 18:29:34 +01:00
Fabian Jeglinski 55a511e9f1 add todos 2019-09-08 12:50:31 +02:00
Fabian Jeglinski 55e0fca470 add todos; add public accessors 2019-09-08 12:13:30 +02:00
Fabian Jeglinski 240d0c9997 several bugfixes; apply color change to rawlayer and vectorlayer; enable color reset 2019-05-24 17:27:38 +02:00
Fabian Jeglinski 5d75f023ad refactoring to life map 2019-05-17 16:23:04 +02:00
Fabian Jeglinski fb978331d9 add todos 2019-05-10 18:03:10 +02:00
Fabian Jeglinski 5c54e0568f small improvements 2019-05-10 17:58:35 +02:00
Fabian Jeglinski 4ae9653e66 refacotr custom color entry component 2019-05-10 17:47:57 +02:00
Fabian Jeglinski 8e29b9f139 work on custom color entry component 2019-05-10 17:40:05 +02:00
Fabian Jeglinski f698beb068 introduce feature management 2019-05-10 16:26:49 +02:00
Fabian Jeglinski 8c273f5d5e refacotor style and feature cache handling 2019-05-04 21:35:37 +02:00
Fabian Jeglinski 9a9a5d1bd6 improve and refactor geo json property handling 2019-05-04 17:58:16 +02:00
Fabian Jeglinski 52501275b5 change cursor if hover over feature; fix highlight isssue 2019-05-04 17:32:55 +02:00
Fabian Jeglinski eb9477c110 add feature selection 2019-05-03 17:48:12 +02:00
Fabian Jeglinski d0892f4c57 improve color saving; improve property handling 2019-05-03 17:02:53 +02:00
29 changed files with 2570 additions and 1446 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# GaneWeb
# Life Map Web
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.3.5.
+9 -9
View File
@@ -3,7 +3,7 @@
"version": 1,
"newProjectRoot": "projects",
"projects": {
"gane-web": {
"lm-web": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
@@ -17,7 +17,7 @@
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/gane-web",
"outputPath": "dist/lm-web",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
@@ -62,18 +62,18 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "gane-web:build"
"browserTarget": "lm-web:build"
},
"configurations": {
"production": {
"browserTarget": "gane-web:build:production"
"browserTarget": "lm-web:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "gane-web:build"
"browserTarget": "lm-web:build"
}
},
"test": {
@@ -107,7 +107,7 @@
}
}
},
"gane-web-e2e": {
"lm-web-e2e": {
"root": "e2e/",
"projectType": "application",
"prefix": "",
@@ -116,11 +116,11 @@
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "gane-web:serve"
"devServerTarget": "lm-web:serve"
},
"configurations": {
"production": {
"devServerTarget": "gane-web:serve:production"
"devServerTarget": "lm-web:serve:production"
}
}
},
@@ -136,5 +136,5 @@
}
}
},
"defaultProject": "gane-web"
"defaultProject": "lm-web"
}
+1 -1
View File
@@ -10,7 +10,7 @@ describe('workspace-project App', () => {
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('Welcome to gane-web!');
expect(page.getTitleText()).toEqual('Welcome to lm web!');
});
afterEach(async () => {
+1701 -1151
View File
File diff suppressed because it is too large Load Diff
+25 -23
View File
@@ -1,5 +1,5 @@
{
"layerName": "gane-web",
"layerName": "lm-web",
"version": "0.0.0",
"scripts": {
"ng": "ng",
@@ -11,43 +11,45 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^7.2.12",
"@angular/animations": "^7.2.16",
"@angular/cdk": "^7.3.7",
"@angular/common": "~7.2.0",
"@angular/compiler": "~7.2.0",
"@angular/core": "~7.2.0",
"@angular/forms": "~7.2.0",
"@angular/common": "^7.2.16",
"@angular/compiler": "^7.2.16",
"@angular/core": "^7.2.16",
"@angular/forms": "^7.2.16",
"@angular/material": "^7.3.7",
"@angular/platform-browser": "~7.2.0",
"@angular/platform-browser-dynamic": "~7.2.0",
"@angular/router": "~7.2.0",
"@types/leaflet": "^1.4.3",
"@types/openlayers": "^4.6.16",
"core-js": "^2.5.4",
"hammerjs": "^2.0.8",
"leaflet": "^1.4.0",
"ol": "^5.3.1",
"@angular/platform-browser": "^7.2.16",
"@angular/platform-browser-dynamic": "^7.2.16",
"@angular/router": "^7.2.16",
"@types/jszip": "^3.1.7",
"@types/openlayers": "^4.6.17",
"core-js": "^2.6.11",
"file-saver": "^2.0.2",
"jszip": "^3.2.2",
"ngx-color-picker": "^7.5.0",
"ol": "^5.3.3",
"rxjs": "~6.3.3",
"tslib": "^1.9.0",
"stream": "0.0.2",
"tslib": "^1.10.0",
"zone.js": "~0.8.26"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.13.0",
"@angular/cli": "~7.3.5",
"@angular/compiler-cli": "~7.2.0",
"@angular/language-service": "~7.2.0",
"@angular-devkit/build-angular": "^0.13.0",
"@angular/cli": "^7.3.9",
"@angular/compiler-cli": "^7.2.16",
"@angular/language-service": "^7.2.16",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.3",
"@types/jasminewd2": "^2.0.8",
"@types/node": "~8.9.4",
"codelyzer": "~4.5.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.0.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-coverage-istanbul-reporter": "^2.0.6",
"karma-jasmine": "~1.1.2",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.4.0",
"protractor": "^5.4.3",
"ts-node": "~7.0.0",
"tslint": "~5.11.0",
"typescript": "~3.2.2"
@@ -1,8 +1,8 @@
import {Component, Inject} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material";
import {ValidationErrors} from "@angular/forms";
import {RawLayer} from "../models/RawLayer";
import {Utils} from "../misc/Utils";
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material';
import {ValidationErrors} from '@angular/forms';
import {RawLayer} from '../models/RawLayer';
import {Utils} from '../misc/Utils';
/**
* TODO: Add description
@@ -24,7 +24,8 @@ export class AddLayerComponent {
layerName: '',
layerData: null,
color: '#4fadc2'
}
};
public uploadError: ValidationErrors = null;
public uploadIsDirty: boolean = false;
public editMode: boolean = false;
@@ -36,7 +37,8 @@ export class AddLayerComponent {
if (data) {
this.formData.layerName = data.name;
this.formData.layerData = data.data;
this.formData.color = Utils.rgbToHex(data.color[0],data.color[1],data.color[2]);
const colorRgb = data.getDefaultColor();
this.formData.color = Utils.rgbToHex(colorRgb[0], colorRgb[1], colorRgb[2]);
this.editMode = true;
}
}
@@ -62,7 +64,7 @@ export class AddLayerComponent {
} else {
this.uploadError = {
invalidFileType: true
}
};
}
const fileReader = new FileReader();
@@ -75,7 +77,8 @@ export class AddLayerComponent {
}
onSubmit() {
const rawLayer = new RawLayer(this.formData.layerName, this.formData.layerData, Utils.hexToRgba(this.formData.color));
const rawLayer = new RawLayer(this.formData.layerName, this.formData.layerData);
rawLayer.saveDefaultColor(Utils.hexToRgba(this.formData.color));
this.close(rawLayer);
}
}
+1 -3
View File
@@ -1,12 +1,10 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {MapComponent} from "./map/map.component";
import {LeafletMapComponent} from "./leaflet-map/leaflet-map.component";
const routes: Routes = [
{path: '**', component: MapComponent},
{path: 'osm', component: MapComponent},
{path: 'leaflet', component: LeafletMapComponent},
{path: 'osm', component: MapComponent}
];
@NgModule({
+3 -3
View File
@@ -20,16 +20,16 @@ describe('AppComponent', () => {
expect(app).toBeTruthy();
});
it(`should have as title 'gane-web'`, () => {
it(`should have as title 'lmweb'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('gane-web');
expect(app.title).toEqual('lm web');
});
it('should render title in a h1 tag', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to gane-web!');
expect(compiled.querySelector('h1').textContent).toContain('Welcome to lm web!');
});
});
-1
View File
@@ -1,5 +1,4 @@
import {Component} from '@angular/core';
import 'hammerjs';
@Component({
selector: 'app-root',
+23 -11
View File
@@ -1,29 +1,37 @@
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {MapComponent} from './map/map.component';
import { LeafletMapComponent } from './leaflet-map/leaflet-map.component';
import {HttpClient, HttpClientModule} from "@angular/common/http";
import {FormsModule} from "@angular/forms";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {MatCardModule, MatDialogModule, MatGridListModule, MatInputModule, MatToolbarModule} from "@angular/material";
import {DragDropModule} from "@angular/cdk/drag-drop";
import {FormsModule} from '@angular/forms';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {
MatCardModule,
MatDialogModule,
MatGridListModule,
MatInputModule,
MatTableModule,
MatToolbarModule,
} from '@angular/material';
import {DragDropModule} from '@angular/cdk/drag-drop';
import {AddLayerComponent} from './add-layer-component/add-layer.component';
import {FeatureManagerComponent} from './feature-management/feature-manager/feature-manager.component';
import {CustomColorEntryComponent} from './feature-management/custom-color-entry/custom-color-entry.component';
import {ColorPickerModule} from 'ngx-color-picker';
import {MatTooltipModule} from '@angular/material/tooltip';
@NgModule({
declarations: [
AppComponent,
MapComponent,
LeafletMapComponent,
AddLayerComponent
AddLayerComponent,
FeatureManagerComponent,
CustomColorEntryComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
AppRoutingModule,
HttpClientModule,
FormsModule,
MatCardModule,
MatGridListModule,
@@ -31,10 +39,14 @@ import { AddLayerComponent } from './add-layer-component/add-layer.component';
DragDropModule,
MatInputModule,
MatDialogModule,
MatTableModule,
MatTooltipModule,
ColorPickerModule
],
entryComponents: [
AddLayerComponent
],
bootstrap: [AppComponent]
})
export class AppModule { }
export class AppModule {
}
+12
View File
@@ -0,0 +1,12 @@
/**
* An enum that presents all custom property keys
*
* TODO
* - add descriptions
*/
export enum CustomProperties {
DEFAULT_COLOR_STROKE = 'default-style-stroke',
DEFAULT_COLOR_FILL = 'default-style-fill',
COLOR_STROKE = 'style-stroke',
COLOR_FILL = 'style-fill',
}
@@ -0,0 +1,16 @@
<div class="entry">
Color:
<div class="color-preview" [ngStyle]="previewStyle"></div>
<span *ngIf="isDefaultColor">(default color)</span>
<div class="controls">
<i class="material-icons"
[(colorPicker)]="prevColorCache"
cpAlphaChannel="disabled"
cpOutputFormat="hex"
[cpOKButton]="true"
[cpCancelButton]="true"
(colorPickerSelect)="onColorPicked($event)"
>edit</i>
<i *ngIf="!isDefaultColor" class="material-icons" (click)="resetColor()">delete</i>
</div>
</div>
@@ -0,0 +1,19 @@
.entry {
display: flex;
height: 3rem;
align-items: center;
margin: 0 0 .125rem 0;
border: 1px black solid;
padding: .5rem;
.controls {
margin-left: auto;
}
.color-preview {
margin: 0 .5rem 0 .5rem;
height: 2rem;
width: 2rem;
border: 2px solid;
}
}
@@ -0,0 +1,139 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {CustomProperties} from '../../enums/CustomProperties';
import {Utils} from '../../misc/Utils';
import {Config} from '../../misc/Config';
/**
* This component represents an entry of the custom properties section inside the feature management and
* is responsible to show and makes changes to the color properties.
*/
@Component({
selector: 'lm-custom-color-entry',
templateUrl: 'custom-color-entry.component.html',
styleUrls: ['custom-color-entry.component.scss']
})
export class CustomColorEntryComponent implements OnInit {
private prevColorStorageKey: string = Config.GEO_JSON_PREFIX + 'prev_selected_color';
/**
* Color cache to make the previous selected color available
*/
private _prevColorCache: string;
get prevColorCache(): string {
return localStorage.getItem(this.prevColorStorageKey);
}
private _colors: any;
@Input() set colors(value: any) {
this._colors = value;
if (this._colors[CustomProperties.COLOR_STROKE]) {
this.isDefaultColor = false;
this.definePreviewStyle();
}
}
get colors(): any {
return this._colors;
}
private _selectedColor: string;
set selectedColor(value: string) {
this._selectedColor = value;
this.applyColor(value);
this.colorChangeEmitter.emit(this.colors);
}
get selectedColor(): string {
return this._selectedColor;
}
// TODO diese funktionen in definePreviewStyles direkt aufrufen
// - default color properties
get defaultStrokeColorAsRgba(): string {
const color = this.colors[CustomProperties.DEFAULT_COLOR_STROKE];
return Utils.colorArrayToRgba(color);
}
get defaultFillColorAsRgba(): string {
const color = this.colors[CustomProperties.DEFAULT_COLOR_FILL];
return Utils.colorArrayToRgba(color);
}
// - no default color properties
get strokeColorAsRgba(): string {
const color = this.colors[CustomProperties.COLOR_STROKE];
return Utils.colorArrayToRgba(color);
}
get fillColorAsRgba(): string {
const color = this.colors[CustomProperties.COLOR_FILL];
return Utils.colorArrayToRgba(color);
}
@Output('colorChanged') colorChangeEmitter: EventEmitter<any> = new EventEmitter();
/**
* The color style of the color preview
*/
previewStyle: any;
/**
* A simple flag that is true, if the default color is active
*/
isDefaultColor: boolean = true;
ngOnInit() {
this.definePreviewStyle();
}
/**
* Returns the style for the preview. If a color was defined, this color is used. If not, the default color is used
* instead.
*/
definePreviewStyle() {
let styles = {
'border-color': this.defaultStrokeColorAsRgba,
'background-color': this.defaultFillColorAsRgba
};
if (!this.isDefaultColor) {
styles = {
'border-color': this.strokeColorAsRgba,
'background-color': this.fillColorAsRgba
};
}
this.previewStyle = styles;
}
/**
* Saves the given hex value to colors object as not default color
* @param value
*/
applyColor(value: string) {
this.colors[CustomProperties.COLOR_STROKE] = Utils.hexToRgba(value);
this.colors[CustomProperties.COLOR_FILL] = Utils.hexToRgba(value, Config.DEFAULT_FILL_OPACITY_TO_STROKE);
this.definePreviewStyle();
}
/**
* Resets the custom color
*/
resetColor() {
this.colors[CustomProperties.COLOR_STROKE] = null;
this.colors[CustomProperties.COLOR_FILL] = null;
this.isDefaultColor = true;
this._selectedColor = null;
this.definePreviewStyle();
this.colorChangeEmitter.emit(this.colors);
}
/**
* Eventhandler that is fired when a color is picked
* @param event
*/
onColorPicked(color: string) {
localStorage.setItem(this.prevColorStorageKey, color);
this.selectedColor = color;
this.isDefaultColor = false;
}
}
@@ -0,0 +1,13 @@
<div *ngIf="curFeature" class="property-list">
<div class="header">
<h4>Custom Properties</h4>
</div>
<lm-custom-color-entry [colors]="curFeatureColors"
(colorChanged)="onFeatureColorChanged($event)"></lm-custom-color-entry>
<div class="header">
<h4>Properties</h4>
</div>
<div *ngFor="let item of curProperties | keyvalue" class="entry">
<span class="property-name">{{item.key}}</span> : <span class="property-value">{{item.value}}</span>
</div>
</div>
@@ -0,0 +1,30 @@
.header,
.entry {
display: flex;
height: 1.5rem;
align-items: center;
margin: 0 0 .125rem 0;
border: 1px black solid;
padding: .5rem;
.controls {
margin-left: auto;
}
.property-name {
}
.property-value {
margin: 0 0 0 6px;
}
}
.header {
background-color: whitesmoke;
}
.button-container {
margin-top: 3rem;
}
@@ -0,0 +1,81 @@
import {Component, Input, Output, EventEmitter} from '@angular/core';
import {Config} from '../../misc/Config';
import {CustomProperties} from '../../enums/CustomProperties';
import {Feature} from 'ol/Feature';
import {RawLayer} from 'src/app/models/RawLayer';
/**
* This component manages all properties of the current feature. It does not matter if it is a simple geo json
* property or a custom property (custom properties are special properties that are just understand by life map
* like the color of the feature). All changes that were made to the properties are saved in the geo json of
* the RawLayer object from which the open layer feature based.
*
* TODO:
* - If a layer with many properties is selected, the list of these properties is too long. A better design has to be used.
* -
*/
@Component({
selector: 'lm-feature-manager',
templateUrl: 'feature-manager.component.html',
styleUrls: ['feature-manager.component.scss']
})
export class FeatureManagerComponent {
/**
* The feature that is selected to manage
*/
private _curFeature: Feature;
@Input() set curFeature(value: Feature) {
this._curFeature = value;
}
get curFeature(): Feature {
return this._curFeature;
}
/**
* An event emitter that is fired if a feature is changed,
*/
@Output('featureChange') featureChangeEmitter: EventEmitter<Feature> = new EventEmitter();
/**
* Returns the properties of curFeature
*/
get curProperties(): any {
const properties = this.curFeature.getProperties();
delete properties.geometry;
delete properties[Config.GEO_JSON_PREFIX];
return properties;
}
/**
* Returns the custom properties of curFeature
*/
get curCustomProperties(): any {
const properties = this.curFeature.getProperties();
return properties[Config.GEO_JSON_PREFIX];
}
/**
* Returns the default color properties of the current feature
*/
get curFeatureColors(): any {
const colors = {};
colors[CustomProperties.DEFAULT_COLOR_STROKE] = this.curCustomProperties[CustomProperties.DEFAULT_COLOR_STROKE];
colors[CustomProperties.DEFAULT_COLOR_FILL] = this.curCustomProperties[CustomProperties.DEFAULT_COLOR_FILL];
colors[CustomProperties.COLOR_STROKE] = this.curCustomProperties[CustomProperties.COLOR_STROKE];
colors[CustomProperties.COLOR_FILL] = this.curCustomProperties[CustomProperties.COLOR_FILL];
return colors;
}
/**
* Event handler if the color set of a feature was changed
* @param colors
*/
onFeatureColorChanged(colors: any) {
RawLayer.saveCustomProperties(this.curFeature, colors);
this.curFeature.setStyle(RawLayer.determineColorStyle(colors));
this.featureChangeEmitter.emit(this.curFeature);
}
}
@@ -1 +0,0 @@
<div id="mapid"></div>
@@ -1 +0,0 @@
#mapid { height: 50vh; }
@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LeafletMapComponent } from './leaflet-map.component';
describe('LeafletMapComponent', () => {
let component: LeafletMapComponent;
let fixture: ComponentFixture<LeafletMapComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LeafletMapComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LeafletMapComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,52 +0,0 @@
import {Component, OnInit} from '@angular/core';
import * as L from 'Leaflet';
import {HttpClient} from "@angular/common/http";
import {GeoJSON} from "Leaflet";
import {GeoJsonObject} from "geojson";
@Component({
selector: 'app-leaflet-map',
templateUrl: './leaflet-map.component.html',
styleUrls: ['./leaflet-map.component.scss']
})
export class LeafletMapComponent implements OnInit {
private map;
private vectorLayerFiles: string[] = [
'./assets/geolayer/flurstuecke-nutzer-baum-kompensation.geojson',
// './assets/geolayer/bebauung.geojson',
'./assets/geolayer/feldwege.geojson',
'./assets/geolayer/gehoelze.geojson',
'./assets/geolayer/gewaesser.geojson',
// './assets/geolayer/grabensysteme.geojson',
// './assets/geolayer/grabenuebergaenge.geojson',
// './assets/geolayer/gsp_einsatzgebiete.geojson',
// './assets/geolayer/stromleitungen.geojson',
// './assets/geolayer/strommasten.geojson',
// './assets/geolayer/windenergieanlagen.geojson',
];
constructor(private http: HttpClient) {
}
async ngOnInit() {
this.map = L.map('mapid').setView([53.1582, 8.7728], 13);
// L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
// attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
// }).addTo(this.map);
// L.marker([53.1582, 8.7728]).addTo(this.map)
// .bindPopup('A pretty CSS3 popup.<br> Easily customizable.')
// .openPopup();
const layers : any[] = await this.getGeoJsons();
layers.forEach(layer => L.geoJSON(layer).addTo(this.map));
}
async getGeoJsons() {
const promises = this.vectorLayerFiles.map(file => this.http.get(file).toPromise());
return await Promise.all(promises);
}
}
+13 -8
View File
@@ -16,15 +16,13 @@
</div>
</div>
<!-- layer property area-->
<div class="layer-property-area">
<div class="property-list-area">
<mat-card>
<mat-card-header>
<mat-card-title>Layer Properties</mat-card-title>
</mat-card-header>
<mat-card-content>
<div>
{{ currentFeatureInfo | json}}
</div>
<lm-feature-manager [curFeature]="cache.selectedFeature"></lm-feature-manager>
</mat-card-content>
</mat-card>
</div>
@@ -32,15 +30,20 @@
<div class="layer-control-area">
<mat-card>
<mat-card-header>
<mat-card-title>Layer</mat-card-title>
<mat-card-title>Layer
</mat-card-title>
<div class="controls">
<i class="material-icons" (click)="addLayer()">add_circle</i>
<i class="material-icons"
matTooltip="Über diese Schaltfläche können Sie neue Layer hinzufügen.
Bitte beachten Sie, dass die Reihenfolge der Layer in der Liste umgekehrt gegenüber Reihenfolge in der Map ist."
(click)="addLayer()">add_circle</i>
</div>
</mat-card-header>
<mat-card-content>
<div cdkDropList class="layer-list" (cdkDropListDropped)="drop($event)">
<div class="list-entry" *ngFor="let layer of customLayerCollection.getArray();let i = index" cdkDrag>
<h4>{{layer.get('name') }}</h4>
<h4 class="title">{{layer.get('name') }}</h4>
<div class="layer-controls">
<i class="material-icons" (click)="centerLayerOnMap(layer)">
center_focus_strong
</i>
@@ -55,9 +58,11 @@
</i>
</div>
</div>
</div>
</mat-card-content>
<mat-card-actions>
<button mat-button (click)="saveLayers()">Speichern</button>
<button mat-button (click)="exportLayerForApp()">Export für App</button>
<button mat-button (click)="exportLayerAsGeoJson()">Export als GeoJSON</button>
</mat-card-actions>
</mat-card>
</div>
+25 -4
View File
@@ -38,7 +38,11 @@
}
.layer-control-area {
max-width:25vw;
.controls {
cursor: pointer;
}
.layer-list {
max-width: 100%;
@@ -51,18 +55,31 @@
.list-entry {
padding: 20px 10px;
padding: 10px;
border-bottom: solid 1px #ccc;
color: rgba(0, 0, 0, 0.87);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
flex-direction: column;
align-items: flex-start;
justify-content: center;
box-sizing: border-box;
cursor: move;
background: white;
font-size: 14px;
.title {
margin: 10px 0px 10px 0;
word-break: break-word;
}
.layer-controls {
> * {
margin-right: 8px;
cursor: pointer;
}
}
&:last-child {
border: none;
}
@@ -73,6 +90,10 @@
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
}
.property-list-area {
max-width: 25vw;
}
}
.cdk-drag-preview {
+303 -68
View File
@@ -5,17 +5,33 @@ import TileLayer from 'ol/layer/Tile';
import {fromLonLat, transform} from 'ol/proj';
import VectorLayer from 'ol/layer/Vector';
import {OSM} from 'ol/source';
import {CdkDragDrop} from "@angular/cdk/drag-drop";
import {MatDialog} from "@angular/material";
import {AddLayerComponent} from "../add-layer-component/add-layer.component";
import {RawLayer} from "../models/RawLayer";
import {CdkDragDrop} from '@angular/cdk/drag-drop';
import {MatDialog} from '@angular/material';
import {AddLayerComponent} from '../add-layer-component/add-layer.component';
import {RawLayer} from '../models/RawLayer';
import LayerGroup from 'ol/layer/Group';
import {Collection} from "openlayers";
import {EPSG} from "../enums/EPSG";
import {Collection} from 'openlayers';
import {EPSG} from '../enums/EPSG';
import {inAndOut} from 'ol/easing';
import {Feature} from 'ol/Feature';
import Style from 'ol/style/Style';
import Stroke from 'ol/style/Stroke';
import Fill from 'ol/style/Fill';
import {FeatureManagerComponent} from '../feature-management/feature-manager/feature-manager.component';
import {saveAs} from 'file-saver';
import {Config} from '../misc/Config';
import * as JSZip from 'jszip';
/**
* TODO: add description
*
* TODO
* - Sometimes the cursor is not there where it is displayed
* - The order of the layers inside the layer menu does not reflect the real order on the map
* - extend feature manager to handle more properties
* - The design und ux of the whole application has to be improved
* - Add bunch edit to ease coloring of the features
* - add possibility to filter for features to color them easier (or add some kind of custom query language)
*/
@Component({
selector: 'app-map',
@@ -25,6 +41,7 @@ import {inAndOut} from 'ol/easing';
export class MapComponent implements OnInit {
@ViewChild('mapContainer') mapContainer: ElementRef<HTMLDivElement>;
@ViewChild(FeatureManagerComponent) featureManager: FeatureManagerComponent;
public map: Map;
public baseLayer: TileLayer;
@@ -46,17 +63,50 @@ export class MapComponent implements OnInit {
public cursorCoordinates: string;
/**
* The properties of the current feature. The current layer is the layer beneath the cursor.
* A simple cache that holds different Features and styles
*/
public currentFeatureInfo: any;
private cache: { [key: string]: Feature | Style } = {
/**
* Contains the feature that is currently selected
*/
selectedFeature: null,
// /**
// * The layer of the selected feature
// */
// selectedLayer: null,
/**
* The feature that was highlighted at least
*/
lastHighlightedFeature: null,
/**
* The style of the last highlighted feature before it was highlighted
*/
lastHighlightedStyle: null,
/**
* Default style for selected feature
*/
defaultSelectedStyle: null,
/**
* Default style for a highlighted feature
*/
defaultHighlightStyle: null
};
constructor(public dialog: MatDialog) {
}
ngOnInit() {
this.initCache();
this.initBaseLayer();
this.initLayerGroup();
this.initMap();
this.addMockData();
}
/**
@@ -93,18 +143,18 @@ export class MapComponent implements OnInit {
})
});
this.addMapEventlistener();
this.addMapEventListener();
this.addFeatureManagerEventListener();
// without this line the map is only visible after resizing window
requestAnimationFrame(() => {
this.map.updateSize();
})
});
}
/**
* Adds all map event listener
*/
private addMapEventlistener() {
private addMapEventListener() {
// get current cursor coordinates to transform and display them
this.map.on('pointermove', (evt) => {
const tmpCoords = transform(evt.coordinate, EPSG.openLayersDefault, EPSG.wgs84);
@@ -113,71 +163,92 @@ export class MapComponent implements OnInit {
// add event to get feature infos
this.map.on('pointermove', (evt) => {
if (evt.dragging) {
if (evt.dragging || this.cache.selectedFeature) {
return;
}
const pixel = this.map.getEventPixel(evt.originalEvent);
this.displayFeatureInfo(pixel);
const features = this.map.getFeaturesAtPixel(pixel);
if (features && features.length > 0) {
this.map.getTargetElement().style.cursor = 'pointer';
this.featureManager.curFeature = features[0];
this.highlightFeature(features[0]);
} else {
this.map.getTargetElement().style.cursor = '';
// reapply original style to last highlighted feature
if (this.cache.lastHighlightedFeature) {
this.cache.lastHighlightedFeature.setStyle(this.cache.lastHighlightedStyle);
}
// reset cache for feature and style
this.cache.lastHighlightedFeature = null;
this.cache.lastHighlightedStyle = null;
this.featureManager.curFeature = null;
}
});
// handle feature selection
this.map.on('click', (evt) => {
const pixel = this.map.getEventPixel(evt.originalEvent);
// const features = this.map.getFeaturesAtPixel(pixel);
let selectedFeature;
// let selectedLayer;
// get the feature and the according layer
this.map.forEachFeatureAtPixel(pixel, (feature, layer) => {
selectedFeature = feature;
// selectedLayer = layer;
return true;
});
if (this.cache.selectedFeature) {
// use lastHighlightedStyle because the original style is saved there
if (this.cache.lastHighlightedStyle) {
this.cache.selectedFeature.setStyle(this.cache.lastHighlightedStyle);
}
this.cache.selectedFeature = null;
// this.cache.selectedLayer = null;
} else if (selectedFeature) {
this.map.getTargetElement().style.cursor = '';
this.cache.selectedFeature = selectedFeature;
// this.cache.selectedLayer = selectedLayer;
this.featureManager.curFeature = this.cache.selectedFeature;
this.cache.lastHighlightedFeature = null;
this.cache.selectedFeature.setStyle(this.cache.defaultSelectedStyle);
}
});
}
/**
* Gets all feature infos for each feature at the given pixel.
* @param pixel
* Adds all feature manager event listener
*/
private displayFeatureInfo(pixel) {
const features = [];
this.map.forEachFeatureAtPixel(pixel, (feature) => {
features.push(feature)
private addFeatureManagerEventListener() {
this.featureManager.featureChangeEmitter.subscribe(() => {
// - reset selection values
// this.cache.selectedFeature = null;
this.cache.lastHighlightedStyle = null;
});
}
);
if (features.length == 0) {
this.currentFeatureInfo = 'Too many features. Please zoom in to see feature infos';
} else if (features.length == 1) {
features.forEach(feature => {
const properties = feature.getProperties();
delete properties.geometry;
this.currentFeatureInfo = properties;
})
} else {
this.currentFeatureInfo = null;
/**
* Highlights a Features
* @param feature
*/
private highlightFeature(feature: Feature) {
// reapply saved feature and style
if (this.cache.lastHighlightedFeature) {
this.cache.lastHighlightedFeature.setStyle(this.cache.lastHighlightedStyle);
}
};
// save current feature and style
this.cache.lastHighlightedFeature = feature;
this.cache.lastHighlightedStyle = feature.getStyle();
// private colorPolygons(feature: Feature, resolution: any) {
// const styles = {
// black: new Style({
// stroke: new Stroke({
// color: '#86665a',
// width: 1
// }),
// fill: new Fill({
// color: 'rgba(121,85,72, 0.4)',
// })
// }),
// green: new Style({
// stroke: new Stroke({
// color: '#b8c633',
// width: 1
// }),
// fill: new Fill({
// color: 'rgba(205,220,57,0.4)',
// })
// })
// }
// // console.log(feature.getProperties()['Grünland'],feature.getProperties());
// if (feature.getProperties()['Nutzung'] === 'Grünland') {
// return styles.green;
// } else {
// return styles.black;
// }
// }
// set new feature style
feature.setStyle(this.cache.defaultHighlightStyle);
}
/**
* Opens a dialog to create a new custom layer
*/
addLayer(): void {
public addLayer(): void {
const dialogRef = this.dialog.open(AddLayerComponent);
dialogRef.afterClosed().subscribe(rawLayer => {
if (rawLayer) {
@@ -192,7 +263,7 @@ export class MapComponent implements OnInit {
/**
* Opens a dialog to edit a custom layer
*/
editLayer(index: number): void {
public editLayer(index: number): void {
const vectorLayer = this.customLayerCollection.item(index);
const rawLayer = RawLayer.fromVectorLayer(vectorLayer);
const dialogRef = this.dialog.open(AddLayerComponent, {data: rawLayer});
@@ -213,13 +284,17 @@ export class MapComponent implements OnInit {
* Updates the customLayer collection according to drag and drop actions.
* @param event
*/
drop(event: CdkDragDrop<RawLayer[]>) {
public drop(event: CdkDragDrop<RawLayer[]>) {
const tmp = this.customLayerCollection.item(event.previousIndex);
this.customLayerCollection.removeAt(event.previousIndex);
this.customLayerCollection.insertAt(event.currentIndex, tmp);
}
deleteLayer(layer: VectorLayer) {
/**
* Deletes a layer from custom layer collection
* @param layer
*/
public deleteLayer(layer: VectorLayer) {
this.customLayerCollection.remove(layer);
}
@@ -227,14 +302,51 @@ export class MapComponent implements OnInit {
* Toggles the visibility of a custom layer
* @param index
*/
toggleLayerVisibility(layer: VectorLayer) {
public toggleLayerVisibility(layer: VectorLayer) {
layer.setVisible(!layer.getVisible());
}
saveLayers(): RawLayer[] {
/**
* Export all Layers in one file for app usage
*/
public exportLayerForApp(): void {
const rawLayers = [];
this.customLayerCollection.forEach(layer => rawLayers.push(RawLayer.fromVectorLayer(layer)));
return rawLayers;
// process raw layer to avoid escaping in data property e. g. {\"type\": \"aaa\"...
// the geo json string in data property comes like this from openlayer function GeoJSON().writeFeatures
const processedRawLayers = rawLayers.map(rawLayer => {
return {
name: rawLayer.name,
data: JSON.parse(rawLayer.data)
};
});
const blob = new Blob([JSON.stringify(processedRawLayers)], {type: 'application/json;charset=utf-8'});
saveAs(blob, `app_export_${new Date().toISOString()}.json`);
}
/**
* Exports all layer as single geoJson files in a zip for later processing
*/
public exportLayerAsGeoJson(): void {
const zipArchive = new JSZip();
const rawLayers = [];
this.customLayerCollection.forEach(layer => rawLayers.push(RawLayer.fromVectorLayer(layer)));
// create a zip archive
rawLayers.forEach(rawLayer => {
// process raw layer with JSON.stringify(JSON.parse(...)) to avoid escaping in data property e. g. {\"type\": \"aaa\"...
// the geo json string in data property comes like this from openlayer function GeoJSON().writeFeatures
const fileContent = new Blob([JSON.stringify(JSON.parse(rawLayer.data))], {type: 'application/geo+json;charset=utf-8'});
const fileName = `${rawLayer.name}.geojson`;
zipArchive.file(fileName, fileContent);
});
// save zip archive
zipArchive.generateAsync({type: 'blob'})
.then((blob) => {
saveAs(blob, `lm_export_${new Date().toISOString()}.zip`);
});
}
/**
@@ -246,7 +358,130 @@ export class MapComponent implements OnInit {
size: this.map.getSize(),
duration: 888,
easing: inAndOut
}
};
this.map.getView().fit(customLayer.getSource().getExtent(), options);
}
/**
* Support function that inits cache values
*/
private initCache() {
this.cache.defaultSelectedStyle = new Style({
stroke: new Stroke({
color: 'rgb(245, 64, 41)',
width: 3
}),
fill: new Fill({
color: 'rgba(245, 64, 41, 0.3)',
})
});
this.cache.defaultHighlightStyle = new Style({
stroke: new Stroke({
color: 'rgb(245, 64, 41)',
width: 2
}),
fill: new Fill({
color: 'rgba(245, 64, 41, 0.1)',
})
});
}
/**
* DeBUG DEBUG DBEUG
*/
private addMockData() {
const data = {
'type': 'FeatureCollection',
'name': 'background',
'crs': {
'type': 'name',
'properties': {
'name': 'urn:ogc:def:crs:OGC:1.3:CRS84'
}
},
'features': [{
'type': 'Feature',
'properties': {
'name': 'Feature 1'
},
'geometry': {
'type': 'MultiPolygon',
'coordinates': [
[
[
[8.801864395832126, 53.14770721254275],
[8.806370506977146, 53.14770721254275],
[8.806542168354099, 53.14500456859233],
[8.80203605720908, 53.14521049030746]
]
]
]
}
},
{
'type': 'Feature',
'properties': {
'name': 'Feature 2'
},
'geometry': {
'type': 'MultiPolygon',
'coordinates': [
[
[
[8.807787400997881, 53.1477644198588],
[8.808044893063311, 53.145010298994144],
[8.813452226437335, 53.145010298994144],
[8.813194734371905, 53.1477644198588]
]
]
]
}
},
{
'type': 'Feature',
'properties': {
'name': 'Feature 3'
},
'geometry': {
'type': 'MultiPolygon',
'coordinates': [
[
[
[8.80207966021419, 53.14392919393571],
[8.802122575558428, 53.138780701270434],
[8.80632827929378, 53.13896090893504],
[8.806542856014971, 53.14410938000193]
]
]
]
}
},
{
'type': 'Feature',
'properties': {
'name': 'Feature 4'
},
'geometry': {
'type': 'MultiPolygon',
'coordinates': [
[
[
[8.808654874418039, 53.143631496039035],
[8.809770673368234, 53.1391523112723],
[8.816036313627023, 53.13930677369489],
[8.8156071601846, 53.14352853151957]
]
]
]
}
}
]
};
const rawLayer = new RawLayer('Mock Data Layer', JSON.stringify(data));
rawLayer.saveDefaultColor([125, 125, 125]);
const customLayer = RawLayer.toVectorLayer(rawLayer);
this.centerLayerOnMap(customLayer);
this.customLayerCollection.push(customLayer);
}
}
+2 -1
View File
@@ -1,3 +1,4 @@
export class Config{
static readonly GEO_JSON_PREFIX: string = 'GANE';
static readonly GEO_JSON_PREFIX: string = 'LM';
static readonly DEFAULT_FILL_OPACITY_TO_STROKE: number = 0.1;
}
+16
View File
@@ -25,4 +25,20 @@ export class Utils {
opacity
] : null;
}
public static colorArrayToRgba(colors: number[]): string{
let tmp = 'rgba(';
for(let i = 0; i < 4; i++){
const color = colors[i];
if(color){
tmp+= color+',';
}else {
tmp += '1';
}
}
// remove last comma
tmp = tmp.replace(/,\s*$/, "");
tmp += ')';
return tmp;
}
}
+103 -52
View File
@@ -1,11 +1,13 @@
import VectorLayer from 'ol/layer/Vector';
import GeoJSON from 'ol/format/GeoJSON';
import {Vector as VectorSource} from 'ol/source';
import {EPSG} from "../enums/EPSG";
import {EPSG} from '../enums/EPSG';
import Style from 'ol/style/Style';
import Stroke from 'ol/style/Stroke';
import Fill from 'ol/style/Fill';
import {Config} from "../misc/Config";
import {Config} from '../misc/Config';
import {Feature} from 'ol/Feature';
import {CustomProperties} from '../enums/CustomProperties';
/**
* Represents a raw (not processed) map layer
@@ -19,15 +21,8 @@ export class RawLayer {
/**
* Data of Layer (normally in geojson format)
*/
public data: string,
/**
* Layer stroke color as number array with opacity like -> [255, 255, 255, 0.5]
* The fill color is an opaque decreased derivation of this value.
*/
public color: number[]
) {
this.saveColorToGeoJSON();
}
public data: string
) {}
/**
* Creates a RawLayer instance from a VectorLayer and saves the style properties in geojson. Consider that the
@@ -35,46 +30,24 @@ export class RawLayer {
* @param layer
*/
public static fromVectorLayer(layer: VectorLayer): RawLayer {
const writer = new GeoJSON();
const source = layer.getSource();
// get default style values
const features = source.getFeatures();
const featureProperties = features[0].getProperties();
const rawLayerColor = featureProperties[Config.GEO_JSON_PREFIX]['default-style-stroke'];
const geoJSONString = writer.writeFeatures(features,{featureProjection: EPSG.openLayersDefault});
return new RawLayer(layer.get('name'), geoJSONString, rawLayerColor);
const geoJSONString = RawLayer.featuresToGeoJSON(features);
return new RawLayer(layer.get('name'), geoJSONString);
}
/**
* Creates a VectorLayer instance from a RawLayer and applies the layer color to all features
* Creates a VectorLayer instance from a RawLayer and applies the layer or custom color to all features
* @param rawLayer
*/
public static toVectorLayer(rawLayer: RawLayer): VectorLayer {
const vectorSource = new VectorSource();
const geoJSON = new GeoJSON();
// load geoJSON features from rawLayer
const features = geoJSON.readFeatures(rawLayer.data, {featureProjection: EPSG.openLayersDefault});
const features = RawLayer.geoJSONToFeatures(rawLayer.data);
// apply style to all features
features.forEach(feature => {
// TODO
// determine if this feature has a own color! If so, do not apply default color and use own color.
const featureProperties = feature.getProperties();
const strokeColor = featureProperties[Config.GEO_JSON_PREFIX]['default-style-stroke'];
const fillColor = featureProperties[Config.GEO_JSON_PREFIX]['default-style-fill'];
feature.setStyle(new Style({
stroke: new Stroke({
color: strokeColor,
width: 1
}),
fill: new Fill({
color: fillColor
})
}));
})
const featureCustomProperties = RawLayer.getCustomProperties(feature);
feature.setStyle(this.determineColorStyle(featureCustomProperties));
});
// add features to source
vectorSource.addFeatures(features);
@@ -87,20 +60,98 @@ export class RawLayer {
}
/**
* Adds the RawLayer color property to all feature properties from data attribute
* Creates the features from geoJSON
*/
private saveColorToGeoJSON() {
const geoJSON = new GeoJSON();
// load geoJSON features from rawLayer
const features = geoJSON.readFeatures(this.data, {featureProjection: EPSG.openLayersDefault});
private static geoJSONToFeatures(geoJSON: string): Feature[] {
return new GeoJSON().readFeatures(geoJSON, {featureProjection: EPSG.openLayersDefault});
}
/**
* Creates geoJSON string from features
* @param features
*/
private static featuresToGeoJSON(features: Feature[]): string {
return new GeoJSON().writeFeatures(features, {featureProjection: EPSG.openLayersDefault});
}
/**
* Saves arbitrary key value pairs to geo json representation of this layer as custom properties
* @param feature
* @param properties
*/
public static saveCustomProperties(feature: Feature, properties: { [key: string]: any }) {
const newProperties = {};
// consider custom properties that were saved before
const savedProperties = RawLayer.getCustomProperties(feature);
newProperties[Config.GEO_JSON_PREFIX] = {...savedProperties, ...properties};
feature.setProperties(newProperties);
}
/**
* Returns all custom properties of this layer from its geo json representation
* @param feature
*/
public static getCustomProperties(feature: Feature): { [key: string]: any } {
const featureProperties = feature.getProperties();
return featureProperties[Config.GEO_JSON_PREFIX];
}
/**
* Determines which color style should be applied. If no custom color property is provided, the default
* color is used.
* @param featureCustomProperties
*/
public static determineColorStyle(featureCustomProperties: { [key: string]: any }): Style {
if (featureCustomProperties && featureCustomProperties[CustomProperties.COLOR_STROKE]) {
return new Style({
stroke: new Stroke({
color: featureCustomProperties[CustomProperties.COLOR_STROKE],
width: 1
}),
fill: new Fill({
color: featureCustomProperties[CustomProperties.COLOR_FILL]
})
});
} else if (featureCustomProperties && featureCustomProperties[CustomProperties.DEFAULT_COLOR_STROKE]) {
return new Style({
stroke: new Stroke({
color: featureCustomProperties[CustomProperties.DEFAULT_COLOR_STROKE],
width: 1
}),
fill: new Fill({
color: featureCustomProperties[CustomProperties.DEFAULT_COLOR_FILL]
})
});
} else {
throw new Error('no color properties are defined');
}
}
/**
* Saves a default color property to geoJSON
*/
public saveDefaultColor(color: number[]) {
const features = RawLayer.geoJSONToFeatures(this.data);
features.forEach(feature => {
// save default color in feature properties
const stylesToSave = {};
stylesToSave[Config.GEO_JSON_PREFIX] = {};
stylesToSave[Config.GEO_JSON_PREFIX]['default-style-stroke'] = this.color;
stylesToSave[Config.GEO_JSON_PREFIX]['default-style-fill'] = [...this.color.slice(0, 3), 0.1];
feature.setProperties(stylesToSave);
})
this.data = geoJSON.writeFeatures(features, {featureProjection: EPSG.openLayersDefault});
const properties = {};
properties[CustomProperties.DEFAULT_COLOR_STROKE] = color;
properties[CustomProperties.DEFAULT_COLOR_FILL] = [...color.slice(0, 3), Config.DEFAULT_FILL_OPACITY_TO_STROKE];
RawLayer.saveCustomProperties(feature, properties);
});
this.data = RawLayer.featuresToGeoJSON(features);
}
/**
* Reads the default color property from geoJSON
*/
public getDefaultColor(): number[] {
const features = RawLayer.geoJSONToFeatures(this.data);
const featureCustomProperties = RawLayer.getCustomProperties(features[0]);
if (featureCustomProperties && featureCustomProperties[CustomProperties.DEFAULT_COLOR_STROKE]) {
return featureCustomProperties[CustomProperties.DEFAULT_COLOR_STROKE];
}
return null;
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>GaneWeb</title>
<title>Life Map Web</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
+1 -1
View File
@@ -16,7 +16,7 @@ module.exports = function (config) {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../coverage/gane-web'),
dir: require('path').join(__dirname, '../coverage/lm-web'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},