1
0
mirror of https://github.com/xuthus83/pigallery2.git synced 2025-01-14 14:43:17 +08:00

Implementing on-the-fly GPX compression.

Its a lossy compression that can be finetuned in the config.

#504
This commit is contained in:
Patrik J. Braun 2022-06-24 22:59:08 +02:00
parent b6b576ba2f
commit 3bff1a4383
7 changed files with 297 additions and 76 deletions

View File

@ -0,0 +1,40 @@
import {NextFunction, Request, Response} from 'express';
import * as fs from 'fs';
import { Config } from '../../common/config/private/Config';
import { GPXProcessing } from '../model/GPXProcessing';
export class MetaFileMWs {
public static async compressGPX(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
if (!req.resultPipe) {
return next();
}
// if conversion is not enabled redirect, so browser can cache the full
if (Config.Client.MetaFile.GPXCompressing.enabled === false) {
return res.redirect(req.originalUrl.slice(0, -1 * '\\bestFit'.length));
}
const fullPath = req.resultPipe as string;
const compressedGPX = GPXProcessing.generateConvertedPath(
fullPath,
);
// check if converted photo exist
if (fs.existsSync(compressedGPX) === true) {
req.resultPipe = compressedGPX;
return next();
}
if (Config.Server.MetaFile.GPXCompressing.onTheFly === true) {
req.resultPipe = await GPXProcessing.compressGPX(fullPath);
return next();
}
// not converted and won't be now
return res.redirect(req.originalUrl.slice(0, -1 * '\\bestFit'.length));
}
}

View File

@ -0,0 +1,127 @@
import * as path from 'path';
import {constants as fsConstants, promises as fsp} from 'fs';
import * as xml2js from 'xml2js';
import {ProjectPath} from '../ProjectPath';
import {Config} from '../../common/config/private/Config';
type gpxEntry = { '$': { lat: string, lon: string }, ele: string[], time: string[], extensions: unknown };
export class GPXProcessing {
public static generateConvertedPath(filePath: string): string {
const file = path.basename(filePath);
return path.join(
ProjectPath.TranscodedFolder,
ProjectPath.getRelativePathToImages(path.dirname(filePath)),
file
);
}
public static async isValidConvertedPath(
convertedPath: string
): Promise<boolean> {
const origFilePath = path.join(
ProjectPath.ImageFolder,
path.relative(
ProjectPath.TranscodedFolder,
convertedPath
)
);
try {
await fsp.access(origFilePath, fsConstants.R_OK);
} catch (e) {
return false;
}
return true;
}
static async compressedGPXExist(
filePath: string
): Promise<boolean> {
// compressed gpx path
const outPath = GPXProcessing.generateConvertedPath(filePath);
// check if file already exist
try {
await fsp.access(outPath, fsConstants.R_OK);
return true;
} catch (e) {
// ignoring errors
}
return false;
}
public static async compressGPX(
filePath: string,
): Promise<string> {
// generate compressed gpx path
const outPath = GPXProcessing.generateConvertedPath(filePath);
// check if file already exist
try {
await fsp.access(outPath, fsConstants.R_OK);
return outPath;
} catch (e) {
// ignoring errors
}
const outDir = path.dirname(outPath);
await fsp.mkdir(outDir, {recursive: true});
const gpxStr = await fsp.readFile(filePath);
const gpxObj = await (new xml2js.Parser()).parseStringPromise(gpxStr);
const items: gpxEntry[] = gpxObj.gpx.trk[0].trkseg[0].trkpt;
const distance = (entry1: gpxEntry, entry2: gpxEntry) => {
const lat1 = parseFloat(entry1.$.lat);
const lon1 = parseFloat(entry1.$.lon);
const lat2 = parseFloat(entry2.$.lat);
const lon2 = parseFloat(entry2.$.lon);
// credits to: https://www.movable-type.co.uk/scripts/latlong.html
const R = 6371e3; // metres
const φ1 = lat1 * Math.PI / 180; // φ, λ in radians
const φ2 = lat2 * Math.PI / 180;
const Δφ = (lat2 - lat1) * Math.PI / 180;
const Δλ = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const d = R * c; // in metres
return d;
};
const gpxEntryFilter = (value: gpxEntry, i: number, list: gpxEntry[]) => {
if (i === 0 || i >= list.length - 1) { // always keep the first and last items
return true;
}
const timeDelta = (Date.parse(list[i].time[0]) - Date.parse(list[i - 1].time[0])); // mill sec.
const dist = distance(list[i - 1], list[i]); // meters
return !(timeDelta < Config.Server.MetaFile.GPXCompressing.minTimeDistance &&
dist < Config.Server.MetaFile.GPXCompressing.minDistance);
};
gpxObj.gpx.trk[0].trkseg[0].trkpt = items.filter(gpxEntryFilter).map((v) => {
v.$.lon = parseFloat(v.$.lon).toFixed(6);
v.$.lat = parseFloat(v.$.lat).toFixed(6);
delete v.ele;
delete v.extensions;
return v;
});
await fsp.writeFile(outPath, (new xml2js.Builder({renderOpts: {pretty: false}})).buildObject(gpxObj));
return outPath;
}
}

View File

@ -9,6 +9,7 @@ import { VersionMWs } from '../middlewares/VersionMWs';
import { SupportedFormats } from '../../common/SupportedFormats'; import { SupportedFormats } from '../../common/SupportedFormats';
import { PhotoConverterMWs } from '../middlewares/thumbnail/PhotoConverterMWs'; import { PhotoConverterMWs } from '../middlewares/thumbnail/PhotoConverterMWs';
import { ServerTimingMWs } from '../middlewares/ServerTimingMWs'; import { ServerTimingMWs } from '../middlewares/ServerTimingMWs';
import {MetaFileMWs} from '../middlewares/MetaFileMWs';
export class GalleryRouter { export class GalleryRouter {
public static route(app: Express): void { public static route(app: Express): void {
@ -21,6 +22,7 @@ export class GalleryRouter {
this.addGetBestFitVideo(app); this.addGetBestFitVideo(app);
this.addGetVideo(app); this.addGetVideo(app);
this.addGetMetaFile(app); this.addGetMetaFile(app);
this.addGetBestFitMetaFile(app);
this.addRandom(app); this.addRandom(app);
this.addDirectoryList(app); this.addDirectoryList(app);
this.addDirectoryZip(app); this.addDirectoryZip(app);
@ -158,6 +160,26 @@ export class GalleryRouter {
); );
} }
protected static addGetBestFitMetaFile(app: Express): void {
app.get(
[
'/api/gallery/content/:mediaPath(*.(' +
SupportedFormats.MetaFiles.join('|') +
'))/bestFit',
],
// common part
AuthenticationMWs.authenticate,
AuthenticationMWs.normalizePathParam('mediaPath'),
AuthenticationMWs.authorisePath('mediaPath', false),
// specific part
GalleryMWs.loadFile,
MetaFileMWs.compressGPX,
ServerTimingMWs.addServerTiming,
RenderingMWs.renderFile
);
}
protected static addRandom(app: Express): void { protected static addRandom(app: Express): void {
app.get( app.get(
['/api/gallery/random/:searchQueryDTO'], ['/api/gallery/random/:searchQueryDTO'],

View File

@ -5,17 +5,17 @@ import {
JobTrigger, JobTrigger,
JobTriggerType, JobTriggerType,
} from '../../entities/job/JobScheduleDTO'; } from '../../entities/job/JobScheduleDTO';
import { ClientConfig } from '../public/ClientConfig'; import {ClientConfig, ClientMetaFileConfig} from '../public/ClientConfig';
import { SubConfigClass } from 'typeconfig/src/decorators/class/SubConfigClass'; import {SubConfigClass} from 'typeconfig/src/decorators/class/SubConfigClass';
import { ConfigProperty } from 'typeconfig/src/decorators/property/ConfigPropoerty'; import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty';
import { DefaultsJobs } from '../../entities/job/JobDTO'; import {DefaultsJobs} from '../../entities/job/JobDTO';
import { import {
SearchQueryDTO, SearchQueryDTO,
SearchQueryTypes, SearchQueryTypes,
TextSearch, TextSearch,
} from '../../entities/SearchQueryDTO'; } from '../../entities/SearchQueryDTO';
import { SortingMethods } from '../../entities/SortingMethods'; import {SortingMethods} from '../../entities/SortingMethods';
import { UserRoles } from '../../entities/UserDTO'; import {UserRoles} from '../../entities/UserDTO';
export enum DatabaseType { export enum DatabaseType {
memory = 1, memory = 1,
@ -71,15 +71,15 @@ export type videoFormatType = 'mp4' | 'webm';
@SubConfigClass() @SubConfigClass()
export class MySQLConfig { export class MySQLConfig {
@ConfigProperty({ envAlias: 'MYSQL_HOST' }) @ConfigProperty({envAlias: 'MYSQL_HOST'})
host: string = 'localhost'; host: string = 'localhost';
@ConfigProperty({ envAlias: 'MYSQL_PORT', min: 0, max: 65535 }) @ConfigProperty({envAlias: 'MYSQL_PORT', min: 0, max: 65535})
port: number = 3306; port: number = 3306;
@ConfigProperty({ envAlias: 'MYSQL_DATABASE' }) @ConfigProperty({envAlias: 'MYSQL_DATABASE'})
database: string = 'pigallery2'; database: string = 'pigallery2';
@ConfigProperty({ envAlias: 'MYSQL_USERNAME' }) @ConfigProperty({envAlias: 'MYSQL_USERNAME'})
username: string = ''; username: string = '';
@ConfigProperty({ envAlias: 'MYSQL_PASSWORD', type: 'password' }) @ConfigProperty({envAlias: 'MYSQL_PASSWORD', type: 'password'})
password: string = ''; password: string = '';
} }
@ -94,13 +94,13 @@ export class UserConfig {
@ConfigProperty() @ConfigProperty()
name: string; name: string;
@ConfigProperty({ type: UserRoles }) @ConfigProperty({type: UserRoles})
role: UserRoles; role: UserRoles;
@ConfigProperty({ description: 'Unencrypted, temporary password' }) @ConfigProperty({description: 'Unencrypted, temporary password'})
password: string; password: string;
@ConfigProperty({ description: 'Encrypted password' }) @ConfigProperty({description: 'Encrypted password'})
encryptedPassword: string | undefined; encryptedPassword: string | undefined;
constructor(name: string, password: string, role: UserRoles) { constructor(name: string, password: string, role: UserRoles) {
@ -142,12 +142,31 @@ export class ServerDataBaseConfig {
@SubConfigClass() @SubConfigClass()
export class ServerThumbnailConfig { export class ServerThumbnailConfig {
@ConfigProperty({ description: 'if true, photos will have better quality.' }) @ConfigProperty({description: 'if true, photos will have better quality.'})
qualityPriority: boolean = true; qualityPriority: boolean = true;
@ConfigProperty({ type: 'ratio' }) @ConfigProperty({type: 'ratio'})
personFaceMargin: number = 0.6; // in ration [0-1] personFaceMargin: number = 0.6; // in ration [0-1]
} }
@SubConfigClass()
export class ServerGPXCompressingConfig {
@ConfigProperty({
description: 'Compresses gpx files on-the-fly, when they are requested.',
})
onTheFly: boolean = true;
@ConfigProperty({type: 'unsignedInt', description: 'Filters out entry that are closer than this in meters.'})
minDistance: number = 5;
@ConfigProperty({type: 'unsignedInt', description: 'Filters out entry that are closer than this in time in milliseconds.'})
minTimeDistance: number = 5000;
}
@SubConfigClass()
export class ServerMetaFileConfig {
@ConfigProperty()
GPXCompressing: ServerGPXCompressingConfig = new ServerGPXCompressingConfig();
}
@SubConfigClass() @SubConfigClass()
export class ServerSharingConfig { export class ServerSharingConfig {
@ConfigProperty() @ConfigProperty()
@ -158,14 +177,14 @@ export class ServerSharingConfig {
export class ServerIndexingConfig { export class ServerIndexingConfig {
@ConfigProperty() @ConfigProperty()
cachedFolderTimeout: number = 1000 * 60 * 60; // Do not rescans the folder if seems ok cachedFolderTimeout: number = 1000 * 60 * 60; // Do not rescans the folder if seems ok
@ConfigProperty({ type: ReIndexingSensitivity }) @ConfigProperty({type: ReIndexingSensitivity})
reIndexingSensitivity: ReIndexingSensitivity = ReIndexingSensitivity.low; reIndexingSensitivity: ReIndexingSensitivity = ReIndexingSensitivity.low;
@ConfigProperty({ @ConfigProperty({
arrayType: 'string', arrayType: 'string',
description: description:
"If an entry starts with '/' it is treated as an absolute path." + 'If an entry starts with \'/\' it is treated as an absolute path.' +
" If it doesn't start with '/' but contains a '/', the path is relative to the image directory." + ' If it doesn\'t start with \'/\' but contains a \'/\', the path is relative to the image directory.' +
" If it doesn't contain a '/', any folder with this name will be excluded.", ' If it doesn\'t contain a \'/\', any folder with this name will be excluded.',
}) })
excludeFolderList: string[] = ['.Trash-1000', '.dtrash', '$RECYCLE.BIN']; excludeFolderList: string[] = ['.Trash-1000', '.dtrash', '$RECYCLE.BIN'];
@ConfigProperty({ @ConfigProperty({
@ -178,11 +197,11 @@ export class ServerIndexingConfig {
@SubConfigClass() @SubConfigClass()
export class ServerThreadingConfig { export class ServerThreadingConfig {
@ConfigProperty({ description: 'App can run on multiple thread' }) @ConfigProperty({description: 'App can run on multiple thread'})
enabled: boolean = true; enabled: boolean = true;
@ConfigProperty({ @ConfigProperty({
description: description:
"Number of threads that are used to generate thumbnails. If 0, number of 'CPU cores -1' threads will be used.", 'Number of threads that are used to generate thumbnails. If 0, number of \'CPU cores -1\' threads will be used.',
}) })
thumbnailThreads: number = 0; // if zero-> CPU count -1 thumbnailThreads: number = 0; // if zero-> CPU count -1
} }
@ -195,9 +214,9 @@ export class ServerDuplicatesConfig {
@SubConfigClass() @SubConfigClass()
export class ServerLogConfig { export class ServerLogConfig {
@ConfigProperty({ type: LogLevel }) @ConfigProperty({type: LogLevel})
level: LogLevel = LogLevel.info; level: LogLevel = LogLevel.info;
@ConfigProperty({ type: SQLLogLevel }) @ConfigProperty({type: SQLLogLevel})
sqlLevel: SQLLogLevel = SQLLogLevel.error; sqlLevel: SQLLogLevel = SQLLogLevel.error;
@ConfigProperty() @ConfigProperty()
logServerTiming: boolean = false; logServerTiming: boolean = false;
@ -205,32 +224,32 @@ export class ServerLogConfig {
@SubConfigClass() @SubConfigClass()
export class NeverJobTrigger implements JobTrigger { export class NeverJobTrigger implements JobTrigger {
@ConfigProperty({ type: JobTriggerType }) @ConfigProperty({type: JobTriggerType})
readonly type = JobTriggerType.never; readonly type = JobTriggerType.never;
} }
@SubConfigClass() @SubConfigClass()
export class ScheduledJobTrigger implements JobTrigger { export class ScheduledJobTrigger implements JobTrigger {
@ConfigProperty({ type: JobTriggerType }) @ConfigProperty({type: JobTriggerType})
readonly type = JobTriggerType.scheduled; readonly type = JobTriggerType.scheduled;
@ConfigProperty({ type: 'unsignedInt' }) @ConfigProperty({type: 'unsignedInt'})
time: number; // data time time: number; // data time
} }
@SubConfigClass() @SubConfigClass()
export class PeriodicJobTrigger implements JobTrigger { export class PeriodicJobTrigger implements JobTrigger {
@ConfigProperty({ type: JobTriggerType }) @ConfigProperty({type: JobTriggerType})
readonly type = JobTriggerType.periodic; readonly type = JobTriggerType.periodic;
@ConfigProperty({ type: 'unsignedInt', max: 7 }) @ConfigProperty({type: 'unsignedInt', max: 7})
periodicity: number | undefined; // 0-6: week days 7 every day periodicity: number | undefined; // 0-6: week days 7 every day
@ConfigProperty({ type: 'unsignedInt', max: 23 * 60 + 59 }) @ConfigProperty({type: 'unsignedInt', max: 23 * 60 + 59})
atTime: number | undefined; // day time atTime: number | undefined; // day time
} }
@SubConfigClass() @SubConfigClass()
export class AfterJobTrigger implements JobTrigger { export class AfterJobTrigger implements JobTrigger {
@ConfigProperty({ type: JobTriggerType }) @ConfigProperty({type: JobTriggerType})
readonly type = JobTriggerType.after; readonly type = JobTriggerType.after;
@ConfigProperty() @ConfigProperty()
afterScheduleName: string | undefined; // runs after schedule afterScheduleName: string | undefined; // runs after schedule
@ -294,16 +313,16 @@ export class JobScheduleConfig implements JobScheduleDTO {
@SubConfigClass() @SubConfigClass()
export class ServerJobConfig { export class ServerJobConfig {
@ConfigProperty({ type: 'integer', description: 'Job history size' }) @ConfigProperty({type: 'integer', description: 'Job history size'})
maxSavedProgress: number = 10; maxSavedProgress: number = 10;
@ConfigProperty({ arrayType: JobScheduleConfig }) @ConfigProperty({arrayType: JobScheduleConfig})
scheduled: JobScheduleConfig[] = [ scheduled: JobScheduleConfig[] = [
new JobScheduleConfig( new JobScheduleConfig(
DefaultsJobs[DefaultsJobs.Indexing], DefaultsJobs[DefaultsJobs.Indexing],
DefaultsJobs[DefaultsJobs.Indexing], DefaultsJobs[DefaultsJobs.Indexing],
false, false,
new NeverJobTrigger(), new NeverJobTrigger(),
{ indexChangesOnly: true } {indexChangesOnly: true}
), ),
new JobScheduleConfig( new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['Preview Filling']], DefaultsJobs[DefaultsJobs['Preview Filling']],
@ -317,39 +336,39 @@ export class ServerJobConfig {
DefaultsJobs[DefaultsJobs['Thumbnail Generation']], DefaultsJobs[DefaultsJobs['Thumbnail Generation']],
false, false,
new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Preview Filling']]), new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Preview Filling']]),
{ sizes: [240], indexedOnly: true } {sizes: [240], indexedOnly: true}
), ),
new JobScheduleConfig( new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['Photo Converting']], DefaultsJobs[DefaultsJobs['Photo Converting']],
DefaultsJobs[DefaultsJobs['Photo Converting']], DefaultsJobs[DefaultsJobs['Photo Converting']],
false, false,
new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Thumbnail Generation']]), new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Thumbnail Generation']]),
{ indexedOnly: true } {indexedOnly: true}
), ),
new JobScheduleConfig( new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['Video Converting']], DefaultsJobs[DefaultsJobs['Video Converting']],
DefaultsJobs[DefaultsJobs['Video Converting']], DefaultsJobs[DefaultsJobs['Video Converting']],
false, false,
new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Photo Converting']]), new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Photo Converting']]),
{ indexedOnly: true } {indexedOnly: true}
), ),
new JobScheduleConfig( new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']], DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']],
DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']], DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']],
false, false,
new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Video Converting']]), new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Video Converting']]),
{ indexedOnly: true } {indexedOnly: true}
), ),
]; ];
} }
@SubConfigClass() @SubConfigClass()
export class VideoTranscodingConfig { export class VideoTranscodingConfig {
@ConfigProperty({ type: 'unsignedInt' }) @ConfigProperty({type: 'unsignedInt'})
bitRate: number = 5 * 1024 * 1024; bitRate: number = 5 * 1024 * 1024;
@ConfigProperty({ type: 'unsignedInt' }) @ConfigProperty({type: 'unsignedInt'})
resolution: videoResolutionType = 720; resolution: videoResolutionType = 720;
@ConfigProperty({ type: 'positiveFloat' }) @ConfigProperty({type: 'positiveFloat'})
fps: number = 25; fps: number = 25;
@ConfigProperty() @ConfigProperty()
codec: videoCodecType = 'libx264'; codec: videoCodecType = 'libx264';
@ -388,7 +407,7 @@ export class PhotoConvertingConfig {
description: 'Converts photos on the fly, when they are requested.', description: 'Converts photos on the fly, when they are requested.',
}) })
onTheFly: boolean = true; onTheFly: boolean = true;
@ConfigProperty({ type: 'unsignedInt' }) @ConfigProperty({type: 'unsignedInt'})
resolution: videoResolutionType = 1080; resolution: videoResolutionType = 1080;
} }
@ -400,12 +419,12 @@ export class ServerPhotoConfig {
@SubConfigClass() @SubConfigClass()
export class ServerPreviewConfig { export class ServerPreviewConfig {
@ConfigProperty({ type: 'object' }) @ConfigProperty({type: 'object'})
SearchQuery: SearchQueryDTO = { SearchQuery: SearchQueryDTO = {
type: SearchQueryTypes.any_text, type: SearchQueryTypes.any_text,
text: '', text: '',
} as TextSearch; } as TextSearch;
@ConfigProperty({ arrayType: SortingMethods }) @ConfigProperty({arrayType: SortingMethods})
Sorting: SortingMethods[] = [ Sorting: SortingMethods[] = [
SortingMethods.descRating, SortingMethods.descRating,
SortingMethods.descDate, SortingMethods.descDate,
@ -434,25 +453,25 @@ export class ServerMediaConfig {
@SubConfigClass() @SubConfigClass()
export class ServerEnvironmentConfig { export class ServerEnvironmentConfig {
@ConfigProperty({ volatile: true }) @ConfigProperty({volatile: true})
upTime: string | undefined; upTime: string | undefined;
@ConfigProperty({ volatile: true }) @ConfigProperty({volatile: true})
appVersion: string | undefined; appVersion: string | undefined;
@ConfigProperty({ volatile: true }) @ConfigProperty({volatile: true})
buildTime: string | undefined; buildTime: string | undefined;
@ConfigProperty({ volatile: true }) @ConfigProperty({volatile: true})
buildCommitHash: string | undefined; buildCommitHash: string | undefined;
@ConfigProperty({ volatile: true }) @ConfigProperty({volatile: true})
isDocker: boolean | undefined; isDocker: boolean | undefined;
} }
@SubConfigClass() @SubConfigClass()
export class ServerConfig { export class ServerConfig {
@ConfigProperty({ volatile: true }) @ConfigProperty({volatile: true})
Environment: ServerEnvironmentConfig = new ServerEnvironmentConfig(); Environment: ServerEnvironmentConfig = new ServerEnvironmentConfig();
@ConfigProperty({ arrayType: 'string' }) @ConfigProperty({arrayType: 'string'})
sessionSecret: string[] = []; sessionSecret: string[] = [];
@ConfigProperty({ type: 'unsignedInt', envAlias: 'PORT', min: 0, max: 65535 }) @ConfigProperty({type: 'unsignedInt', envAlias: 'PORT', min: 0, max: 65535})
port: number = 80; port: number = 80;
@ConfigProperty() @ConfigProperty()
host: string = '0.0.0.0'; host: string = '0.0.0.0';
@ -466,7 +485,7 @@ export class ServerConfig {
Database: ServerDataBaseConfig = new ServerDataBaseConfig(); Database: ServerDataBaseConfig = new ServerDataBaseConfig();
@ConfigProperty() @ConfigProperty()
Sharing: ServerSharingConfig = new ServerSharingConfig(); Sharing: ServerSharingConfig = new ServerSharingConfig();
@ConfigProperty({ type: 'unsignedInt', description: 'unit: ms' }) @ConfigProperty({type: 'unsignedInt', description: 'unit: ms'})
sessionTimeout: number = 1000 * 60 * 60 * 24 * 7; // in ms sessionTimeout: number = 1000 * 60 * 60 * 24 * 7; // in ms
@ConfigProperty() @ConfigProperty()
Indexing: ServerIndexingConfig = new ServerIndexingConfig(); Indexing: ServerIndexingConfig = new ServerIndexingConfig();
@ -482,6 +501,8 @@ export class ServerConfig {
Log: ServerLogConfig = new ServerLogConfig(); Log: ServerLogConfig = new ServerLogConfig();
@ConfigProperty() @ConfigProperty()
Jobs: ServerJobConfig = new ServerJobConfig(); Jobs: ServerJobConfig = new ServerJobConfig();
@ConfigProperty()
MetaFile: ServerMetaFileConfig = new ServerMetaFileConfig();
} }
export interface IPrivateConfig { export interface IPrivateConfig {

View File

@ -182,6 +182,12 @@ export class ClientPhotoConfig {
loadFullImageOnZoom: boolean = true; loadFullImageOnZoom: boolean = true;
} }
@SubConfigClass()
export class ClientGPXCompressingConfig {
@ConfigProperty()
enabled: boolean = true;
}
@SubConfigClass() @SubConfigClass()
export class ClientMediaConfig { export class ClientMediaConfig {
@ConfigProperty() @ConfigProperty()
@ -198,6 +204,11 @@ export class ClientMetaFileConfig {
description: 'Reads *.gpx files and renders them on the map.', description: 'Reads *.gpx files and renders them on the map.',
}) })
gpx: boolean = true; gpx: boolean = true;
@ConfigProperty({
description: 'Reads *.gpx files and renders them on the map.',
})
@ConfigProperty()
GPXCompressing: ClientGPXCompressingConfig = new ClientGPXCompressingConfig();
@ConfigProperty({ @ConfigProperty({
description: description:
'Reads *.md files in a directory and shows the next to the map.', 'Reads *.md files in a directory and shows the next to the map.',

View File

@ -6,22 +6,22 @@ import {
OnChanges, OnChanges,
ViewChild, ViewChild,
} from '@angular/core'; } from '@angular/core';
import { PhotoDTO } from '../../../../../../common/entities/PhotoDTO'; import {PhotoDTO} from '../../../../../../common/entities/PhotoDTO';
import { Dimension } from '../../../../model/IRenderable'; import {Dimension} from '../../../../model/IRenderable';
import { FullScreenService } from '../../fullscreen.service'; import {FullScreenService} from '../../fullscreen.service';
import { import {
IconThumbnail, IconThumbnail,
Thumbnail, Thumbnail,
ThumbnailBase, ThumbnailBase,
ThumbnailManagerService, ThumbnailManagerService,
} from '../../thumbnailManager.service'; } from '../../thumbnailManager.service';
import { MediaIcon } from '../../MediaIcon'; import {MediaIcon} from '../../MediaIcon';
import { Media } from '../../Media'; import {Media} from '../../Media';
import { PageHelper } from '../../../../model/page.helper'; import {PageHelper} from '../../../../model/page.helper';
import { FileDTO } from '../../../../../../common/entities/FileDTO'; import {FileDTO} from '../../../../../../common/entities/FileDTO';
import { Utils } from '../../../../../../common/Utils'; import {Utils} from '../../../../../../common/Utils';
import { Config } from '../../../../../../common/config/public/Config'; import {Config} from '../../../../../../common/config/public/Config';
import { MapService } from '../map.service'; import {MapService} from '../map.service';
import { import {
control, control,
Control, Control,
@ -41,7 +41,7 @@ import {
polyline, polyline,
tileLayer, tileLayer,
} from 'leaflet'; } from 'leaflet';
import { LeafletControlLayersConfig } from '@asymmetrik/ngx-leaflet'; import {LeafletControlLayersConfig} from '@asymmetrik/ngx-leaflet';
@Component({ @Component({
selector: 'app-gallery-map-lightbox', selector: 'app-gallery-map-lightbox',
@ -66,7 +66,7 @@ export class GalleryMapLightboxComponent implements OnChanges {
public visible = false; public visible = false;
public controllersVisible = false; public controllersVisible = false;
public opacity = 1.0; public opacity = 1.0;
@ViewChild('root', { static: true }) elementRef: ElementRef; @ViewChild('root', {static: true}) elementRef: ElementRef;
public mapOptions: MapOptions = { public mapOptions: MapOptions = {
zoom: 2, zoom: 2,
// setting max zoom is needed to MarkerCluster https://github.com/Leaflet/Leaflet.markercluster/issues/611 // setting max zoom is needed to MarkerCluster https://github.com/Leaflet/Leaflet.markercluster/issues/611
@ -130,7 +130,7 @@ export class GalleryMapLightboxComponent implements OnChanges {
]; ];
for (let i = 0; i < mapService.Layers.length; ++i) { for (let i = 0; i < mapService.Layers.length; ++i) {
const l = mapService.Layers[i]; const l = mapService.Layers[i];
const tl = tileLayer(l.url, { attribution: mapService.Attributions }); const tl = tileLayer(l.url, {attribution: mapService.Attributions});
if (i === 0) { if (i === 0) {
this.mapOptions.layers.push(tl); this.mapOptions.layers.push(tl);
} }
@ -140,7 +140,7 @@ export class GalleryMapLightboxComponent implements OnChanges {
this.mapLayerControl = control.layers( this.mapLayerControl = control.layers(
this.mapLayersControlOption.baseLayers, this.mapLayersControlOption.baseLayers,
this.mapLayersControlOption.overlays, this.mapLayersControlOption.overlays,
{ position: 'bottomright' } {position: 'bottomright'}
); );
} }
@ -215,7 +215,7 @@ export class GalleryMapLightboxComponent implements OnChanges {
if ( if (
PageHelper.ScrollY > to.top || PageHelper.ScrollY > to.top ||
PageHelper.ScrollY + GalleryMapLightboxComponent.getScreenHeight() < PageHelper.ScrollY + GalleryMapLightboxComponent.getScreenHeight() <
to.top to.top
) { ) {
PageHelper.ScrollY = to.top; PageHelper.ScrollY = to.top;
} }
@ -277,7 +277,7 @@ export class GalleryMapLightboxComponent implements OnChanges {
`<img style="width: ${width}px; height: ${height}px" ` + `<img style="width: ${width}px; height: ${height}px" ` +
`src="${photoTh.Src}" alt="preview">`; `src="${photoTh.Src}" alt="preview">`;
if (!mkr.getPopup()) { if (!mkr.getPopup()) {
mkr.bindPopup(photoPopup, { minWidth: width }); mkr.bindPopup(photoPopup, {minWidth: width});
} else { } else {
mkr.setPopupContent(photoPopup); mkr.setPopupContent(photoPopup);
} }
@ -293,7 +293,7 @@ export class GalleryMapLightboxComponent implements OnChanges {
</span> </span>
</div>`; </div>`;
mkr.bindPopup(noPhotoPopup, { minWidth: width }); mkr.bindPopup(noPhotoPopup, {minWidth: width});
mkr.on('popupopen', () => { mkr.on('popupopen', () => {
photoTh.load(); photoTh.load();
photoTh.CurrentlyWaiting = true; photoTh.CurrentlyWaiting = true;

View File

@ -1,13 +1,13 @@
import { Injectable } from '@angular/core'; import {Injectable} from '@angular/core';
import { NetworkService } from '../../../model/network/network.service'; import {NetworkService} from '../../../model/network/network.service';
import { FileDTO } from '../../../../../common/entities/FileDTO'; import {FileDTO} from '../../../../../common/entities/FileDTO';
import { Utils } from '../../../../../common/Utils'; import {Utils} from '../../../../../common/Utils';
import { Config } from '../../../../../common/config/public/Config'; import {Config} from '../../../../../common/config/public/Config';
import { import {
MapLayers, MapLayers,
MapProviders, MapProviders,
} from '../../../../../common/config/public/ClientConfig'; } from '../../../../../common/config/public/ClientConfig';
import { LatLngLiteral } from 'leaflet'; import {LatLngLiteral} from 'leaflet';
@Injectable() @Injectable()
export class MapService { export class MapService {