mirror of
https://github.com/bpatrik/pigallery2.git
synced 2025-01-10 04:07:35 +02:00
Implementing smarter map layers for paths. It can now recognize sport and transportation #647
This commit is contained in:
parent
1766cf2062
commit
61970c2025
@ -100,6 +100,10 @@ export class Utils {
|
||||
return size.toFixed(2) + postFixes[index];
|
||||
}
|
||||
|
||||
static getUnique(arr: any[]) {
|
||||
return arr.filter((value, index, arr) => arr.indexOf(value) === index);
|
||||
}
|
||||
|
||||
static createRange(from: number, to: number): Array<number> {
|
||||
const arr = new Array(to - from + 1);
|
||||
let c = to - from + 1;
|
||||
@ -129,7 +133,7 @@ export class Utils {
|
||||
|
||||
url += part + '/';
|
||||
}
|
||||
url = url.replace(/(https?:\/\/)|(\/){2,}/g, "$1$2")
|
||||
url = url.replace(/(https?:\/\/)|(\/){2,}/g, '$1$2');
|
||||
|
||||
if (url.trim() === '') {
|
||||
url = './';
|
||||
|
@ -107,6 +107,7 @@ import {SharingsListComponent} from './ui/settings/sharings-list/sharings-list.c
|
||||
import {ThemeService} from './model/theme.service';
|
||||
import {StringifyConfigPriority} from './pipes/StringifyConfigPriority';
|
||||
import {StringifySearchType} from './pipes/StringifySearchType';
|
||||
import {MarkerFactory} from './ui/gallery/map/MarkerFactory';
|
||||
|
||||
@Injectable()
|
||||
export class MyHammerConfig extends HammerGestureConfig {
|
||||
@ -137,22 +138,8 @@ export class CustomUrlSerializer implements UrlSerializer {
|
||||
}
|
||||
}
|
||||
|
||||
// Fixes Leaflet icon path issue:
|
||||
// https://stackoverflow.com/questions/41144319/leaflet-marker-not-found-production-env
|
||||
const iconRetinaUrl = 'assets/marker-icon-2x.png';
|
||||
const iconUrl = 'assets/marker-icon.png';
|
||||
const shadowUrl = 'assets/marker-shadow.png';
|
||||
const iconDefault = icon({
|
||||
iconRetinaUrl,
|
||||
iconUrl,
|
||||
shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
tooltipAnchor: [16, -28],
|
||||
shadowSize: [41, 41],
|
||||
});
|
||||
Marker.prototype.options.icon = iconDefault;
|
||||
|
||||
Marker.prototype.options.icon = MarkerFactory.defIcon;
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
37
src/frontend/app/ui/gallery/map/MarkerFactory.ts
Normal file
37
src/frontend/app/ui/gallery/map/MarkerFactory.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import {DivIcon, setOptions} from 'leaflet';
|
||||
|
||||
export interface SvgIconOptions {
|
||||
color?: string;
|
||||
svgPath?: string;
|
||||
width?: number;
|
||||
small?: boolean;
|
||||
}
|
||||
|
||||
const SvgIcon: { new(options?: SvgIconOptions): DivIcon } = DivIcon.extend({
|
||||
initialize: function(options: SvgIconOptions = {}) {
|
||||
options.color = options.color || 'var(--bs-primary)';
|
||||
options.svgPath = options.svgPath || 'M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512z';
|
||||
options.width = options.width || 512;
|
||||
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + options.width + ' 512"><path fill="' + options.color + '" d="' + options.svgPath + '"/></svg>';
|
||||
setOptions(this, {
|
||||
iconSize: options.small ? [15, 15] : [30, 30],
|
||||
iconAnchor: options.small ? [15, 28] : [15, 35],
|
||||
popupAnchor: options.small ? [0, -15] : [0, -30],
|
||||
className: 'custom-div-icon' + (options.small ? ' marker-svg-small' : ''),
|
||||
html: '<div class="marker-svg-wrapper"><div class="marker-svg-shadow"></div>' +
|
||||
'<div class="marker-svg-pin" style="border-color: ' + options.color + '">' +
|
||||
'</div>' + svg + '</div>',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export class MarkerFactory {
|
||||
public static readonly defIcon = MarkerFactory.getSvgIcon();
|
||||
public static readonly defIconSmall = MarkerFactory.getSvgIcon({small: true});
|
||||
|
||||
|
||||
static getSvgIcon(options?: SvgIconOptions) {
|
||||
return new SvgIcon(options);
|
||||
}
|
||||
|
||||
}
|
@ -73,6 +73,48 @@
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
::ng-deep .marker-svg-small .marker-svg-wrapper{
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
|
||||
::ng-deep .marker-svg-pin {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin: -15px 0 0 -15px;
|
||||
border-radius: 50% 50% 50% 0;
|
||||
position: absolute;
|
||||
border: 3px solid;
|
||||
transform: rotate(-45deg);
|
||||
background-color: rgba(248, 249, 250, 0.9);
|
||||
}
|
||||
|
||||
::ng-deep .marker-svg-shadow {
|
||||
content: "";
|
||||
background: rgba(128, 128, 128, 0.4);
|
||||
width: 20px;
|
||||
height: 10px;
|
||||
border-radius: 100%;
|
||||
position: absolute;
|
||||
top: 31px;
|
||||
left: 5px
|
||||
}
|
||||
|
||||
|
||||
::ng-deep .custom-div-icon svg {
|
||||
position: absolute;
|
||||
width: 16px !important;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
::ng-deep .lightbox-map-gallery-component-preview-loading {
|
||||
background-color: #bbbbbb;
|
||||
color: #7f7f7f;
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
control,
|
||||
Control,
|
||||
divIcon,
|
||||
DivIcon,
|
||||
icon,
|
||||
latLng,
|
||||
latLngBounds,
|
||||
@ -33,6 +34,8 @@ import {
|
||||
import {LeafletControlLayersConfig} from '@asymmetrik/ngx-leaflet';
|
||||
import {ThemeService} from '../../../../model/theme.service';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {MarkerFactory} from '../MarkerFactory';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-gallery-map-lightbox',
|
||||
@ -76,7 +79,10 @@ export class GalleryMapLightboxComponent implements OnChanges, OnDestroy {
|
||||
);
|
||||
private usedIconSize = this.iconSize;
|
||||
private mapLayersControlOption: LeafletControlLayersConfig & {
|
||||
overlays: { Photos: MarkerClusterGroup; Paths: LayerGroup };
|
||||
overlays: {
|
||||
Photos: MarkerClusterGroup;
|
||||
[name: string]: LayerGroup;
|
||||
};
|
||||
} = {
|
||||
baseLayers: {},
|
||||
overlays: {
|
||||
@ -104,10 +110,37 @@ export class GalleryMapLightboxComponent implements OnChanges, OnDestroy {
|
||||
});
|
||||
},
|
||||
}),
|
||||
Paths: layerGroup([]),
|
||||
},
|
||||
};
|
||||
private mapLayerControl: Control.Layers;
|
||||
// ordered list
|
||||
private pathLayersConfigOrdered = [
|
||||
{
|
||||
name: $localize`Transportation`,
|
||||
matchers: [/flight/gi, /flying/gi, /drive/gi, /driving/gi] as RegExp[],
|
||||
layer: layerGroup([]),
|
||||
theme: {color: 'var(--bs-orange)', dashArray: '4 8'},
|
||||
icon: null as DivIcon,
|
||||
svgIcon: {
|
||||
width: 567,
|
||||
path: 'M482.3 192c34.2 0 93.7 29 93.7 64c0 36-59.5 64-93.7 64l-116.6 0L265.2 495.9c-5.7 10-16.3 16.1-27.8 16.1l-56.2 0c-10.6 0-18.3-10.2-15.4-20.4l49-171.6L112 320 68.8 377.6c-3 4-7.8 6.4-12.8 6.4l-42 0c-7.8 0-14-6.3-14-14c0-1.3 .2-2.6 .5-3.9L32 256 .5 145.9c-.4-1.3-.5-2.6-.5-3.9c0-7.8 6.3-14 14-14l42 0c5 0 9.8 2.4 12.8 6.4L112 192l102.9 0-49-171.6C162.9 10.2 170.6 0 181.2 0l56.2 0c11.5 0 22.1 6.2 27.8 16.1L365.7 192l116.6 0z'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: $localize`Sport`,
|
||||
matchers: [/run/gi, /walk/gi, /hike/gi, /hiking/gi, /bike/gi, /biking/gi, /cycling/gi] as RegExp[],
|
||||
layer: layerGroup([]),
|
||||
theme: {color: 'var(--bs-primary)'},
|
||||
svgIcon: {
|
||||
width: 417,
|
||||
path: 'M320 48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM125.7 175.5c9.9-9.9 23.4-15.5 37.5-15.5c1.9 0 3.8 .1 5.6 .3L137.6 254c-9.3 28 1.7 58.8 26.8 74.5l86.2 53.9-25.4 88.8c-4.9 17 5 34.7 22 39.6s34.7-5 39.6-22l28.7-100.4c5.9-20.6-2.6-42.6-20.7-53.9L238 299l30.9-82.4 5.1 12.3C289 264.7 323.9 288 362.7 288H384c17.7 0 32-14.3 32-32s-14.3-32-32-32H362.7c-12.9 0-24.6-7.8-29.5-19.7l-6.3-15c-14.6-35.1-44.1-61.9-80.5-73.1l-48.7-15c-11.1-3.4-22.7-5.2-34.4-5.2c-31 0-60.8 12.3-82.7 34.3L57.4 153.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l23.1-23.1zM91.2 352H32c-17.7 0-32 14.3-32 32s14.3 32 32 32h69.6c19 0 36.2-11.2 43.9-28.5L157 361.6l-9.5-6c-17.5-10.9-30.5-26.8-37.9-44.9L91.2 352z'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: $localize`Other paths`,
|
||||
matchers: null as RegExp[], layer: layerGroup([]), theme: {color: 'var(--bs-secondary)'}
|
||||
}
|
||||
];
|
||||
mapLayerControl: Control.Layers;
|
||||
private thumbnailsOnLoad: ThumbnailBase[] = [];
|
||||
private startPosition: Dimension = null;
|
||||
private leafletMap: Map;
|
||||
@ -119,10 +152,9 @@ export class GalleryMapLightboxComponent implements OnChanges, OnDestroy {
|
||||
public mapService: MapService,
|
||||
private themeService: ThemeService
|
||||
) {
|
||||
this.mapOptions.layers = [
|
||||
this.mapLayersControlOption.overlays.Photos,
|
||||
this.mapLayersControlOption.overlays.Paths,
|
||||
];
|
||||
this.setUpPathLayers();
|
||||
this.mapOptions.layers = [this.mapLayersControlOption.overlays.Photos];
|
||||
this.pathLayersConfigOrdered.forEach(pl => this.mapOptions.layers.push(pl.layer));
|
||||
for (let i = 0; i < mapService.Layers.length; ++i) {
|
||||
const l = mapService.Layers[i];
|
||||
const tl = tileLayer(l.url, {attribution: mapService.Attributions});
|
||||
@ -149,6 +181,13 @@ export class GalleryMapLightboxComponent implements OnChanges, OnDestroy {
|
||||
this.darkModeSubscription = this.themeService.darkMode.subscribe(this.selectBaseLayer);
|
||||
}
|
||||
|
||||
setUpPathLayers() {
|
||||
this.pathLayersConfigOrdered.forEach(pl => {
|
||||
pl.icon = MarkerFactory.getSvgIcon({color: pl.theme.color, svgPath: pl.svgIcon?.path, width: pl.svgIcon?.width});
|
||||
this.mapLayersControlOption.overlays[pl.name] = pl.layer;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.darkModeSubscription.unsubscribe();
|
||||
}
|
||||
@ -324,6 +363,7 @@ export class GalleryMapLightboxComponent implements OnChanges, OnDestroy {
|
||||
photoTh.OnLoad = setPopUpPhoto;
|
||||
}
|
||||
|
||||
mkr.setIcon(MarkerFactory.defIcon);
|
||||
// Setting photo icon
|
||||
if (Config.Map.useImageMarkers === true) {
|
||||
mkr.on('add', () => {
|
||||
@ -446,31 +486,19 @@ export class GalleryMapLightboxComponent implements OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
private async loadGPXFiles(): Promise<void> {
|
||||
this.mapLayersControlOption.overlays.Paths.clearLayers();
|
||||
this.pathLayersConfigOrdered.forEach(p => p.layer.clearLayers());
|
||||
if (this.gpxFiles.length === 0) {
|
||||
// remove from controls
|
||||
this.mapLayerControl.removeLayer(
|
||||
this.mapLayersControlOption.overlays.Paths
|
||||
);
|
||||
// remove from map
|
||||
if (this.leafletMap) {
|
||||
this.leafletMap.removeLayer(this.mapLayersControlOption.overlays.Paths);
|
||||
}
|
||||
} else {
|
||||
// make sure it does not appear twice
|
||||
this.mapLayerControl.removeLayer(
|
||||
this.mapLayersControlOption.overlays.Paths
|
||||
);
|
||||
this.mapLayerControl.addOverlay(
|
||||
this.mapLayersControlOption.overlays.Paths,
|
||||
'Paths'
|
||||
);
|
||||
if (
|
||||
this.leafletMap &&
|
||||
!this.leafletMap.hasLayer(this.mapLayersControlOption.overlays.Paths)
|
||||
) {
|
||||
this.leafletMap.addLayer(this.mapLayersControlOption.overlays.Paths);
|
||||
}
|
||||
|
||||
this.pathLayersConfigOrdered.forEach(p => {
|
||||
// remove from controls
|
||||
this.mapLayerControl.removeLayer(p.layer);
|
||||
// remove from map
|
||||
if (this.leafletMap) {
|
||||
this.leafletMap.removeLayer(p.layer);
|
||||
}
|
||||
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
||||
@ -481,19 +509,57 @@ export class GalleryMapLightboxComponent implements OnChanges, OnDestroy {
|
||||
// check race condition
|
||||
return;
|
||||
}
|
||||
const pathLayer = this.pathLayersConfigOrdered.find((pl) => {
|
||||
return pl.matchers === null || // null matchers match everything
|
||||
(parsedGPX.name &&
|
||||
pl.matchers.findIndex(m => m.test(parsedGPX.name)) !== -1);
|
||||
}) || this.pathLayersConfigOrdered[0];
|
||||
|
||||
if (parsedGPX.path.length !== 0) {
|
||||
// render the beginning of the path with a marker
|
||||
this.mapLayersControlOption.overlays.Paths.addLayer(
|
||||
marker(parsedGPX.path[0])
|
||||
);
|
||||
this.mapLayersControlOption.overlays.Paths.addLayer(
|
||||
polyline(parsedGPX.path, {smoothFactor: 3})
|
||||
const mkr = marker(parsedGPX.path[0]);
|
||||
pathLayer.layer.addLayer(mkr);
|
||||
|
||||
mkr.setIcon(pathLayer.icon);
|
||||
|
||||
// Setting popup photo
|
||||
mkr.bindPopup(file.name + ': ' + parsedGPX.name);
|
||||
|
||||
pathLayer.layer.addLayer(
|
||||
polyline(parsedGPX.path, {
|
||||
smoothFactor: 3,
|
||||
interactive: false,
|
||||
color: pathLayer?.theme?.color,
|
||||
dashArray: pathLayer?.theme?.dashArray
|
||||
})
|
||||
);
|
||||
}
|
||||
parsedGPX.markers.forEach((mc) => {
|
||||
this.mapLayersControlOption.overlays.Paths.addLayer(marker(mc));
|
||||
const mkr = marker(mc);
|
||||
mkr.setIcon(pathLayer.icon);
|
||||
pathLayer.layer.addLayer(mkr);
|
||||
mkr.bindPopup($localize`Latitude` + ': ' + mc.lat + ', ' + $localize`longitude` + ': ' + mc.lng);
|
||||
});
|
||||
}
|
||||
|
||||
// Add layer to the map
|
||||
this.pathLayersConfigOrdered.filter(pl => pl.layer.getLayers().length > 0).forEach((pl) => {
|
||||
// make sure it does not appear twice
|
||||
this.mapLayerControl.removeLayer(
|
||||
pl.layer
|
||||
);
|
||||
this.mapLayerControl.addOverlay(
|
||||
pl.layer,
|
||||
pl.name
|
||||
);
|
||||
if (
|
||||
this.leafletMap &&
|
||||
!this.leafletMap.hasLayer(pl.layer)
|
||||
) {
|
||||
this.leafletMap.addLayer(pl.layer);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import {Config} from '../../../../../common/config/public/Config';
|
||||
import {LatLngLiteral, Map, MapOptions, Marker, marker, tileLayer, TileLayer} from 'leaflet';
|
||||
import {ThemeService} from '../../../model/theme.service';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {MarkerFactory} from './MarkerFactory';
|
||||
|
||||
@Component({
|
||||
selector: 'app-gallery-map',
|
||||
@ -113,7 +114,7 @@ export class GalleryMapComponent implements OnChanges, IRenderable {
|
||||
return marker({
|
||||
lat: p.metadata.positionData.GPSData.latitude,
|
||||
lng: p.metadata.positionData.GPSData.longitude,
|
||||
} as LatLngLiteral);
|
||||
} as LatLngLiteral).setIcon(MarkerFactory.defIconSmall);
|
||||
});
|
||||
|
||||
if (this.leafletMap && this.markerLayer.length > 0) {
|
||||
|
@ -100,7 +100,7 @@ export class MapService {
|
||||
|
||||
public async getMapCoordinates(
|
||||
file: FileDTO
|
||||
): Promise<{ path: LatLngLiteral[]; markers: LatLngLiteral[] }> {
|
||||
): Promise<{ name: string, path: LatLngLiteral[]; markers: LatLngLiteral[] }> {
|
||||
const filePath = Utils.concatUrls(
|
||||
file.directory.path,
|
||||
file.directory.name,
|
||||
@ -121,8 +121,8 @@ export class MapService {
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
return {
|
||||
name: gpx.getElementsByTagName('name')?.[0]?.textContent || '',
|
||||
path: getCoordinates('trkpt'),
|
||||
markers: getCoordinates('wpt'),
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user