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 CatalogTermContext {
116 fieldClass: string[];
122 ccvmFilters: {[ccvmCode: string]: string[]};
123 facetFilters: FacetFilter[];
124 copyLocations: string[]; // ID's, but treated as strings in the UI.
126 // True when searching for metarecords
127 groupByMetarecord: boolean;
129 // Filter results by records which link to this metarecord ID.
130 fromMetarecord: number;
132 hasBrowseEntry: string; // "entryId,fieldId"
133 browseEntry: IdlObject;
136 dateOp: string; // before, after, between, is
140 this.fieldClass = ['keyword'];
141 this.matchOp = ['contains'];
143 this.facetFilters = [];
144 this.copyLocations = [''];
146 this.hasBrowseEntry = '';
150 this.fromMetarecord = null;
152 // Apply empty string values for each ccvm filter
153 this.ccvmFilters = {};
154 CATALOG_CCVM_FILTERS.forEach(code => this.ccvmFilters[code] = ['']);
157 // True when grouping by metarecord but not when displaying the
158 // contents of a metarecord.
159 isMetarecordSearch(): boolean {
161 this.isSearchable() &&
162 this.groupByMetarecord &&
163 this.fromMetarecord === null
167 isSearchable(): boolean {
170 || this.hasBrowseEntry !== ''
171 || this.fromMetarecord !== null
175 hasFacet(facet: FacetFilter): boolean {
177 this.facetFilters.filter(f => f.equals(facet))[0]
181 removeFacet(facet: FacetFilter): void {
182 this.facetFilters = this.facetFilters.filter(f => !f.equals(facet));
185 addFacet(facet: FacetFilter): void {
186 if (!this.hasFacet(facet)) {
187 this.facetFilters.push(facet);
191 toggleFacet(facet: FacetFilter): void {
192 if (this.hasFacet(facet)) {
193 this.removeFacet(facet);
195 this.facetFilters.push(facet);
202 // Not an angular service.
203 // It's conceviable there could be multiple contexts.
204 export class CatalogSearchContext {
206 // Attributes that are used across different contexts.
210 searchOrg: IdlObject;
213 termSearch: CatalogTermContext;
214 marcSearch: CatalogMarcContext;
215 identSearch: CatalogIdentContext;
216 browseSearch: CatalogBrowseContext;
218 // Result from most recent search.
219 result: CatalogSearchResults;
220 searchState: CatalogSearchState = CatalogSearchState.PENDING;
222 // List of IDs in page/offset context.
230 this.pager = new Pager();
231 this.termSearch = new CatalogTermContext();
232 this.marcSearch = new CatalogMarcContext();
233 this.identSearch = new CatalogIdentContext();
234 this.browseSearch = new CatalogBrowseContext();
239 * Return search context to its default state, resetting search
240 * parameters and clearing any cached result data.
243 this.pager.offset = 0;
245 this.showBasket = false;
246 this.result = new CatalogSearchResults();
248 this.searchState = CatalogSearchState.PENDING;
249 this.termSearch.reset();
250 this.marcSearch.reset();
251 this.identSearch.reset();
252 this.browseSearch.reset();
255 isSearchable(): boolean {
258 this.termSearch.isSearchable() ||
259 this.marcSearch.isSearchable() ||
260 this.identSearch.isSearchable() ||
261 this.browseSearch.isSearchable()
265 // List of result IDs for the current page of data.
266 currentResultIds(): number[] {
268 const max = Math.min(
269 this.pager.offset + this.pager.limit,
270 this.pager.resultCount
272 for (let idx = this.pager.offset; idx < max; idx++) {
273 ids.push(this.resultIds[idx]);
278 addResultId(id: number, resultIdx: number ): void {
279 this.resultIds[resultIdx + this.pager.offset] = id;
282 // Return the record at the requested index.
283 resultIdAt(index: number): number {
284 return this.resultIds[index] || null;
287 // Return the index of the requested record
288 indexForResult(id: number): number {
289 for (let i = 0; i < this.resultIds.length; i++) {
290 if (this.resultIds[i] === id) {
297 compileMarcSearchArgs(): any {
298 const searches: any = [];
299 const ms = this.marcSearch;
301 ms.values.forEach((val, idx) => {
305 // "_" is the wildcard subfield for the API.
306 subfield: ms.subfields[idx] ? ms.subfields[idx] : '_',
316 limit : this.pager.limit,
317 offset : this.pager.offset,
318 org_unit: this.searchOrg.id()
322 const parts = this.sort.split(/\./);
323 args.sort = parts[0]; // title, author, etc.
324 if (parts[1]) { args.sort_dir = 'descending' };
330 compileIdentSearchQuery(): string {
332 let str = ' site(' + this.searchOrg.shortname() + ')';
334 this.identSearch.queryType + ':' + this.identSearch.value;
338 compileBoolQuerySet(idx: number): string {
339 const ts = this.termSearch;
340 let query = ts.query[idx];
341 const joinOp = ts.joinOp[idx];
342 const matchOp = ts.matchOp[idx];
343 const fieldClass = ts.fieldClass[idx];
346 if (!query) { return str; }
348 if (idx > 0) { str += ' ' + joinOp + ' '; }
351 if (fieldClass) { str += fieldClass + ':'; }
355 query = this.addQuotes(this.stripQuotes(query));
358 query = '-' + this.addQuotes(this.stripQuotes(query));
361 query = '^' + this.stripAnchors(query) + '$';
364 query = this.addQuotes('^' +
365 this.stripAnchors(this.stripQuotes(query)));
369 return str + query + ')';
372 stripQuotes(query: string): string {
373 return query.replace(/"/g, '');
376 stripAnchors(query: string): string {
377 return query.replace(/[\^\$]/g, '');
380 addQuotes(query: string): string {
381 if (query.match(/ /)) {
382 return '"' + query + '"';
387 compileTermSearchQuery(): string {
388 const ts = this.termSearch;
396 // e.g. title, title.descending
397 const parts = this.sort.split(/\./);
398 if (parts[1]) { str += ' #descending'; }
399 str += ' sort(' + parts[0] + ')';
402 if (ts.date1 && ts.dateOp) {
405 str += ` date1(${ts.date1})`;
408 str += ` before(${ts.date1})`;
411 str += ` after(${ts.date1})`;
415 str += ` between(${ts.date1},${ts.date2})`;
421 // Compile boolean sub-query components
422 if (str.length) { str += ' '; }
423 const qcount = ts.query.length;
425 // if we multiple boolean query components, wrap them in parens.
426 if (qcount > 1) { str += '('; }
427 ts.query.forEach((q, idx) => {
428 str += this.compileBoolQuerySet(idx);
430 if (qcount > 1) { str += ')'; }
433 if (ts.hasBrowseEntry) {
434 // stored as a comma-separated string of "entryId,fieldId"
435 str += ` has_browse_entry(${ts.hasBrowseEntry})`;
438 if (ts.fromMetarecord) {
439 str += ` from_metarecord(${ts.fromMetarecord})`;
443 str += ' format(' + ts.format + ')';
448 this.org.root().ou_type().depth() + ')';
451 if (ts.copyLocations[0] !== '') {
452 str += ' locations(' + ts.copyLocations + ')';
455 str += ' site(' + this.searchOrg.shortname() + ')';
457 Object.keys(ts.ccvmFilters).forEach(field => {
458 if (ts.ccvmFilters[field][0] !== '') {
459 str += ' ' + field + '(' + ts.ccvmFilters[field] + ')';
463 ts.facetFilters.forEach(f => {
464 str += ' ' + f.facetClass + '|'
465 + f.facetName + '[' + f.facetValue + ']';