1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-12-27 02:09:16 +02:00

adding zoom control and multiple layer support to maps

This commit is contained in:
Patrik J. Braun 2019-07-20 19:52:47 +02:00
parent 88a2460d48
commit 875f300ba1
16 changed files with 232 additions and 159 deletions

View File

@ -163,8 +163,15 @@ export class ConfigDiagnostics {
throw new Error('Mapbox needs a valid api key.');
}
if (map.mapProvider === ClientConfig.MapProviders.Custom &&
(!map.tileUrl || map.tileUrl.length === 0)) {
throw new Error('Custom maps need a valid tile url');
(!map.customLayers || map.customLayers.length === 0)) {
throw new Error('Custom maps need at least one valid layer');
}
if (map.mapProvider === ClientConfig.MapProviders.Custom) {
map.customLayers.forEach(l => {
if (!l.url || l.url.length === 0) {
throw new Error('Custom maps url need to be a valid layer');
}
});
}
}

View File

@ -31,11 +31,16 @@ export module ClientConfig {
enabled: boolean;
}
export interface MapLayers {
name: string;
url: string;
}
export interface MapConfig {
enabled: boolean;
mapProvider: MapProviders;
mapboxAccessToken: string;
tileUrl: string;
customLayers: MapLayers[];
}
export interface ThumbnailConfig {
@ -125,7 +130,7 @@ export class PublicConfigClass {
enabled: true,
mapProvider: ClientConfig.MapProviders.OpenStreetMap,
mapboxAccessToken: '',
tileUrl: ''
customLayers: [{name: 'street', url: ''}]
},
RandomPhoto: {
enabled: true

View File

@ -90,12 +90,10 @@ export class ControlsLightboxComponent implements OnDestroy, OnInit, OnChanges {
}
public containerWidth() {
console.log(this.photoFrameDim);
return this.root.nativeElement.width;
}
public containerHeight() {
console.log(this.photoFrameDim);
return this.root.nativeElement.height;
}

View File

@ -10,46 +10,70 @@
<yaga-map #yagaMap
[style.width.px]="mapDimension.width"
[style.height.px]="mapDimension.height">
<yaga-polyline *ngFor="let path of paths"
[latLngs]="path">
</yaga-polyline>
<yaga-marker
*ngFor="let path of paths"
[lat]="path[0].lat"
[lng]="path[1].lng">
</yaga-marker>
<yaga-marker
*ngFor="let photo of mapPhotos"
[lat]="photo.lat"
[lng]="photo.lng">
<yaga-icon
*ngIf="photo.iconUrl"
[iconUrl]="photo.iconUrl"
[iconSize]="yagaMap.zoom < 15 ? smallIconSize : iconSize"
></yaga-icon>
<yaga-popup
(open)="loadPreview(photo)"
[minWidth]="photo.preview.width">
<img *ngIf="photo.preview.thumbnail.Src"
[style.width.px]="photo.preview.width"
[style.height.px]="photo.preview.height"
[src]="photo.preview.thumbnail.Src | fixOrientation:photo.orientation | async">
<div class="preview-loading"
*ngIf="!photo.preview.thumbnail.Src"
[style.width.px]="photo.preview.width"
[style.height.px]="photo.preview.height">
<yaga-layers-control
position="bottomright">
<yaga-feature-group
[caption]="'path'"
*ngIf="paths.length>0"
yaga-overlay-layer="'path'">
<yaga-polyline
*ngFor="let path of paths"
[latLngs]="path">
</yaga-polyline>
<yaga-marker
*ngFor="let path of paths"
[lat]="path[0].lat"
[lng]="path[1].lng">
</yaga-marker>
</yaga-feature-group>
<yaga-feature-group
[caption]="'photos'"
yaga-overlay-layer="'photos'">
<yaga-marker [title]="photo.name"
*ngFor="let photo of mapPhotos"
[lat]="photo.lat"
[lng]="photo.lng">
<yaga-icon
*ngIf="photo.iconUrl"
[iconUrl]="photo.iconUrl"
[iconSize]="yagaMap.zoom < 15 ? smallIconSize : iconSize"
></yaga-icon>
<yaga-popup
(open)="loadPreview(photo)"
[minWidth]="photo.preview.width">
<img *ngIf="photo.preview.thumbnail.Src"
[style.width.px]="photo.preview.width"
[style.height.px]="photo.preview.height"
[src]="photo.preview.thumbnail.Src | fixOrientation:photo.orientation | async">
<div class="preview-loading"
*ngIf="!photo.preview.thumbnail.Src"
[style.width.px]="photo.preview.width"
[style.height.px]="photo.preview.height">
<span class="oi"
[ngClass]="photo.preview.thumbnail.Error ? 'oi-warning' : 'oi-picture'"
aria-hidden="true">
</span>
</div>
</yaga-popup>
</yaga-marker>
</div>
</yaga-popup>
</yaga-marker>
</yaga-feature-group>
<ng-container *ngFor="let l of mapService.Layers">
<yaga-tile-layer yaga-base-layer
[caption]="l.name"
[url]="l.url"></yaga-tile-layer>
</ng-container>
</yaga-layers-control>
<yaga-zoom-control position="bottomright">
</yaga-zoom-control>
<yaga-attribution-control
prefix=""
[attributions]="mapService.Attributions">
</yaga-attribution-control>
<yaga-tile-layer [url]="mapService.MapLayer"></yaga-tile-layer>
</yaga-map>
</div>

View File

@ -1,4 +1,4 @@
import {Component, ElementRef, HostListener, Input, OnChanges, ViewChild, AfterViewInit} from '@angular/core';
import {AfterViewInit, Component, ElementRef, HostListener, Input, OnChanges, OnInit, ViewChild} from '@angular/core';
import {PhotoDTO} from '../../../../../../common/entities/PhotoDTO';
import {Dimension} from '../../../../model/IRenderable';
import {FullScreenService} from '../../fullscreen.service';
@ -23,9 +23,9 @@ import {FixOrientationPipe} from '../../../../pipes/FixOrientationPipe';
})
export class GalleryMapLightboxComponent implements OnChanges, AfterViewInit {
@Input() photos: PhotoDTO[];
@Input() gpxFiles: FileDTO[];
private startPosition: Dimension = null;
public lightboxDimension: Dimension = <Dimension>{top: 0, left: 0, width: 0, height: 0};
public mapDimension: Dimension = <Dimension>{top: 0, left: 0, width: 0, height: 0};
public visible = false;
@ -33,27 +33,34 @@ export class GalleryMapLightboxComponent implements OnChanges, AfterViewInit {
public opacity = 1.0;
mapPhotos: MapPhoto[] = [];
paths: LatLng[][] = [];
@ViewChild('root', {static: false}) elementRef: ElementRef;
@ViewChild('yagaMap', {static: false}) yagaMap: MapComponent;
@ViewChild('root', {static: true}) elementRef: ElementRef;
@ViewChild('yagaMap', {static: true}) yagaMap: MapComponent;
public smallIconSize = new Point(Config.Client.Thumbnail.iconSize * 0.75, Config.Client.Thumbnail.iconSize * 0.75);
public iconSize = new Point(Config.Client.Thumbnail.iconSize, Config.Client.Thumbnail.iconSize);
private startPosition: Dimension = null;
constructor(public fullScreenService: FullScreenService,
private thumbnailService: ThumbnailManagerService,
public mapService: MapService) {
}
ngOnChanges() {
if (this.visible === false) {
return;
}
this.showImages();
}
ngAfterViewInit() {
let i = 0;
this.yagaMap.eachLayer(l => {
if (i >= 3 || (this.paths.length === 0 && i >= 2)) {
this.yagaMap.removeLayer(l);
}
++i;
});
}
@HostListener('window:resize', ['$event'])
@ -75,7 +82,6 @@ export class GalleryMapLightboxComponent implements OnChanges, AfterViewInit {
}
public async show(position: Dimension) {
this.hideImages();
this.visible = true;
@ -145,6 +151,7 @@ export class GalleryMapLightboxComponent implements OnChanges, AfterViewInit {
const iconTh = this.thumbnailService.getIcon(new MediaIcon(p));
iconTh.Visible = true;
const obj: MapPhoto = {
name: p.name,
lat: p.metadata.positionData.GPSData.latitude,
lng: p.metadata.positionData.GPSData.longitude,
iconThumbnail: iconTh,
@ -175,29 +182,6 @@ export class GalleryMapLightboxComponent implements OnChanges, AfterViewInit {
}
private centerMap() {
if (this.mapPhotos.length > 0) {
this.yagaMap.fitBounds(<any>this.mapPhotos);
}
}
private async loadGPXFiles(): Promise<void> {
this.paths = [];
for (let i = 0; i < this.gpxFiles.length; i++) {
const file = this.gpxFiles[i];
const path = await this.mapService.getMapPath(file);
if (file !== this.gpxFiles[i]) { // check race condition
return;
}
if (path.length === 0) {
continue;
}
this.paths.push(<LatLng[]>path);
}
}
public loadPreview(mp: MapPhoto) {
mp.preview.thumbnail.load();
mp.preview.thumbnail.CurrentlyWaiting = true;
@ -211,15 +195,6 @@ export class GalleryMapLightboxComponent implements OnChanges, AfterViewInit {
this.mapPhotos = [];
}
private getScreenWidth() {
return window.innerWidth;
}
private getScreenHeight() {
return window.innerHeight;
}
@HostListener('window:keydown', ['$event'])
onKeyPress(e: KeyboardEvent) {
if (this.visible !== true) {
@ -241,10 +216,40 @@ export class GalleryMapLightboxComponent implements OnChanges, AfterViewInit {
}
}
private centerMap() {
if (this.mapPhotos.length > 0) {
this.yagaMap.fitBounds(<any>this.mapPhotos);
}
}
private async loadGPXFiles(): Promise<void> {
this.paths = [];
for (let i = 0; i < this.gpxFiles.length; i++) {
const file = this.gpxFiles[i];
const path = await this.mapService.getMapPath(file);
if (file !== this.gpxFiles[i]) { // check race condition
return;
}
if (path.length === 0) {
continue;
}
this.paths.push(<LatLng[]>path);
}
}
private getScreenWidth() {
return window.innerWidth;
}
private getScreenHeight() {
return window.innerHeight;
}
}
export interface MapPhoto {
name: string;
lat: number;
lng: number;
iconUrl?: string;

View File

@ -4,30 +4,27 @@ import {FileDTO} from '../../../../../common/entities/FileDTO';
import {Utils} from '../../../../../common/Utils';
import {Config} from '../../../../../common/config/public/Config';
import {ClientConfig} from '../../../../../common/config/public/ConfigClass';
import MapLayers = ClientConfig.MapLayers;
@Injectable()
export class MapService {
private static readonly OSMLAYERS = [{name: 'street', url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'}];
private static MAPBOXLAYERS: MapLayers[] = [];
constructor(private networkService: NetworkService) {
}
public async getMapPath(file: FileDTO): Promise<MapPath[]> {
const filePath = Utils.concatUrls(file.directory.path, file.directory.name, file.name);
const gpx = await this.networkService.getXML('/gallery/content/' + filePath);
const elements = gpx.getElementsByTagName('trkpt');
const points: MapPath[] = [];
for (let i = 0; i < elements.length; i++) {
points.push({
lat: parseFloat(elements[i].getAttribute('lat')),
lng: parseFloat(elements[i].getAttribute('lon'))
});
MapService.MAPBOXLAYERS = [{
name: 'street', url: 'https://api.tiles.mapbox.com/v4/mapbox.streets/{z}/{x}/{y}.png?access_token='
+ Config.Client.Map.mapboxAccessToken
}, {
name: 'satellite', url: 'https://api.tiles.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.png?access_token='
+ Config.Client.Map.mapboxAccessToken
}, {
name: 'hybrid', url: 'https://api.tiles.mapbox.com/v4/mapbox.streets-satellite/{z}/{x}/{y}.png?access_token='
+ Config.Client.Map.mapboxAccessToken
}
return points;
];
}
public get ShortAttributions(): string[] {
const yaga = '<a href="https://yagajs.org" title="YAGA">YAGA</a>';
const lf = '<a href="https://leaflet-ng2.yagajs.org" title="Leaflet in Angular2">leaflet-ng2</a>';
@ -62,14 +59,33 @@ export class MapService {
}
public get MapLayer(): string {
if (Config.Client.Map.mapProvider === ClientConfig.MapProviders.Custom) {
return Config.Client.Map.tileUrl;
return this.Layers[0].url;
}
public get Layers(): { name: string, url: string }[] {
switch (Config.Client.Map.mapProvider) {
case ClientConfig.MapProviders.Custom:
return Config.Client.Map.customLayers;
case ClientConfig.MapProviders.Mapbox:
return MapService.MAPBOXLAYERS;
case ClientConfig.MapProviders.OpenStreetMap:
return MapService.OSMLAYERS;
}
if (Config.Client.Map.mapProvider === ClientConfig.MapProviders.Mapbox) {
return 'https://api.tiles.mapbox.com/v4/mapbox.streets/{z}/{x}/{y}.png?access_token='
+ Config.Client.Map.mapboxAccessToken;
}
public async getMapPath(file: FileDTO): Promise<MapPath[]> {
const filePath = Utils.concatUrls(file.directory.path, file.directory.name, file.name);
const gpx = await this.networkService.getXML('/gallery/content/' + filePath);
const elements = gpx.getElementsByTagName('trkpt');
const points: MapPath[] = [];
for (let i = 0; i < elements.length; i++) {
points.push({
lat: parseFloat(elements[i].getAttribute('lat')),
lng: parseFloat(elements[i].getAttribute('lon'))
});
}
return 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
return points;
}
}

View File

@ -67,6 +67,9 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
if (!newSettings) {
return false;
}
if (Array.isArray(original) && original.length !== newSettings.length) {
return false;
}
const keys = Object.keys(newSettings);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];

View File

@ -0,0 +1,9 @@
.custom-layer-info {
margin-top: -2rem;
margin-bottom: 1rem;
}
.custom-layer-container {
margin-bottom: 1rem;
}

View File

@ -7,12 +7,12 @@
class="switch"
name="enabled"
[switch-on-color]="'success'"
[switch-inverse]="'inverse'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-disabled]="inProgress"
[switch-handle-width]="'100'"
[switch-label-width]="'20'"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.enabled">
</bSwitch>
</div>
@ -33,15 +33,43 @@
</div>
</div>
<div class="form-group row" *ngIf="settings.mapProvider === MapProviders.Custom">
<label class="col-md-2 control-label" for="tileUrl" i18n>Map tile url</label>
<div class="col-md-10">
<input type="text" class="form-control" placeholder="http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
[(ngModel)]="settings.tileUrl"
name="tileUrl" id="tileUrl" required>
<small class="form-text text-muted">
<ng-container i18n>The map module will use this url to fetch the map tiles.</ng-container>
</small>
<div class="container custom-layer-container" *ngIf="settings.mapProvider === MapProviders.Custom">
<table class="table table-hover">
<thead>
<tr>
<th i18n>Name</th>
<th i18n>Tile Url*</th>
<th></th>
</tr>
</thead>
<tr *ngFor="let layer of settings.customLayers; let i = index">
<td><input type="text" class="form-control" placeholder="Street"
[(ngModel)]="layer.name"
[name]="'tileName-'+i" [id]="'tileName-'+i" required></td>
<td>
<input type="text" class="form-control" placeholder="http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
[(ngModel)]="layer.url"
[name]="'tileUrl-'+i" [id]="'tileUrl-'+i" required>
</td>
<td>
<button [disabled]="settings.customLayers.length == 1" (click)="removeLayer(layer)"
[ngClass]="settings.customLayers.length > 1? 'btn-danger':'btn-secondary'"
class="btn float-right">
<span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span>
</button>
</td>
</tr>
</table>
<div class="row justify-content-end">
<small class="form-text text-muted custom-layer-info">
<ng-container i18n>*The map module will use these urls to fetch the map tiles.</ng-container>
</small>
</div>
<div class="row justify-content-end">
<button class="btn btn-primary"
(click)="addNewLayer()" i18n>+ Add Layer
</button>
</div>
</div>
@ -52,7 +80,8 @@
[(ngModel)]="settings.mapboxAccessToken"
name="mapboxAccessToken" id="mapboxAccessToken" required>
<small class="form-text text-muted">
<ng-container i18n>MapBox needs an access token to work, create one at </ng-container><a href="https://www.mapbox.com">https://www.mapbox.com</a>.
<ng-container i18n>MapBox needs an access token to work, create one at</ng-container>
&nbsp;<a href="https://www.mapbox.com">https://www.mapbox.com</a>.
</small>
</div>
</div>

View File

@ -32,6 +32,16 @@ export class MapSettingsComponent extends SettingsComponent<ClientConfig.MapConf
}
addNewLayer() {
this.settings.customLayers.push({
name: 'Layer-' + this.settings.customLayers.length,
url: ''
});
}
removeLayer(layer: ClientConfig.MapLayers) {
this.settings.customLayers.splice(this.settings.customLayers.indexOf(layer), 1);
}
}

View File

@ -46,7 +46,7 @@ export class SettingsService {
enabled: true,
mapProvider: ClientConfig.MapProviders.OpenStreetMap,
mapboxAccessToken: '',
tileUrl: ''
customLayers: [{name: 'street', url: ''}]
},
RandomPhoto: {
enabled: true

View File

@ -6,11 +6,11 @@
class="switch"
name="enabled"
[switch-on-color]="'success'"
[switch-inverse]="'inverse'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-handle-width]="'100'"
[switch-label-width]="'20'"
[switch-handle-width]="100"
[switch-label-width]="20"
[switch-disabled]="inProgress"
[(ngModel)]="enabled"
(changeState)="switched($event)">

View File

@ -1,11 +1,16 @@
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {enableProdMode} from '@angular/core';
import {ApplicationRef, enableProdMode} from '@angular/core';
import {environment} from './environments/environment';
import {AppModule} from './app/app.module';
import {enableDebugTools} from '@angular/platform-browser';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule).catch(err => console.error(err));
}
platformBrowserDynamic().bootstrapModule(AppModule).then(moduleRef => {
const applicationRef = moduleRef.injector.get(ApplicationRef);
const componentRef = applicationRef.components[0];
// allows to run `ng.profiler.timeChangeDetection();`
enableDebugTools(componentRef);
}).catch(err => console.error(err));

View File

@ -1,15 +0,0 @@
import {TestProjectPage} from './app.po';
describe('test-project App', () => {
let page: TestProjectPage;
beforeEach(() => {
page = new TestProjectPage();
});
it('should display welcome message', async (done) => {
page.navigateTo();
expect(await page.getParagraphText()).toEqual('Welcome to app!!');
done();
});
});

View File

@ -1,11 +0,0 @@
import {browser, by, element} from 'protractor';
export class TestProjectPage {
navigateTo() {
return browser.get('/');
}
getParagraphText() {
return element(by.css('app-root h1')).getText();
}
}

View File

@ -1,12 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"node"
]
}
}