mirror of
https://github.com/bpatrik/pigallery2.git
synced 2024-12-25 02:04:15 +02:00
Implementing on-the-fly GPX compression.
Its a lossy compression that can be finetuned in the config. #504
This commit is contained in:
parent
b6b576ba2f
commit
3bff1a4383
40
src/backend/middlewares/MetaFileMWs.ts
Normal file
40
src/backend/middlewares/MetaFileMWs.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
|
127
src/backend/model/GPXProcessing.ts
Normal file
127
src/backend/model/GPXProcessing.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import { VersionMWs } from '../middlewares/VersionMWs';
|
||||
import { SupportedFormats } from '../../common/SupportedFormats';
|
||||
import { PhotoConverterMWs } from '../middlewares/thumbnail/PhotoConverterMWs';
|
||||
import { ServerTimingMWs } from '../middlewares/ServerTimingMWs';
|
||||
import {MetaFileMWs} from '../middlewares/MetaFileMWs';
|
||||
|
||||
export class GalleryRouter {
|
||||
public static route(app: Express): void {
|
||||
@ -21,6 +22,7 @@ export class GalleryRouter {
|
||||
this.addGetBestFitVideo(app);
|
||||
this.addGetVideo(app);
|
||||
this.addGetMetaFile(app);
|
||||
this.addGetBestFitMetaFile(app);
|
||||
this.addRandom(app);
|
||||
this.addDirectoryList(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 {
|
||||
app.get(
|
||||
['/api/gallery/random/:searchQueryDTO'],
|
||||
|
@ -5,17 +5,17 @@ import {
|
||||
JobTrigger,
|
||||
JobTriggerType,
|
||||
} from '../../entities/job/JobScheduleDTO';
|
||||
import { ClientConfig } from '../public/ClientConfig';
|
||||
import { SubConfigClass } from 'typeconfig/src/decorators/class/SubConfigClass';
|
||||
import { ConfigProperty } from 'typeconfig/src/decorators/property/ConfigPropoerty';
|
||||
import { DefaultsJobs } from '../../entities/job/JobDTO';
|
||||
import {ClientConfig, ClientMetaFileConfig} from '../public/ClientConfig';
|
||||
import {SubConfigClass} from 'typeconfig/src/decorators/class/SubConfigClass';
|
||||
import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty';
|
||||
import {DefaultsJobs} from '../../entities/job/JobDTO';
|
||||
import {
|
||||
SearchQueryDTO,
|
||||
SearchQueryTypes,
|
||||
TextSearch,
|
||||
} from '../../entities/SearchQueryDTO';
|
||||
import { SortingMethods } from '../../entities/SortingMethods';
|
||||
import { UserRoles } from '../../entities/UserDTO';
|
||||
import {SortingMethods} from '../../entities/SortingMethods';
|
||||
import {UserRoles} from '../../entities/UserDTO';
|
||||
|
||||
export enum DatabaseType {
|
||||
memory = 1,
|
||||
@ -71,15 +71,15 @@ export type videoFormatType = 'mp4' | 'webm';
|
||||
|
||||
@SubConfigClass()
|
||||
export class MySQLConfig {
|
||||
@ConfigProperty({ envAlias: 'MYSQL_HOST' })
|
||||
@ConfigProperty({envAlias: 'MYSQL_HOST'})
|
||||
host: string = 'localhost';
|
||||
@ConfigProperty({ envAlias: 'MYSQL_PORT', min: 0, max: 65535 })
|
||||
@ConfigProperty({envAlias: 'MYSQL_PORT', min: 0, max: 65535})
|
||||
port: number = 3306;
|
||||
@ConfigProperty({ envAlias: 'MYSQL_DATABASE' })
|
||||
@ConfigProperty({envAlias: 'MYSQL_DATABASE'})
|
||||
database: string = 'pigallery2';
|
||||
@ConfigProperty({ envAlias: 'MYSQL_USERNAME' })
|
||||
@ConfigProperty({envAlias: 'MYSQL_USERNAME'})
|
||||
username: string = '';
|
||||
@ConfigProperty({ envAlias: 'MYSQL_PASSWORD', type: 'password' })
|
||||
@ConfigProperty({envAlias: 'MYSQL_PASSWORD', type: 'password'})
|
||||
password: string = '';
|
||||
}
|
||||
|
||||
@ -94,13 +94,13 @@ export class UserConfig {
|
||||
@ConfigProperty()
|
||||
name: string;
|
||||
|
||||
@ConfigProperty({ type: UserRoles })
|
||||
@ConfigProperty({type: UserRoles})
|
||||
role: UserRoles;
|
||||
|
||||
@ConfigProperty({ description: 'Unencrypted, temporary password' })
|
||||
@ConfigProperty({description: 'Unencrypted, temporary password'})
|
||||
password: string;
|
||||
|
||||
@ConfigProperty({ description: 'Encrypted password' })
|
||||
@ConfigProperty({description: 'Encrypted password'})
|
||||
encryptedPassword: string | undefined;
|
||||
|
||||
constructor(name: string, password: string, role: UserRoles) {
|
||||
@ -142,12 +142,31 @@ export class ServerDataBaseConfig {
|
||||
|
||||
@SubConfigClass()
|
||||
export class ServerThumbnailConfig {
|
||||
@ConfigProperty({ description: 'if true, photos will have better quality.' })
|
||||
@ConfigProperty({description: 'if true, photos will have better quality.'})
|
||||
qualityPriority: boolean = true;
|
||||
@ConfigProperty({ type: 'ratio' })
|
||||
@ConfigProperty({type: 'ratio'})
|
||||
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()
|
||||
export class ServerSharingConfig {
|
||||
@ConfigProperty()
|
||||
@ -158,14 +177,14 @@ export class ServerSharingConfig {
|
||||
export class ServerIndexingConfig {
|
||||
@ConfigProperty()
|
||||
cachedFolderTimeout: number = 1000 * 60 * 60; // Do not rescans the folder if seems ok
|
||||
@ConfigProperty({ type: ReIndexingSensitivity })
|
||||
@ConfigProperty({type: ReIndexingSensitivity})
|
||||
reIndexingSensitivity: ReIndexingSensitivity = ReIndexingSensitivity.low;
|
||||
@ConfigProperty({
|
||||
arrayType: 'string',
|
||||
description:
|
||||
"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 contain a '/', any folder with this name will be excluded.",
|
||||
'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 contain a \'/\', any folder with this name will be excluded.',
|
||||
})
|
||||
excludeFolderList: string[] = ['.Trash-1000', '.dtrash', '$RECYCLE.BIN'];
|
||||
@ConfigProperty({
|
||||
@ -178,11 +197,11 @@ export class ServerIndexingConfig {
|
||||
|
||||
@SubConfigClass()
|
||||
export class ServerThreadingConfig {
|
||||
@ConfigProperty({ description: 'App can run on multiple thread' })
|
||||
@ConfigProperty({description: 'App can run on multiple thread'})
|
||||
enabled: boolean = true;
|
||||
@ConfigProperty({
|
||||
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
|
||||
}
|
||||
@ -195,9 +214,9 @@ export class ServerDuplicatesConfig {
|
||||
|
||||
@SubConfigClass()
|
||||
export class ServerLogConfig {
|
||||
@ConfigProperty({ type: LogLevel })
|
||||
@ConfigProperty({type: LogLevel})
|
||||
level: LogLevel = LogLevel.info;
|
||||
@ConfigProperty({ type: SQLLogLevel })
|
||||
@ConfigProperty({type: SQLLogLevel})
|
||||
sqlLevel: SQLLogLevel = SQLLogLevel.error;
|
||||
@ConfigProperty()
|
||||
logServerTiming: boolean = false;
|
||||
@ -205,32 +224,32 @@ export class ServerLogConfig {
|
||||
|
||||
@SubConfigClass()
|
||||
export class NeverJobTrigger implements JobTrigger {
|
||||
@ConfigProperty({ type: JobTriggerType })
|
||||
@ConfigProperty({type: JobTriggerType})
|
||||
readonly type = JobTriggerType.never;
|
||||
}
|
||||
|
||||
@SubConfigClass()
|
||||
export class ScheduledJobTrigger implements JobTrigger {
|
||||
@ConfigProperty({ type: JobTriggerType })
|
||||
@ConfigProperty({type: JobTriggerType})
|
||||
readonly type = JobTriggerType.scheduled;
|
||||
|
||||
@ConfigProperty({ type: 'unsignedInt' })
|
||||
@ConfigProperty({type: 'unsignedInt'})
|
||||
time: number; // data time
|
||||
}
|
||||
|
||||
@SubConfigClass()
|
||||
export class PeriodicJobTrigger implements JobTrigger {
|
||||
@ConfigProperty({ type: JobTriggerType })
|
||||
@ConfigProperty({type: JobTriggerType})
|
||||
readonly type = JobTriggerType.periodic;
|
||||
@ConfigProperty({ type: 'unsignedInt', max: 7 })
|
||||
@ConfigProperty({type: 'unsignedInt', max: 7})
|
||||
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
|
||||
}
|
||||
|
||||
@SubConfigClass()
|
||||
export class AfterJobTrigger implements JobTrigger {
|
||||
@ConfigProperty({ type: JobTriggerType })
|
||||
@ConfigProperty({type: JobTriggerType})
|
||||
readonly type = JobTriggerType.after;
|
||||
@ConfigProperty()
|
||||
afterScheduleName: string | undefined; // runs after schedule
|
||||
@ -294,16 +313,16 @@ export class JobScheduleConfig implements JobScheduleDTO {
|
||||
|
||||
@SubConfigClass()
|
||||
export class ServerJobConfig {
|
||||
@ConfigProperty({ type: 'integer', description: 'Job history size' })
|
||||
@ConfigProperty({type: 'integer', description: 'Job history size'})
|
||||
maxSavedProgress: number = 10;
|
||||
@ConfigProperty({ arrayType: JobScheduleConfig })
|
||||
@ConfigProperty({arrayType: JobScheduleConfig})
|
||||
scheduled: JobScheduleConfig[] = [
|
||||
new JobScheduleConfig(
|
||||
DefaultsJobs[DefaultsJobs.Indexing],
|
||||
DefaultsJobs[DefaultsJobs.Indexing],
|
||||
false,
|
||||
new NeverJobTrigger(),
|
||||
{ indexChangesOnly: true }
|
||||
{indexChangesOnly: true}
|
||||
),
|
||||
new JobScheduleConfig(
|
||||
DefaultsJobs[DefaultsJobs['Preview Filling']],
|
||||
@ -317,39 +336,39 @@ export class ServerJobConfig {
|
||||
DefaultsJobs[DefaultsJobs['Thumbnail Generation']],
|
||||
false,
|
||||
new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Preview Filling']]),
|
||||
{ sizes: [240], indexedOnly: true }
|
||||
{sizes: [240], indexedOnly: true}
|
||||
),
|
||||
new JobScheduleConfig(
|
||||
DefaultsJobs[DefaultsJobs['Photo Converting']],
|
||||
DefaultsJobs[DefaultsJobs['Photo Converting']],
|
||||
false,
|
||||
new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Thumbnail Generation']]),
|
||||
{ indexedOnly: true }
|
||||
{indexedOnly: true}
|
||||
),
|
||||
new JobScheduleConfig(
|
||||
DefaultsJobs[DefaultsJobs['Video Converting']],
|
||||
DefaultsJobs[DefaultsJobs['Video Converting']],
|
||||
false,
|
||||
new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Photo Converting']]),
|
||||
{ indexedOnly: true }
|
||||
{indexedOnly: true}
|
||||
),
|
||||
new JobScheduleConfig(
|
||||
DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']],
|
||||
DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']],
|
||||
false,
|
||||
new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Video Converting']]),
|
||||
{ indexedOnly: true }
|
||||
{indexedOnly: true}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@SubConfigClass()
|
||||
export class VideoTranscodingConfig {
|
||||
@ConfigProperty({ type: 'unsignedInt' })
|
||||
@ConfigProperty({type: 'unsignedInt'})
|
||||
bitRate: number = 5 * 1024 * 1024;
|
||||
@ConfigProperty({ type: 'unsignedInt' })
|
||||
@ConfigProperty({type: 'unsignedInt'})
|
||||
resolution: videoResolutionType = 720;
|
||||
@ConfigProperty({ type: 'positiveFloat' })
|
||||
@ConfigProperty({type: 'positiveFloat'})
|
||||
fps: number = 25;
|
||||
@ConfigProperty()
|
||||
codec: videoCodecType = 'libx264';
|
||||
@ -388,7 +407,7 @@ export class PhotoConvertingConfig {
|
||||
description: 'Converts photos on the fly, when they are requested.',
|
||||
})
|
||||
onTheFly: boolean = true;
|
||||
@ConfigProperty({ type: 'unsignedInt' })
|
||||
@ConfigProperty({type: 'unsignedInt'})
|
||||
resolution: videoResolutionType = 1080;
|
||||
}
|
||||
|
||||
@ -400,12 +419,12 @@ export class ServerPhotoConfig {
|
||||
|
||||
@SubConfigClass()
|
||||
export class ServerPreviewConfig {
|
||||
@ConfigProperty({ type: 'object' })
|
||||
@ConfigProperty({type: 'object'})
|
||||
SearchQuery: SearchQueryDTO = {
|
||||
type: SearchQueryTypes.any_text,
|
||||
text: '',
|
||||
} as TextSearch;
|
||||
@ConfigProperty({ arrayType: SortingMethods })
|
||||
@ConfigProperty({arrayType: SortingMethods})
|
||||
Sorting: SortingMethods[] = [
|
||||
SortingMethods.descRating,
|
||||
SortingMethods.descDate,
|
||||
@ -434,25 +453,25 @@ export class ServerMediaConfig {
|
||||
|
||||
@SubConfigClass()
|
||||
export class ServerEnvironmentConfig {
|
||||
@ConfigProperty({ volatile: true })
|
||||
@ConfigProperty({volatile: true})
|
||||
upTime: string | undefined;
|
||||
@ConfigProperty({ volatile: true })
|
||||
@ConfigProperty({volatile: true})
|
||||
appVersion: string | undefined;
|
||||
@ConfigProperty({ volatile: true })
|
||||
@ConfigProperty({volatile: true})
|
||||
buildTime: string | undefined;
|
||||
@ConfigProperty({ volatile: true })
|
||||
@ConfigProperty({volatile: true})
|
||||
buildCommitHash: string | undefined;
|
||||
@ConfigProperty({ volatile: true })
|
||||
@ConfigProperty({volatile: true})
|
||||
isDocker: boolean | undefined;
|
||||
}
|
||||
|
||||
@SubConfigClass()
|
||||
export class ServerConfig {
|
||||
@ConfigProperty({ volatile: true })
|
||||
@ConfigProperty({volatile: true})
|
||||
Environment: ServerEnvironmentConfig = new ServerEnvironmentConfig();
|
||||
@ConfigProperty({ arrayType: 'string' })
|
||||
@ConfigProperty({arrayType: 'string'})
|
||||
sessionSecret: string[] = [];
|
||||
@ConfigProperty({ type: 'unsignedInt', envAlias: 'PORT', min: 0, max: 65535 })
|
||||
@ConfigProperty({type: 'unsignedInt', envAlias: 'PORT', min: 0, max: 65535})
|
||||
port: number = 80;
|
||||
@ConfigProperty()
|
||||
host: string = '0.0.0.0';
|
||||
@ -466,7 +485,7 @@ export class ServerConfig {
|
||||
Database: ServerDataBaseConfig = new ServerDataBaseConfig();
|
||||
@ConfigProperty()
|
||||
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
|
||||
@ConfigProperty()
|
||||
Indexing: ServerIndexingConfig = new ServerIndexingConfig();
|
||||
@ -482,6 +501,8 @@ export class ServerConfig {
|
||||
Log: ServerLogConfig = new ServerLogConfig();
|
||||
@ConfigProperty()
|
||||
Jobs: ServerJobConfig = new ServerJobConfig();
|
||||
@ConfigProperty()
|
||||
MetaFile: ServerMetaFileConfig = new ServerMetaFileConfig();
|
||||
}
|
||||
|
||||
export interface IPrivateConfig {
|
||||
|
@ -182,6 +182,12 @@ export class ClientPhotoConfig {
|
||||
loadFullImageOnZoom: boolean = true;
|
||||
}
|
||||
|
||||
@SubConfigClass()
|
||||
export class ClientGPXCompressingConfig {
|
||||
@ConfigProperty()
|
||||
enabled: boolean = true;
|
||||
}
|
||||
|
||||
@SubConfigClass()
|
||||
export class ClientMediaConfig {
|
||||
@ConfigProperty()
|
||||
@ -198,6 +204,11 @@ export class ClientMetaFileConfig {
|
||||
description: 'Reads *.gpx files and renders them on the map.',
|
||||
})
|
||||
gpx: boolean = true;
|
||||
@ConfigProperty({
|
||||
description: 'Reads *.gpx files and renders them on the map.',
|
||||
})
|
||||
@ConfigProperty()
|
||||
GPXCompressing: ClientGPXCompressingConfig = new ClientGPXCompressingConfig();
|
||||
@ConfigProperty({
|
||||
description:
|
||||
'Reads *.md files in a directory and shows the next to the map.',
|
||||
|
@ -6,22 +6,22 @@ import {
|
||||
OnChanges,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { PhotoDTO } from '../../../../../../common/entities/PhotoDTO';
|
||||
import { Dimension } from '../../../../model/IRenderable';
|
||||
import { FullScreenService } from '../../fullscreen.service';
|
||||
import {PhotoDTO} from '../../../../../../common/entities/PhotoDTO';
|
||||
import {Dimension} from '../../../../model/IRenderable';
|
||||
import {FullScreenService} from '../../fullscreen.service';
|
||||
import {
|
||||
IconThumbnail,
|
||||
Thumbnail,
|
||||
ThumbnailBase,
|
||||
ThumbnailManagerService,
|
||||
} from '../../thumbnailManager.service';
|
||||
import { MediaIcon } from '../../MediaIcon';
|
||||
import { Media } from '../../Media';
|
||||
import { PageHelper } from '../../../../model/page.helper';
|
||||
import { FileDTO } from '../../../../../../common/entities/FileDTO';
|
||||
import { Utils } from '../../../../../../common/Utils';
|
||||
import { Config } from '../../../../../../common/config/public/Config';
|
||||
import { MapService } from '../map.service';
|
||||
import {MediaIcon} from '../../MediaIcon';
|
||||
import {Media} from '../../Media';
|
||||
import {PageHelper} from '../../../../model/page.helper';
|
||||
import {FileDTO} from '../../../../../../common/entities/FileDTO';
|
||||
import {Utils} from '../../../../../../common/Utils';
|
||||
import {Config} from '../../../../../../common/config/public/Config';
|
||||
import {MapService} from '../map.service';
|
||||
import {
|
||||
control,
|
||||
Control,
|
||||
@ -41,7 +41,7 @@ import {
|
||||
polyline,
|
||||
tileLayer,
|
||||
} from 'leaflet';
|
||||
import { LeafletControlLayersConfig } from '@asymmetrik/ngx-leaflet';
|
||||
import {LeafletControlLayersConfig} from '@asymmetrik/ngx-leaflet';
|
||||
|
||||
@Component({
|
||||
selector: 'app-gallery-map-lightbox',
|
||||
@ -66,7 +66,7 @@ export class GalleryMapLightboxComponent implements OnChanges {
|
||||
public visible = false;
|
||||
public controllersVisible = false;
|
||||
public opacity = 1.0;
|
||||
@ViewChild('root', { static: true }) elementRef: ElementRef;
|
||||
@ViewChild('root', {static: true}) elementRef: ElementRef;
|
||||
public mapOptions: MapOptions = {
|
||||
zoom: 2,
|
||||
// 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) {
|
||||
const l = mapService.Layers[i];
|
||||
const tl = tileLayer(l.url, { attribution: mapService.Attributions });
|
||||
const tl = tileLayer(l.url, {attribution: mapService.Attributions});
|
||||
if (i === 0) {
|
||||
this.mapOptions.layers.push(tl);
|
||||
}
|
||||
@ -140,7 +140,7 @@ export class GalleryMapLightboxComponent implements OnChanges {
|
||||
this.mapLayerControl = control.layers(
|
||||
this.mapLayersControlOption.baseLayers,
|
||||
this.mapLayersControlOption.overlays,
|
||||
{ position: 'bottomright' }
|
||||
{position: 'bottomright'}
|
||||
);
|
||||
}
|
||||
|
||||
@ -215,7 +215,7 @@ export class GalleryMapLightboxComponent implements OnChanges {
|
||||
if (
|
||||
PageHelper.ScrollY > to.top ||
|
||||
PageHelper.ScrollY + GalleryMapLightboxComponent.getScreenHeight() <
|
||||
to.top
|
||||
to.top
|
||||
) {
|
||||
PageHelper.ScrollY = to.top;
|
||||
}
|
||||
@ -277,7 +277,7 @@ export class GalleryMapLightboxComponent implements OnChanges {
|
||||
`<img style="width: ${width}px; height: ${height}px" ` +
|
||||
`src="${photoTh.Src}" alt="preview">`;
|
||||
if (!mkr.getPopup()) {
|
||||
mkr.bindPopup(photoPopup, { minWidth: width });
|
||||
mkr.bindPopup(photoPopup, {minWidth: width});
|
||||
} else {
|
||||
mkr.setPopupContent(photoPopup);
|
||||
}
|
||||
@ -293,7 +293,7 @@ export class GalleryMapLightboxComponent implements OnChanges {
|
||||
</span>
|
||||
</div>`;
|
||||
|
||||
mkr.bindPopup(noPhotoPopup, { minWidth: width });
|
||||
mkr.bindPopup(noPhotoPopup, {minWidth: width});
|
||||
mkr.on('popupopen', () => {
|
||||
photoTh.load();
|
||||
photoTh.CurrentlyWaiting = true;
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { NetworkService } from '../../../model/network/network.service';
|
||||
import { FileDTO } from '../../../../../common/entities/FileDTO';
|
||||
import { Utils } from '../../../../../common/Utils';
|
||||
import { Config } from '../../../../../common/config/public/Config';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {NetworkService} from '../../../model/network/network.service';
|
||||
import {FileDTO} from '../../../../../common/entities/FileDTO';
|
||||
import {Utils} from '../../../../../common/Utils';
|
||||
import {Config} from '../../../../../common/config/public/Config';
|
||||
import {
|
||||
MapLayers,
|
||||
MapProviders,
|
||||
} from '../../../../../common/config/public/ClientConfig';
|
||||
import { LatLngLiteral } from 'leaflet';
|
||||
import {LatLngLiteral} from 'leaflet';
|
||||
|
||||
@Injectable()
|
||||
export class MapService {
|
||||
|
Loading…
Reference in New Issue
Block a user