improving thumbnail generation. adding thumbnails to map

This commit is contained in:
Braun Patrik 2017-03-20 21:37:23 +01:00
parent 483af01c99
commit 385dcd7c4d
14 changed files with 270 additions and 96 deletions

View File

@ -73,10 +73,10 @@ export class ThumbnailGeneratorMWs {
private static addThInfoToPhotos(photos: Array<PhotoDTO>) {
let thumbnailFolder = ProjectPath.ThumbnailFolder;
for (let j = 0; j < Config.Client.thumbnailSizes.length; j++) {
let size = Config.Client.thumbnailSizes[j];
for (let i = 0; i < photos.length; i++) {
let fullImagePath = path.join(ProjectPath.ImageFolder, photos[i].directory.path, photos[i].directory.name, photos[i].name);
for (let j = 0; j < Config.Client.thumbnailSizes.length; j++) {
let size = Config.Client.thumbnailSizes[j];
let thPath = path.join(thumbnailFolder, ThumbnailGeneratorMWs.generateThumbnailName(fullImagePath, size));
if (fs.existsSync(thPath) === true) {
if (typeof photos[i].readyThumbnails == "undefined") {
@ -85,6 +85,11 @@ export class ThumbnailGeneratorMWs {
photos[i].readyThumbnails.push(size);
}
}
let iconPath = path.join(thumbnailFolder, ThumbnailGeneratorMWs.generateThumbnailName(fullImagePath, Config.Client.iconSize));
if (fs.existsSync(iconPath) === true) {
photos[i].readyIcon = true;
}
}
}
@ -129,13 +134,13 @@ export class ThumbnailGeneratorMWs {
//load parameters
let imagePath = req.resultPipe;
let size: number = 30;
let size: number = Config.Client.iconSize;
ThumbnailGeneratorMWs.generateImage(imagePath, size, true, req, res, next);
}
private static generateImage(imagePath, size, makeSquare, req: Request, res: Response, next: NextFunction) {
private static generateImage(imagePath: string, size: number, makeSquare: boolean, req: Request, res: Response, next: NextFunction) {
//generate thumbnail path
let thPath = path.join(ProjectPath.ThumbnailFolder, ThumbnailGeneratorMWs.generateThumbnailName(imagePath, size));

View File

@ -39,6 +39,7 @@ export class GalleryManager implements IGalleryManager {
dir.photos[i].metadata.positionData = <any>JSON.parse(<any>dir.photos[i].metadata.positionData);
dir.photos[i].metadata.size = <any>JSON.parse(<any>dir.photos[i].metadata.size);
dir.photos[i].readyThumbnails = [];
dir.photos[i].readyIcon = false;
}
}

View File

@ -26,6 +26,8 @@ export class PhotoEntity implements PhotoDTO {
readyThumbnails: Array<number> = [];
readyIcon: boolean = false;
}

View File

@ -14,34 +14,36 @@ interface DataBaseConfig {
}
interface ServerConfig {
port:number;
imagesFolder:string;
thumbnailFolder:string;
port: number;
imagesFolder: string;
thumbnailFolder: string;
database: DataBaseConfig;
}
interface SearchConfig {
searchEnabled:boolean
instantSearchEnabled:boolean
autocompleteEnabled:boolean
searchEnabled: boolean
instantSearchEnabled: boolean
autocompleteEnabled: boolean
}
interface ClientConfig {
thumbnailSizes:Array<number>;
Search:SearchConfig;
concurrentThumbnailGenerations:number;
enableCache:boolean;
enableOnScrollRendering:boolean;
enableOnScrollThumbnailPrioritising:boolean;
authenticationRequired:boolean;
iconSize: number;
thumbnailSizes: Array<number>;
Search: SearchConfig;
concurrentThumbnailGenerations: number;
enableCache: boolean;
enableOnScrollRendering: boolean;
enableOnScrollThumbnailPrioritising: boolean;
authenticationRequired: boolean;
googleApiKey: string;
}
export class ConfigClass {
public Server:ServerConfig = null;
public Server: ServerConfig = null;
public Client:ClientConfig = {
public Client: ClientConfig = {
thumbnailSizes: [200, 400, 600],
iconSize: 30,
Search: {
searchEnabled: true,
instantSearchEnabled: true,
@ -55,7 +57,7 @@ export class ConfigClass {
googleApiKey: ""
};
public setDatabaseType(type:DatabaseType) {
public setDatabaseType(type: DatabaseType) {
this.Server.database.type = type;
if (type === DatabaseType.memory) {
this.Client.Search.searchEnabled = false;

View File

@ -6,6 +6,7 @@ export interface PhotoDTO {
directory: DirectoryDTO;
metadata: PhotoMetadata;
readyThumbnails: Array<number>;
readyIcon: boolean;
}
export interface PhotoMetadata {

View File

@ -8,7 +8,7 @@ import {appRoutes} from "./app.routing";
import {UserService} from "./model/network/user.service";
import {GalleryService} from "./gallery/gallery.service";
import {NetworkService} from "./model/network/network.service";
import {ThumbnailLoaderService} from "./gallery/grid/thumnailLoader.service";
import {ThumbnailLoaderService} from "./gallery/thumnailLoader.service";
import {GalleryCacheService} from "./gallery/cache.gallery.service";
import {FullScreenService} from "./gallery/fullscreen.service";
import {AuthenticationService} from "./model/network/authentication.service";
@ -29,7 +29,7 @@ import {StringifyRole} from "./pipes/StringifyRolePipe";
import {Config} from "./config/Config";
import {GalleryMapComponent} from "./gallery/map/map.gallery.component";
import {GalleryMapLightboxComponent} from "./gallery/map/lightbox/lightbox.map.gallery.component";
import {ThumbnailManagerService} from "./gallery/grid/thumnailManager.service";
import {ThumbnailManagerService} from "./gallery/thumnailManager.service";
@NgModule({
imports: [

View File

@ -0,0 +1,42 @@
import {PhotoDTO} from "../../../common/entities/PhotoDTO";
import {Utils} from "../../../common/Utils";
export class IconPhoto {
protected replacementSizeCache: number|boolean = false;
constructor(public photo: PhotoDTO) {
}
iconLoaded() {
this.photo.readyIcon = true;
}
isIconAvailable() {
return this.photo.readyIcon;
}
getIconPath() {
return Utils.concatUrls("/api/gallery/content/", this.photo.directory.path, this.photo.directory.name, this.photo.name, "icon");
}
getPhotoPath() {
return Utils.concatUrls("/api/gallery/content/", this.photo.directory.path, this.photo.directory.name, this.photo.name);
}
equals(other: PhotoDTO | IconPhoto): boolean {
//is gridphoto
if (other instanceof IconPhoto) {
return this.photo.directory.path === other.photo.directory.path && this.photo.directory.name === other.photo.directory.name && this.photo.name === other.photo.name
}
//is photo
if (other.directory) {
return this.photo.directory.path === other.directory.path && this.photo.directory.name === other.directory.name && this.photo.name === other.name
}
return false;
}
}

View File

@ -1,12 +1,12 @@
import {PhotoDTO} from "../../../common/entities/PhotoDTO";
import {Utils} from "../../../common/Utils";
import {Config} from "../config/Config";
export class Photo {
import {IconPhoto} from "./IconPhoto";
export class Photo extends IconPhoto {
protected replacementSizeCache: boolean|number = false;
constructor(public photo: PhotoDTO, public renderWidth: number, public renderHeight: number) {
constructor(photo: PhotoDTO, public renderWidth: number, public renderHeight: number) {
super(photo);
}
@ -22,7 +22,7 @@ export class Photo {
return Utils.findClosest(renderSize, Config.Client.thumbnailSizes);
}
getReplacementThumbnailSize() {
getReplacementThumbnailSize(): number {
if (this.replacementSizeCache === false) {
this.replacementSizeCache = null;
@ -37,7 +37,7 @@ export class Photo {
}
}
}
return this.replacementSizeCache;
return <number>this.replacementSizeCache;
}
isReplacementThumbnailAvailable() {
@ -59,22 +59,5 @@ export class Photo {
return Utils.concatUrls("/api/gallery/content/", this.photo.directory.path, this.photo.directory.name, this.photo.name, "thumbnail", size.toString());
}
getPhotoPath() {
return Utils.concatUrls("/api/gallery/content/", this.photo.directory.path, this.photo.directory.name, this.photo.name);
}
equals(other: any) {
//is gridphoto
if (other.photo) {
return this.photo.directory.path === other.photo.directory.path && this.photo.directory.name === other.photo.directory.name && this.photo.name === other.photo.name
}
//is photo
if (other.directory) {
return this.photo.directory.path === other.directory.path && this.photo.directory.name === other.directory.name && this.photo.name === other.name
}
return false;
}
}

View File

@ -3,7 +3,7 @@ import {DirectoryDTO} from "../../../../common/entities/DirectoryDTO";
import {RouterLink} from "@angular/router";
import {Utils} from "../../../../common/Utils";
import {Photo} from "../Photo";
import {Thumbnail, ThumbnailManagerService} from "../grid/thumnailManager.service";
import {Thumbnail, ThumbnailManagerService} from "../thumnailManager.service";
@Component({
selector: 'gallery-directory',

View File

@ -4,7 +4,7 @@ import {GridPhoto} from "../GridPhoto";
import {SearchTypes} from "../../../../../common/entities/AutoCompleteItem";
import {RouterLink} from "@angular/router";
import {Config} from "../../../config/Config";
import {Thumbnail, ThumbnailManagerService} from "../thumnailManager.service";
import {Thumbnail, ThumbnailManagerService} from "../../thumnailManager.service";
@Component({
selector: 'gallery-grid-photo',

View File

@ -2,8 +2,9 @@ import {Component, Input, OnChanges, ElementRef, ViewChild} from "@angular/core"
import {PhotoDTO} from "../../../../../common/entities/PhotoDTO";
import {Dimension} from "../../../model/IRenderable";
import {FullScreenService} from "../../fullscreen.service";
import {Utils} from "../../../../../common/Utils";
import {SebmGoogleMap} from "angular2-google-maps/core";
import {ThumbnailManagerService, IconThumbnail} from "../../thumnailManager.service";
import {IconPhoto} from "../../IconPhoto";
@Component({
selector: 'gallery-map-lightbox',
@ -18,7 +19,7 @@ export class GalleryMapLightboxComponent implements OnChanges {
public mapDimension: Dimension = <Dimension>{top: 0, left: 0, width: 0, height: 0};
private visible = false;
private opacity = 1.0;
mapPhotos: Array<{latitude: number, longitude: number, iconUrl}> = [];
mapPhotos: Array<{latitude: number, longitude: number, iconUrl?: string, thumbnail: IconThumbnail}> = [];
mapCenter = {latitude: 0, longitude: 0};
@ViewChild("root") elementRef: ElementRef;
@ -26,7 +27,7 @@ export class GalleryMapLightboxComponent implements OnChanges {
@ViewChild(SebmGoogleMap) map: SebmGoogleMap;
constructor(private fullScreenService: FullScreenService) {
constructor(private fullScreenService: FullScreenService, private thumbnailService: ThumbnailManagerService) {
}
@ -81,21 +82,33 @@ export class GalleryMapLightboxComponent implements OnChanges {
this.opacity = 0.0;
setTimeout(() => {
this.visible = false;
this.mapPhotos = [];
this.hideImages();
}, 500);
}
showImages() {
this.hideImages();
this.mapPhotos = this.photos.filter(p => {
return p.metadata && p.metadata.positionData && p.metadata.positionData.GPSData;
}).map(p => {
return {
let th = this.thumbnailService.getIcon(new IconPhoto(p));
let obj: {latitude: number, longitude: number, iconUrl?: string, thumbnail: IconThumbnail} = {
latitude: p.metadata.positionData.GPSData.latitude,
longitude: p.metadata.positionData.GPSData.longitude,
iconUrl: Utils.concatUrls("/api/gallery/content/", p.directory.path, p.directory.name, p.name, "icon")
thumbnail: th
};
if (th.Available == true) {
obj.iconUrl = th.Src;
} else {
th.OnLoad = () => {
obj.iconUrl = th.Src;
};
}
return obj;
});
if (this.mapPhotos.length > 0) {
@ -103,6 +116,11 @@ export class GalleryMapLightboxComponent implements OnChanges {
}
}
hideImages() {
this.mapPhotos.forEach(mp => mp.thumbnail.destroy());
this.mapPhotos = [];
}
private getBodyScrollTop(): number {
return window.scrollY;

View File

@ -1,6 +1,5 @@
import {Component, OnChanges, Input, ViewChild, ElementRef} from "@angular/core";
import {PhotoDTO} from "../../../../common/entities/PhotoDTO";
import {Utils} from "../../../../common/Utils";
import {IRenderable, Dimension} from "../../model/IRenderable";
import {GalleryMapLightboxComponent} from "./lightbox/lightbox.map.gallery.component";
@Component({
@ -13,7 +12,7 @@ export class GalleryMapComponent implements OnChanges, IRenderable {
@Input() photos: Array<PhotoDTO>;
@ViewChild(GalleryMapLightboxComponent) mapLightbox: GalleryMapLightboxComponent;
mapPhotos: Array<{latitude: number, longitude: number, iconUrl}> = [];
mapPhotos: Array<{latitude: number, longitude: number}> = [];
mapCenter = {latitude: 0, longitude: 0};
@ViewChild("map") map: ElementRef;
@ -24,8 +23,7 @@ export class GalleryMapComponent implements OnChanges, IRenderable {
}).map(p => {
return {
latitude: p.metadata.positionData.GPSData.latitude,
longitude: p.metadata.positionData.GPSData.longitude,
iconUrl: Utils.concatUrls("/api/gallery/content/", p.directory.path, p.directory.name, p.name, "icon")
longitude: p.metadata.positionData.GPSData.longitude
};
});

View File

@ -1,7 +1,9 @@
import {Injectable} from "@angular/core";
import {Config} from "../../config/Config";
import {GalleryCacheService} from "../cache.gallery.service";
import {Photo} from "../Photo";
import {Config} from "../config/Config";
import {GalleryCacheService} from "./cache.gallery.service";
import {Photo} from "./Photo";
import {IconPhoto} from "./IconPhoto";
import {PhotoDTO} from "../../../common/entities/PhotoDTO";
export enum ThumbnailLoadingPriority{
high, medium, low
@ -36,12 +38,46 @@ export class ThumbnailLoaderService {
}
loadIcon(photo: IconPhoto, priority: ThumbnailLoadingPriority, listener: ThumbnailLoadingListener): ThumbnailTaskEntity {
let tmp: ThumbnailTask = null;
//is image already qued?
for (let i = 0; i < this.que.length; i++) {
if (this.que[i].path == photo.getIconPath()) {
tmp = this.que[i];
break;
}
}
let thumbnailTaskEntity = {priority: priority, listener: listener};
//add to previous
if (tmp != null) {
tmp.taskEntities.push(thumbnailTaskEntity);
if (tmp.inProgress == true) {
listener.onStartedLoading();
}
} else {//create new task
this.que.push(<ThumbnailTask>{
photo: photo.photo,
inProgress: false,
taskEntities: [thumbnailTaskEntity],
onLoaded: () => {
photo.iconLoaded();
},
path: photo.getIconPath()
});
}
setImmediate(this.run);
return thumbnailTaskEntity;
}
loadImage(photo: Photo, priority: ThumbnailLoadingPriority, listener: ThumbnailLoadingListener): ThumbnailTaskEntity {
let tmp: ThumbnailTask = null;
//is image already qued?
for (let i = 0; i < this.que.length; i++) {
if (this.que[i].photo.getThumbnailPath() == photo.getThumbnailPath()) {
if (this.que[i].path == photo.getThumbnailPath()) {
tmp = this.que[i];
break;
}
@ -58,9 +94,13 @@ export class ThumbnailLoaderService {
} else {//create new task
this.que.push({
photo: photo,
photo: photo.photo,
inProgress: false,
taskEntities: [thumbnailTaskEntity]
taskEntities: [thumbnailTaskEntity],
onLoaded: () => {
photo.thumbnailLoaded();
},
path: photo.getThumbnailPath()
});
}
setImmediate(this.run);
@ -129,8 +169,8 @@ export class ThumbnailLoaderService {
let curImg = new Image();
curImg.onload = () => {
task.photo.thumbnailLoaded();
this.galleryChacheService.photoUpdated(task.photo.photo);
task.onLoaded();
this.galleryChacheService.photoUpdated(task.photo);
task.taskEntities.forEach((te: ThumbnailTaskEntity) => te.listener.onLoad());
this.taskReady(task);
@ -146,7 +186,7 @@ export class ThumbnailLoaderService {
this.run();
};
curImg.src = task.photo.getThumbnailPath();
curImg.src = task.path;
};
}
@ -164,8 +204,10 @@ export interface ThumbnailTaskEntity {
}
interface ThumbnailTask {
photo: Photo;
photo: PhotoDTO;
inProgress: boolean;
taskEntities: Array<ThumbnailTaskEntity>;
path: string;
onLoaded: Function;
}

View File

@ -1,6 +1,7 @@
import {Injectable} from "@angular/core";
import {Photo} from "../Photo";
import {ThumbnailLoaderService, ThumbnailLoadingListener, ThumbnailTaskEntity} from "./thumnailLoader.service";
import {Photo} from "./Photo";
import {IconPhoto} from "./IconPhoto";
export enum ThumbnailLoadingPriority{
high, medium, low
@ -16,21 +17,117 @@ export class ThumbnailManagerService {
public getThumbnail(photo: Photo) {
return new Thumbnail(photo, this.thumbnailLoader);
}
public getIcon(photo: IconPhoto) {
return new IconThumbnail(photo, this.thumbnailLoader);
}
}
export class Thumbnail {
export abstract class ThumbnailBase {
private available: boolean = false;
private src: string = null;
private loading: boolean = false;
private thumbnailTask: ThumbnailTaskEntity;
protected available: boolean = false;
protected src: string = null;
protected loading: boolean = false;
protected onLoad: Function = null;
protected thumbnailTask: ThumbnailTaskEntity;
constructor(private photo: Photo, private thumbnailService: ThumbnailLoaderService) {
constructor(protected thumbnailService: ThumbnailLoaderService) {
}
abstract set Visible(visible: boolean);
set OnLoad(onLoad: Function) {
this.onLoad = onLoad;
}
get Available() {
return this.available;
}
get Src() {
return this.src;
}
get Loading() {
return this.loading;
}
destroy() {
if (this.thumbnailTask != null) {
this.thumbnailService.removeTask(this.thumbnailTask);
this.thumbnailTask = null;
}
}
}
export class IconThumbnail extends ThumbnailBase {
constructor(private photo: IconPhoto, thumbnailService: ThumbnailLoaderService) {
super(thumbnailService);
this.src = "";
if (this.photo.isIconAvailable()) {
this.src = this.photo.getIconPath();
this.available = true;
if (this.onLoad) this.onLoad();
}
if (!this.photo.isIconAvailable()) {
setImmediate(() => {
let listener: ThumbnailLoadingListener = {
onStartedLoading: () => { //onLoadStarted
this.loading = true;
},
onLoad: () => {//onLoaded
this.src = this.photo.getIconPath();
if (this.onLoad) this.onLoad();
this.available = true;
this.loading = false;
this.thumbnailTask = null;
},
onError: (error) => {//onError
this.thumbnailTask = null;
//TODO: handle error
//TODO: not an error if its from cache
console.error("something bad happened");
console.error(error);
}
};
this.thumbnailTask = this.thumbnailService.loadIcon(this.photo, ThumbnailLoadingPriority.high, listener);
});
}
}
set Visible(visible: boolean) {
if (!this.thumbnailTask) return;
if (visible === true) {
this.thumbnailTask.priority = ThumbnailLoadingPriority.high;
} else {
this.thumbnailTask.priority = ThumbnailLoadingPriority.medium;
}
}
}
export class Thumbnail extends ThumbnailBase {
constructor(private photo: Photo, thumbnailService: ThumbnailLoaderService) {
super(thumbnailService);
if (this.photo.isThumbnailAvailable()) {
this.src = this.photo.getThumbnailPath();
this.available = true;
if (this.onLoad) this.onLoad();
} else if (this.photo.isReplacementThumbnailAvailable()) {
this.src = this.photo.getReplacementThumbnailPath();
this.available = true;
@ -45,6 +142,7 @@ export class Thumbnail {
},
onLoad: () => {//onLoaded
this.src = this.photo.getThumbnailPath();
if (this.onLoad) this.onLoad();
this.available = true;
this.loading = false;
this.thumbnailTask = null;
@ -70,6 +168,7 @@ export class Thumbnail {
}
set Visible(visible: boolean) {
if (!this.thumbnailTask) return;
if (visible === true) {
if (this.photo.isReplacementThumbnailAvailable()) {
this.thumbnailTask.priority = ThumbnailLoadingPriority.medium;
@ -86,24 +185,5 @@ export class Thumbnail {
}
get Available() {
return this.available;
}
get Src() {
return this.src;
}
get Loading() {
return this.loading;
}
destroy() {
if (this.thumbnailTask != null) {
this.thumbnailService.removeTask(this.thumbnailTask);
this.thumbnailTask = null;
}
}
}