1 import {OrgService} from '@eg/core/org.service';
2 import {IdlObject} from '@eg/core/idl.service';
3 import {Pager} from '@eg/share/util/pager';
4 import {Params} from '@angular/router';
6 // CCVM's we care about in a catalog context
7 // Don't fetch them all because there are a lot.
8 export const CATALOG_CCVM_FILTERS = [
21 export enum CatalogSearchState {
27 export class FacetFilter {
32 constructor(cls: string, name: string, value: string) {
33 this.facetClass = cls;
34 this.facetName = name;
35 this.facetValue = value;
38 equals(filter: FacetFilter): boolean {
40 this.facetClass === filter.facetClass &&
41 this.facetName === filter.facetName &&
42 this.facetValue === filter.facetValue
47 export class CatalogSearchResults {
58 export class CatalogBrowseContext {
66 this.fieldClass = 'title';
69 isSearchable(): boolean {
72 this.fieldClass !== ''
77 export class CatalogMarcContext {
85 this.subfields = [''];
90 this.tags[0] !== '' &&
97 export class CatalogIdentContext {
109 && this.queryType !== ''
115 export class CatalogCnBrowseContext {
117 // offset in pages from base browse term
118 // e.g. -2 means 2 pages back (alphabetically) from the original search.
127 return this.value !== '';
131 export class CatalogTermContext {
132 fieldClass: string[];
138 ccvmFilters: {[ccvmCode: string]: string[]};
139 facetFilters: FacetFilter[];
140 copyLocations: string[]; // ID's, but treated as strings in the UI.
142 // True when searching for metarecords
143 groupByMetarecord: boolean;
145 // Filter results by records which link to this metarecord ID.
146 fromMetarecord: number;
148 hasBrowseEntry: string; // "entryId,fieldId"
149 browseEntry: IdlObject;
152 dateOp: string; // before, after, between, is
156 this.fieldClass = ['keyword'];
157 this.matchOp = ['contains'];
159 this.facetFilters = [];
160 this.copyLocations = [''];
162 this.hasBrowseEntry = '';
166 this.fromMetarecord = null;
168 // Apply empty string values for each ccvm filter
169 this.ccvmFilters = {};
170 CATALOG_CCVM_FILTERS.forEach(code => this.ccvmFilters[code] = ['']);
173 // True when grouping by metarecord but not when displaying the
174 // contents of a metarecord.
175 isMetarecordSearch(): boolean {
177 this.isSearchable() &&
178 this.groupByMetarecord &&
179 this.fromMetarecord === null
183 isSearchable(): boolean {
186 || this.hasBrowseEntry !== ''
187 || this.fromMetarecord !== null
191 hasFacet(facet: FacetFilter): boolean {
193 this.facetFilters.filter(f => f.equals(facet))[0]
197 removeFacet(facet: FacetFilter): void {
198 this.facetFilters = this.facetFilters.filter(f => !f.equals(facet));
201 addFacet(facet: FacetFilter): void {
202 if (!this.hasFacet(facet)) {
203 this.facetFilters.push(facet);
207 toggleFacet(facet: FacetFilter): void {
208 if (this.hasFacet(facet)) {
209 this.removeFacet(facet);
211 this.facetFilters.push(facet);
218 // Not an angular service.
219 // It's conceviable there could be multiple contexts.
220 export class CatalogSearchContext {
222 // Attributes that are used across different contexts.
226 searchOrg: IdlObject;
229 termSearch: CatalogTermContext;
230 marcSearch: CatalogMarcContext;
231 identSearch: CatalogIdentContext;
232 browseSearch: CatalogBrowseContext;
233 cnBrowseSearch: CatalogCnBrowseContext;
235 // Result from most recent search.
236 result: CatalogSearchResults;
237 searchState: CatalogSearchState = CatalogSearchState.PENDING;
239 // List of IDs in page/offset context.
247 this.pager = new Pager();
248 this.termSearch = new CatalogTermContext();
249 this.marcSearch = new CatalogMarcContext();
250 this.identSearch = new CatalogIdentContext();
251 this.browseSearch = new CatalogBrowseContext();
252 this.cnBrowseSearch = new CatalogCnBrowseContext();
257 * Return search context to its default state, resetting search
258 * parameters and clearing any cached result data.
261 this.pager.offset = 0;
263 this.showBasket = false;
264 this.result = new CatalogSearchResults();
266 this.searchState = CatalogSearchState.PENDING;
267 this.termSearch.reset();
268 this.marcSearch.reset();
269 this.identSearch.reset();
270 this.browseSearch.reset();
273 isSearchable(): boolean {
276 this.termSearch.isSearchable() ||
277 this.marcSearch.isSearchable() ||
278 this.identSearch.isSearchable() ||
279 this.browseSearch.isSearchable()
283 // List of result IDs for the current page of data.
284 currentResultIds(): number[] {
286 const max = Math.min(
287 this.pager.offset + this.pager.limit,
288 this.pager.resultCount
290 for (let idx = this.pager.offset; idx < max; idx++) {
291 ids.push(this.resultIds[idx]);
296 addResultId(id: number, resultIdx: number ): void {
297 this.resultIds[resultIdx + this.pager.offset] = Number(id);
300 // Return the record at the requested index.
301 resultIdAt(index: number): number {
302 return this.resultIds[index] || null;
305 // Return the index of the requested record
306 indexForResult(id: number): number {
307 for (let i = 0; i < this.resultIds.length; i++) {
308 if (this.resultIds[i] === id) {
315 compileMarcSearchArgs(): any {
316 const searches: any = [];
317 const ms = this.marcSearch;
319 ms.values.forEach((val, idx) => {
323 // "_" is the wildcard subfield for the API.
324 subfield: ms.subfields[idx] ? ms.subfields[idx] : '_',
334 limit : this.pager.limit,
335 offset : this.pager.offset,
336 org_unit: this.searchOrg.id()
340 const parts = this.sort.split(/\./);
341 args.sort = parts[0]; // title, author, etc.
342 if (parts[1]) { args.sort_dir = 'descending'; }
348 compileIdentSearchQuery(): string {
349 const str = ' site(' + this.searchOrg.shortname() + ')';
351 this.identSearch.queryType + ':' + this.identSearch.value;
355 compileBoolQuerySet(idx: number): string {
356 const ts = this.termSearch;
357 let query = ts.query[idx];
358 const joinOp = ts.joinOp[idx];
359 const matchOp = ts.matchOp[idx];
360 const fieldClass = ts.fieldClass[idx];
363 if (!query) { return str; }
365 if (idx > 0) { str += ' ' + joinOp + ' '; }
368 if (fieldClass) { str += fieldClass + ':'; }
372 query = this.addQuotes(this.stripQuotes(query));
375 query = '-' + this.addQuotes(this.stripQuotes(query));
378 query = '^' + this.stripAnchors(query) + '$';
381 query = this.addQuotes('^' +
382 this.stripAnchors(this.stripQuotes(query)));
386 return str + query + ')';
389 stripQuotes(query: string): string {
390 return query.replace(/"/g, '');
393 stripAnchors(query: string): string {
394 return query.replace(/[\^\$]/g, '');
397 addQuotes(query: string): string {
398 if (query.match(/ /)) {
399 return '"' + query + '"';
404 compileTermSearchQuery(): string {
405 const ts = this.termSearch;
413 // e.g. title, title.descending
414 const parts = this.sort.split(/\./);
415 if (parts[1]) { str += ' #descending'; }
416 str += ' sort(' + parts[0] + ')';
419 if (ts.date1 && ts.dateOp) {
422 str += ` date1(${ts.date1})`;
425 str += ` before(${ts.date1})`;
428 str += ` after(${ts.date1})`;
432 str += ` between(${ts.date1},${ts.date2})`;
438 // Compile boolean sub-query components
439 if (str.length) { str += ' '; }
440 const qcount = ts.query.length;
442 // if we multiple boolean query components, wrap them in parens.
443 if (qcount > 1) { str += '('; }
444 ts.query.forEach((q, idx) => {
445 str += this.compileBoolQuerySet(idx);
447 if (qcount > 1) { str += ')'; }
450 if (ts.hasBrowseEntry) {
451 // stored as a comma-separated string of "entryId,fieldId"
452 str += ` has_browse_entry(${ts.hasBrowseEntry})`;
455 if (ts.fromMetarecord) {
456 str += ` from_metarecord(${ts.fromMetarecord})`;
460 str += ' format(' + ts.format + ')';
465 this.org.root().ou_type().depth() + ')';
468 if (ts.copyLocations[0] !== '') {
469 str += ' locations(' + ts.copyLocations + ')';
472 str += ' site(' + this.searchOrg.shortname() + ')';
474 Object.keys(ts.ccvmFilters).forEach(field => {
475 if (ts.ccvmFilters[field][0] !== '') {
476 str += ' ' + field + '(' + ts.ccvmFilters[field] + ')';
480 ts.facetFilters.forEach(f => {
481 str += ' ' + f.facetClass + '|'
482 + f.facetName + '[' + f.facetValue + ']';