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

Implementing distance search #58

This commit is contained in:
Patrik J. Braun 2021-04-06 11:32:31 +02:00
parent cbfbebf697
commit 0f7ac812ea
17 changed files with 235 additions and 63 deletions

61
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "pigallery2", "name": "pigallery2",
"version": "1.8.2", "version": "1.8.4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -3344,6 +3344,15 @@
"integrity": "sha512-4nhBPStMK04rruRVtVc6cDqhu7S9GZai0fpXgPXrFpcPX6Xul8xnrjSdGB4KPBVYG/R5+fXWdCM8qBoiULWGPQ==", "integrity": "sha512-4nhBPStMK04rruRVtVc6cDqhu7S9GZai0fpXgPXrFpcPX6Xul8xnrjSdGB4KPBVYG/R5+fXWdCM8qBoiULWGPQ==",
"dev": true "dev": true
}, },
"@types/node-geocoder": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/@types/node-geocoder/-/node-geocoder-3.24.1.tgz",
"integrity": "sha512-NpkCX6ooS3Fc6fjVVCWUDcq7/0siDU6zVu+Q29neHLXOGkRc9VmLvLTl3yeaDRatdpAjkD9O7hOY67zrf7WP9w==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/q": { "@types/q": {
"version": "0.0.32", "version": "0.0.32",
"resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz",
@ -4702,8 +4711,7 @@
"bluebird": { "bluebird": {
"version": "3.5.3", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz",
"integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==", "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw=="
"dev": true
}, },
"bmp-js": { "bmp-js": {
"version": "0.1.0", "version": "0.1.0",
@ -13607,6 +13615,11 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.0.tgz", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.0.tgz",
"integrity": "sha512-sSHCgWfJ+Lui/u+0msF3oyCgvdkhxDbkCS6Q8uiJquzOimkJBvX6hl5aSSA7DR1XbMpdM8r7phjcF63sF4rkKg==" "integrity": "sha512-sSHCgWfJ+Lui/u+0msF3oyCgvdkhxDbkCS6Q8uiJquzOimkJBvX6hl5aSSA7DR1XbMpdM8r7phjcF63sF4rkKg=="
}, },
"node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
"node-fetch-npm": { "node-fetch-npm": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz", "resolved": "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz",
@ -13624,6 +13637,17 @@
"integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==", "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==",
"dev": true "dev": true
}, },
"node-geocoder": {
"version": "3.27.0",
"resolved": "https://registry.npmjs.org/node-geocoder/-/node-geocoder-3.27.0.tgz",
"integrity": "sha512-fNMi9smx56wFhG+2sd0qVsq5RgNlkUuQQ7UWqPwynoMb0GjxSP9joAn8wah4YDv6UzZu3b41cNmd0BglEPkC+Q==",
"requires": {
"bluebird": "^3.5.2",
"node-fetch": "^2.6.0",
"request": "^2.88.0",
"request-promise": "^4.2.2"
}
},
"node-gyp": { "node-gyp": {
"version": "3.8.0", "version": "3.8.0",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz",
@ -16502,6 +16526,32 @@
} }
} }
}, },
"request-promise": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz",
"integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==",
"requires": {
"bluebird": "^3.5.0",
"request-promise-core": "1.1.4",
"stealthy-require": "^1.1.1",
"tough-cookie": "^2.3.3"
}
},
"request-promise-core": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz",
"integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==",
"requires": {
"lodash": "^4.17.19"
},
"dependencies": {
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}
}
},
"require-directory": { "require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -17840,6 +17890,11 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
}, },
"stealthy-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
},
"stream-browserify": { "stream-browserify": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",

View File

@ -43,6 +43,7 @@
"image-size": "0.9.3", "image-size": "0.9.3",
"jimp": "0.16.1", "jimp": "0.16.1",
"locale": "0.1.0", "locale": "0.1.0",
"node-geocoder": "^3.27.0",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"sharp": "0.23.4", "sharp": "0.23.4",
"sqlite3": "5.0.0", "sqlite3": "5.0.0",
@ -84,6 +85,7 @@
"@types/jasmine": "3.6.2", "@types/jasmine": "3.6.2",
"@types/jsonwebtoken": "8.5.0", "@types/jsonwebtoken": "8.5.0",
"@types/node": "14.14.19", "@types/node": "14.14.19",
"@types/node-geocoder": "^3.24.1",
"@types/sharp": "0.26.1", "@types/sharp": "0.26.1",
"@types/winston": "2.4.4", "@types/winston": "2.4.4",
"@types/xml2js": "0.4.7", "@types/xml2js": "0.4.7",

View File

@ -0,0 +1,6 @@
export class LocationLookupException extends Error {
constructor(message: string, public location: string) {
super(message);
}
}

View File

@ -15,6 +15,7 @@ import {Utils} from '../../common/Utils';
import {QueryParams} from '../../common/QueryParams'; import {QueryParams} from '../../common/QueryParams';
import {VideoProcessing} from '../model/fileprocessing/VideoProcessing'; import {VideoProcessing} from '../model/fileprocessing/VideoProcessing';
import {SearchQueryDTO, SearchQueryTypes} from '../../common/entities/SearchQueryDTO'; import {SearchQueryDTO, SearchQueryTypes} from '../../common/entities/SearchQueryDTO';
import {LocationLookupException} from '../exceptions/LocationLookupException';
export class GalleryMWs { export class GalleryMWs {
@ -167,6 +168,9 @@ export class GalleryMWs {
req.resultPipe = new ContentWrapper(null, result); req.resultPipe = new ContentWrapper(null, result);
return next(); return next();
} catch (err) { } catch (err) {
if (err instanceof LocationLookupException) {
return next(new ErrorDTO(ErrorCodes.LocationLookUp_ERROR, 'Cannot find location: ' + err.location, err));
}
return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'Error during searching', err)); return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'Error during searching', err));
} }
} }

View File

@ -138,7 +138,6 @@ export class ThumbnailGeneratorMWs {
private static addThInfoTODir(directory: DirectoryDTO) { private static addThInfoTODir(directory: DirectoryDTO) {
console.log(directory.path, directory.name);
if (typeof directory.media !== 'undefined') { if (typeof directory.media !== 'undefined') {
ThumbnailGeneratorMWs.addThInfoToPhotos(directory.media); ThumbnailGeneratorMWs.addThInfoToPhotos(directory.media);
} }

View File

@ -1,10 +1,30 @@
import {GPSMetadata} from '../../../common/entities/PhotoDTO'; import {GPSMetadata} from '../../../common/entities/PhotoDTO';
import * as NodeGeocoder from 'node-geocoder';
import {LocationLookupException} from '../../exceptions/LocationLookupException';
import {Utils} from '../../../common/Utils';
export class LocationManager { export class LocationManager {
readonly geocoder: NodeGeocoder.Geocoder;
cache = new Utils.LRU<GPSMetadata>(100);
constructor() {
this.geocoder = NodeGeocoder({provider: 'openstreetmap'});
}
async getGPSData(text: string): Promise<GPSMetadata> { async getGPSData(text: string): Promise<GPSMetadata> {
throw new Error('TODO implement'); if (!this.cache.get(text)) {
const ret = await this.geocoder.geocode(text);
if (ret.length < 1) {
throw new LocationLookupException('Cannot find location:' + text, text);
}
this.cache.set(text, {
latitude: ret[0].latitude,
longitude: ret[0].longitude
});
}
return this.cache.get(text);
} }
} }

View File

@ -236,7 +236,6 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
if (dir.preview) { if (dir.preview) {
dir.preview.readyThumbnails = []; dir.preview.readyThumbnails = [];
dir.preview.readyIcon = false; dir.preview.readyIcon = false;
console.log(dir.preview.directory);
} }
} }

View File

@ -84,7 +84,7 @@ export class SearchManager implements ISearchManager {
.map(r => r.name), SearchQueryTypes.person)); .map(r => r.name), SearchQueryTypes.person));
} }
if (type === SearchQueryTypes.any_text || type === SearchQueryTypes.position) { if (type === SearchQueryTypes.any_text || type === SearchQueryTypes.position || type === SearchQueryTypes.distance) {
(await photoRepository (await photoRepository
.createQueryBuilder('photo') .createQueryBuilder('photo')
.select('photo.metadata.positionData.country as country, ' + .select('photo.metadata.positionData.country as country, ' +
@ -99,7 +99,8 @@ export class SearchManager implements ISearchManager {
.map(pm => <Array<string>>[pm.city || '', pm.country || '', pm.state || '']) .map(pm => <Array<string>>[pm.city || '', pm.country || '', pm.state || ''])
.forEach(positions => { .forEach(positions => {
result = result.concat(this.encapsulateAutoComplete(positions result = result.concat(this.encapsulateAutoComplete(positions
.filter(p => p.toLowerCase().indexOf(text.toLowerCase()) !== -1), SearchQueryTypes.position)); .filter(p => p.toLowerCase().indexOf(text.toLowerCase()) !== -1),
type === SearchQueryTypes.distance ? type : SearchQueryTypes.position));
}); });
} }
@ -241,12 +242,18 @@ export class SearchManager implements ISearchManager {
latDelta = (1 / ((2 * Math.PI / 360) * earth)), // 1 km in degree latDelta = (1 / ((2 * Math.PI / 360) * earth)), // 1 km in degree
lonDelta = (1 / ((2 * Math.PI / 360) * earth)); // 1 km in degree lonDelta = (1 / ((2 * Math.PI / 360) * earth)); // 1 km in degree
const minLat = (<DistanceSearch>query).from.GPSData.latitude - ((<DistanceSearch>query).distance * latDelta), // TODO: properly handle latitude / longitude boundaries
maxLat = (<DistanceSearch>query).from.GPSData.latitude + ((<DistanceSearch>query).distance * latDelta), const trimRange = (value: number, min: number, max: number) => {
minLon = (<DistanceSearch>query).from.GPSData.latitude - return Math.min(Math.max(value, min), max);
((<DistanceSearch>query).distance * lonDelta) / Math.cos(minLat * (Math.PI / 180)), };
maxLon = (<DistanceSearch>query).from.GPSData.latitude +
((<DistanceSearch>query).distance * lonDelta) / Math.cos(maxLat * (Math.PI / 180)); const minLat = trimRange((<DistanceSearch>query).from.GPSData.latitude - ((<DistanceSearch>query).distance * latDelta), -90, 90),
maxLat = trimRange((<DistanceSearch>query).from.GPSData.latitude + ((<DistanceSearch>query).distance * latDelta), -90, 90),
minLon = trimRange((<DistanceSearch>query).from.GPSData.longitude -
((<DistanceSearch>query).distance * lonDelta) / Math.cos(minLat * (Math.PI / 180)), -180, 180),
maxLon = trimRange((<DistanceSearch>query).from.GPSData.longitude +
((<DistanceSearch>query).distance * lonDelta) / Math.cos(maxLat * (Math.PI / 180)), -180, 180);
return new Brackets(q => { return new Brackets(q => {
const textParam: any = {}; const textParam: any = {};

View File

@ -146,7 +146,7 @@ export class SearchQueryParser {
this.keywords.someOf + ':' : this.keywords.someOf + ':' :
new RegExp('^\\d*-' + this.keywords.NSomeOf + ':').exec(str)[0]; new RegExp('^\\d*-' + this.keywords.NSomeOf + ':').exec(str)[0];
let tmpList: any = this.parse(str.slice(prefix.length + 1, -1), false); // trim brackets let tmpList: any = this.parse(str.slice(prefix.length + 1, -1), false); // trim brackets
// console.log(JSON.stringify(tmpList, null, 4));
const unfoldList = (q: SearchListQuery): SearchQueryDTO[] => { const unfoldList = (q: SearchListQuery): SearchQueryDTO[] => {
if (q.list) { if (q.list) {
if (q.type === SearchQueryTypes.UNKNOWN_RELATION) { if (q.type === SearchQueryTypes.UNKNOWN_RELATION) {
@ -207,7 +207,8 @@ export class SearchQueryParser {
} }
if (new RegExp('^\\d*-' + this.keywords.kmFrom + ':').test(str)) { if (new RegExp('^\\d*-' + this.keywords.kmFrom + ':').test(str)) {
let from = str.slice(new RegExp('^\\d*-' + this.keywords.kmFrom + ':').exec(str)[0].length); let from = str.slice(new RegExp('^\\d*-' + this.keywords.kmFrom + ':').exec(str)[0].length);
if (from.charAt(0) === '(' && from.charAt(from.length - 1) === ')') { if ((from.charAt(0) === '(' && from.charAt(from.length - 1) === ')') ||
(from.charAt(0) === '"' && from.charAt(from.length - 1) === '"')) {
from = from.slice(1, from.length - 1); from = from.slice(1, from.length - 1);
} }
return <DistanceSearch>{ return <DistanceSearch>{

View File

@ -268,3 +268,35 @@ export class Utils {
} }
} }
export namespace Utils {
export class LRU<V> {
data: { [key: string]: { value: V, usage: number } } = {};
constructor(public readonly size: number) {
}
set(key: string, value: V): void {
this.data[key] = {usage: Date.now(), value: value};
if (Object.keys(this.data).length > this.size) {
let oldestK = key;
let oldest = this.data[oldestK].usage;
for (const k in this.data) {
if (this.data[k].usage < oldest) {
oldestK = k;
oldest = this.data[oldestK].usage;
}
}
delete this.data[oldestK];
}
}
get(key: string): V {
if (!this.data[key]) {
return;
}
return this.data[key].value;
}
}
}

View File

@ -22,7 +22,8 @@ export enum ErrorCodes {
SETTINGS_ERROR = 13, SETTINGS_ERROR = 13,
TASK_ERROR = 14, TASK_ERROR = 14,
JOB_ERROR = 15 JOB_ERROR = 15,
LocationLookUp_ERROR = 16,
} }
export class ErrorDTO { export class ErrorDTO {

View File

@ -27,40 +27,55 @@
</ng-container> </ng-container>
<div body class="container-fluid" style="width: 100%; padding:0" *ngIf="_galleryService.content.value.directory"> <div body class="container-fluid" style="width: 100%; padding:0">
<app-gallery-navbar [directory]="_galleryService.content.value.directory"></app-gallery-navbar> <ng-container *ngIf="_galleryService.content.value.error">
<div class="alert alert-danger" role="alert">
{{_galleryService.content.value.error}}
</div>
</ng-container>
<ng-container *ngIf="!_galleryService.content.value.error">
<ng-container *ngIf="_galleryService.content.value.directory">
<app-gallery-directories class="directories" [directories]="directories"></app-gallery-directories> <app-gallery-navbar [directory]="_galleryService.content.value.directory"></app-gallery-navbar>
<app-gallery-map *ngIf="isPhotoWithLocation && mapEnabled" <app-gallery-directories class="directories" [directories]="directories"></app-gallery-directories>
[photos]="_galleryService.content.value.directory.media"
[gpxFiles]="_galleryService.content.value.directory.metaFile | gpxFiles"></app-gallery-map>
<app-gallery-grid [media]="_galleryService.content.value.directory.media"
[lightbox]="lightbox"></app-gallery-grid>
</div>
<!-- Search-->
<div body class="container-fluid" style="width: 100%; padding:0" *ngIf="_galleryService.content.value.searchResult">
<div class="alert alert-info" role="alert"
*ngIf="_galleryService.content.value.searchResult.resultOverflow == true" i18n>
Too many results to show. Refine your search.
</div>
<app-gallery-navbar [searchResult]="_galleryService.content.value.searchResult"></app-gallery-navbar>
<app-gallery-map *ngIf="isPhotoWithLocation && mapEnabled" <app-gallery-map *ngIf="isPhotoWithLocation && mapEnabled"
[photos]="_galleryService.content.value.searchResult.media" [photos]="_galleryService.content.value.directory.media"
[gpxFiles]="_galleryService.content.value.searchResult.metaFile | gpxFiles"></app-gallery-map> [gpxFiles]="_galleryService.content.value.directory.metaFile | gpxFiles"></app-gallery-map>
<app-gallery-grid [media]="_galleryService.content.value.directory.media"
[lightbox]="lightbox"></app-gallery-grid>
<app-gallery-directories class="directories" [directories]="directories"></app-gallery-directories>
<app-gallery-grid [media]="_galleryService.content.value.searchResult.media" </ng-container>
[lightbox]="lightbox"></app-gallery-grid> <!-- Search-->
<ng-container *ngIf="_galleryService.content.value.searchResult">
<div class="alert alert-info" role="alert"
*ngIf="_galleryService.content.value.searchResult.resultOverflow == true" i18n>
Too many results to show. Refine your search.
</div>
<app-gallery-navbar [searchResult]="_galleryService.content.value.searchResult"></app-gallery-navbar>
<app-gallery-map *ngIf="isPhotoWithLocation && mapEnabled"
[photos]="_galleryService.content.value.searchResult.media"
[gpxFiles]="_galleryService.content.value.searchResult.metaFile | gpxFiles"></app-gallery-map>
<app-gallery-directories class="directories" [directories]="directories"></app-gallery-directories>
<app-gallery-grid [media]="_galleryService.content.value.searchResult.media"
[lightbox]="lightbox"></app-gallery-grid>
</ng-container>
</ng-container>
</div> </div>
<div body class="container" <div body class="container"
style="width: 100%; padding:0" style="width: 100%; padding:0"
*ngIf="(!_galleryService.content.value.directory || *ngIf="(!_galleryService.content.value.directory ||
_galleryService.content.value.directory.isPartial == true) _galleryService.content.value.directory.isPartial == true)
&& !_galleryService.content.value.searchResult"> && !_galleryService.content.value.searchResult
&& !_galleryService.content.value.error">
<div class="spinner"> <div class="spinner">
</div> </div>

View File

@ -11,12 +11,13 @@ import {SortingMethods} from '../../../../common/entities/SortingMethods';
import {QueryParams} from '../../../../common/QueryParams'; import {QueryParams} from '../../../../common/QueryParams';
import {PG2ConfMap} from '../../../../common/PG2ConfMap'; import {PG2ConfMap} from '../../../../common/PG2ConfMap';
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO'; import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
import {ErrorCodes} from '../../../../common/entities/Error';
@Injectable() @Injectable()
export class GalleryService { export class GalleryService {
public content: BehaviorSubject<ContentWrapper>; public content: BehaviorSubject<ContentWrapperWithError>;
public sorting: BehaviorSubject<SortingMethods>; public sorting: BehaviorSubject<SortingMethods>;
lastRequest: { directory: string } = { lastRequest: { directory: string } = {
directory: null directory: null
@ -29,7 +30,7 @@ export class GalleryService {
private galleryCacheService: GalleryCacheService, private galleryCacheService: GalleryCacheService,
private _shareService: ShareService, private _shareService: ShareService,
private navigationService: NavigationService) { private navigationService: NavigationService) {
this.content = new BehaviorSubject<ContentWrapper>(new ContentWrapper()); this.content = new BehaviorSubject<ContentWrapperWithError>(new ContentWrapperWithError());
this.sorting = new BehaviorSubject<SortingMethods>(Config.Client.Other.defaultPhotoSortingMethod); this.sorting = new BehaviorSubject<SortingMethods>(Config.Client.Other.defaultPhotoSortingMethod);
} }
@ -56,7 +57,7 @@ export class GalleryService {
} }
setContent(content: ContentWrapper): void { setContent(content: ContentWrapperWithError): void {
this.content.next(content); this.content.next(content);
if (content.directory) { if (content.directory) {
const sort = this.galleryCacheService.getSorting(content.directory); const sort = this.galleryCacheService.getSorting(content.directory);
@ -70,7 +71,7 @@ export class GalleryService {
public async loadDirectory(directoryName: string): Promise<void> { public async loadDirectory(directoryName: string): Promise<void> {
const content = new ContentWrapper(); const content = new ContentWrapperWithError();
content.directory = this.galleryCacheService.getDirectory(directoryName); content.directory = this.galleryCacheService.getDirectory(directoryName);
content.searchResult = null; content.searchResult = null;
@ -93,7 +94,7 @@ export class GalleryService {
} }
try { try {
const cw = await this.networkService.getJson<ContentWrapper>('/gallery/content/' + directoryName, params); const cw = await this.networkService.getJson<ContentWrapperWithError>('/gallery/content/' + directoryName, params);
if (!cw || cw.notModified === true) { if (!cw || cw.notModified === true) {
@ -124,12 +125,20 @@ export class GalleryService {
this.ongoingSearch = query; this.ongoingSearch = query;
this.setContent(new ContentWrapper()); this.setContent(new ContentWrapperWithError());
const cw = new ContentWrapper(); const cw = new ContentWrapperWithError();
cw.searchResult = this.galleryCacheService.getSearch(query); cw.searchResult = this.galleryCacheService.getSearch(query);
if (cw.searchResult == null) { if (cw.searchResult == null) {
cw.searchResult = (await this.networkService.getJson<ContentWrapper>('/search/' + query)).searchResult; try {
this.galleryCacheService.setSearch(query, cw.searchResult); cw.searchResult = (await this.networkService.getJson<ContentWrapper>('/search/' + query)).searchResult;
this.galleryCacheService.setSearch(query, cw.searchResult);
} catch (e) {
if (e.code === ErrorCodes.LocationLookUp_ERROR) {
cw.error = 'Cannot find location: ' + e.message;
} else {
throw e;
}
}
} }
if (this.ongoingSearch !== query) { if (this.ongoingSearch !== query) {
@ -146,3 +155,8 @@ export class GalleryService {
} }
export class ContentWrapperWithError extends ContentWrapper {
public error: string;
}

View File

@ -12,7 +12,6 @@ import {SearchQueryParser} from '../../../../../common/SearchQueryParser';
export class AutoCompleteService { export class AutoCompleteService {
private keywords: string[] = []; private keywords: string[] = [];
private relationKeywords: string[] = [];
private textSearchKeywordsMap: { [key: string]: SearchQueryTypes } = {}; private textSearchKeywordsMap: { [key: string]: SearchQueryTypes } = {};
constructor(private _networkService: NetworkService, constructor(private _networkService: NetworkService,
@ -29,7 +28,11 @@ export class AutoCompleteService {
this.keywords.push(this._searchQueryParserService.keywords.and); this.keywords.push(this._searchQueryParserService.keywords.and);
this.keywords.push(this._searchQueryParserService.keywords.or); this.keywords.push(this._searchQueryParserService.keywords.or);
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
this.keywords.push(i + this._searchQueryParserService.keywords.NSomeOf); this.keywords.push(i + '-' + this._searchQueryParserService.keywords.NSomeOf + ':( )');
}
for (let i = 0; i < 10; i++) {
this.keywords.push(i + '-' + this._searchQueryParserService.keywords.kmFrom + ':');
} }
this.keywords.push(this._searchQueryParserService.keywords.to + ':' + this.keywords.push(this._searchQueryParserService.keywords.to + ':' +
@ -55,11 +58,11 @@ export class AutoCompleteService {
if (searchText === '') { if (searchText === '') {
return items; return items;
} }
this.typedAutoComplete(searchText, type, items); this.typedAutoComplete(searchText, text.current, type, items);
return items; return items;
} }
public typedAutoComplete(text: string, type: SearchQueryTypes, public typedAutoComplete(text: string, fullText: string, type: SearchQueryTypes,
items?: BehaviorSubject<RenderableAutoCompleteItem[]>): BehaviorSubject<RenderableAutoCompleteItem[]> { items?: BehaviorSubject<RenderableAutoCompleteItem[]>): BehaviorSubject<RenderableAutoCompleteItem[]> {
items = items || new BehaviorSubject([]); items = items || new BehaviorSubject([]);
@ -71,10 +74,10 @@ export class AutoCompleteService {
} }
this._networkService.getJson<IAutoCompleteItem[]>('/autocomplete/' + text, acParams).then(ret => { this._networkService.getJson<IAutoCompleteItem[]>('/autocomplete/' + text, acParams).then(ret => {
this._galleryCacheService.setAutoComplete(text, type, ret); this._galleryCacheService.setAutoComplete(text, type, ret);
items.next(this.sortResults(text, ret.map(i => this.ACItemToRenderable(i)).concat(items.value))); items.next(this.sortResults(text, ret.map(i => this.ACItemToRenderable(i, fullText)).concat(items.value)));
}); });
} else { } else {
items.next(this.sortResults(text, cached.map(i => this.ACItemToRenderable(i)).concat(items.value))); items.next(this.sortResults(text, cached.map(i => this.ACItemToRenderable(i, fullText)).concat(items.value)));
} }
return items; return items;
} }
@ -91,23 +94,37 @@ export class AutoCompleteService {
return tokens[1]; return tokens[1];
} }
private getTypeFromPrefix(text: string): SearchQueryTypes { private getTypeFromPrefix(text: string): SearchQueryTypes {
const tokens = text.split(':'); const tokens = text.split(':');
if (tokens.length !== 2) { if (tokens.length !== 2) {
return null; return null;
} }
if (new RegExp('^\\d*-' + this._searchQueryParserService.keywords.kmFrom).test(tokens[0])) {
return SearchQueryTypes.distance;
}
return this.textSearchKeywordsMap[tokens[0]] || null; return this.textSearchKeywordsMap[tokens[0]] || null;
} }
private ACItemToRenderable(item: IAutoCompleteItem): RenderableAutoCompleteItem { private ACItemToRenderable(item: IAutoCompleteItem, searchToken: string): RenderableAutoCompleteItem {
if (!item.type) { if (!item.type) {
return {text: item.text, queryHint: item.text}; return {text: item.text, queryHint: item.text};
} }
if (TextSearchQueryTypes.includes(item.type) && item.type !== SearchQueryTypes.any_text) { if ((TextSearchQueryTypes.includes(item.type) ||
item.type === SearchQueryTypes.distance) &&
item.type !== SearchQueryTypes.any_text) {
let queryHint = (<any>this._searchQueryParserService.keywords)[SearchQueryTypes[item.type]] + ':"' + item.text + '"';
// if its a distance search, change hint text
const tokens = searchToken.split(':');
if (tokens.length === 2 &&
new RegExp('^\\d*-' + this._searchQueryParserService.keywords.kmFrom).test(tokens[0])) {
queryHint = tokens[0] + ':"' + item.text + '"';
}
return { return {
text: item.text, type: item.type, text: item.text, type: item.type,
queryHint: queryHint: queryHint
(<any>this._searchQueryParserService.keywords)[SearchQueryTypes[item.type]] + ':"' + item.text + '"'
}; };
} }
return { return {

View File

@ -34,7 +34,7 @@
<div class="autocomplete-list" *ngIf="autoCompleteRenders.length > 0" <div class="autocomplete-list" *ngIf="autoCompleteRenders.length > 0"
(mouseover)="setMouseOverAutoComplete(true)" (mouseout)="setMouseOverAutoComplete(false)"> (mouseover)="setMouseOverAutoComplete(true)" (mouseout)="setMouseOverAutoComplete(false)">
<div class="autocomplete-item" <div class="autocomplete-item"
[ngClass]="{'autocomplete-item-selected':highlightedAutoCompleteItem == i}" [ngClass]="{'autocomplete-item-selected': highlightedAutoCompleteItem === i}"
(mouseover)="setMouseOverAutoCompleteItem(i)" (mouseover)="setMouseOverAutoCompleteItem(i)"
(click)="searchAutoComplete(item)" (click)="searchAutoComplete(item)"
*ngFor="let item of autoCompleteRenders; let i = index"> *ngFor="let item of autoCompleteRenders; let i = index">
@ -46,6 +46,7 @@
<span *ngSwitchCase="SearchQueryTypes.keyword" class="oi oi-tag"></span> <span *ngSwitchCase="SearchQueryTypes.keyword" class="oi oi-tag"></span>
<span *ngSwitchCase="SearchQueryTypes.person" class="oi oi-person"></span> <span *ngSwitchCase="SearchQueryTypes.person" class="oi oi-person"></span>
<span *ngSwitchCase="SearchQueryTypes.position" class="oi oi-map-marker"></span> <span *ngSwitchCase="SearchQueryTypes.position" class="oi oi-map-marker"></span>
<span *ngSwitchCase="SearchQueryTypes.distance" class="oi oi-map-marker"></span>
</span> </span>
{{item.preText}}<strong>{{item.highLightText}}</strong>{{item.postText}} {{item.preText}}<strong>{{item.highLightText}}</strong>{{item.postText}}
</div> </div>

View File

@ -62,7 +62,6 @@ describe('GalleryRouter', (sqlHelper: DBTestHelper) => {
expect(result.body.error).to.be.equal(null); expect(result.body.error).to.be.equal(null);
expect(result.body.result).to.not.be.equal(null); expect(result.body.result).to.not.be.equal(null);
expect(result.body.result.directory).to.not.be.equal(null); expect(result.body.result.directory).to.not.be.equal(null);
console.log(result.body.result.directory);
}); });
it('should load gallery twice (to force loading form db)', async () => { it('should load gallery twice (to force loading form db)', async () => {

View File

@ -999,7 +999,7 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => {
}); });
it('should search distance', async () => { it('should search distance', async () => {
ObjectManagers.getInstance().LocationManager = new LocationManager(); ObjectManagers.getInstance().LocationManager = new LocationManager();
const sm = new SearchManager(); const sm = new SearchManager();