/*! * SAPUI5 * Copyright (c) 2025 SAP SE or an SAP affiliate company. All rights reserved. */ import { Sina } from "../../sina/Sina"; import { AttributeMetadata } from "../../sina/AttributeMetadata"; import { AttributeType } from "../../sina/AttributeType"; import { FacetType } from "../../sina/FacetType"; import { Record, RecordService } from "./RecordService"; import { Value as RawValue } from "../../sina/types"; import { format10Power } from "./Util"; export interface FacetSet { dataSourceId: string; facets: Facet[]; } export interface Facet { id: string; label: string; type: FacetType; facetTotalCount: number; position: number; items: FacetItem[]; } export interface FacetItem { description: string; count: number; rawValueLow: RawValue; rawValueHigh: RawValue; stringValueLow: string; stringValueHigh: string; // type: "AttributeValue", } export interface FacetItemData { lowStringValue: string; lowRawValue: RawValue; highStringValue: string; highRawValue: RawValue; count: number; } export class FacetService { constructor( readonly sina: Sina, readonly recordService: RecordService ) {} createFacets(dataSourceId: string, records: Record[]): FacetSet | undefined { if (records.length === 0) { return undefined; } if (dataSourceId === "All") { return this.createDataSourceFacetSet(records); } else { const attributes = this.sina.getDataSource(dataSourceId).attributesMetadata; const facetAttributes = attributes.filter((attribute) => attribute.usage.Facet !== undefined); const facetAttributesSorted = facetAttributes.sort( (a, b) => (a.usage.Facet.displayOrder || 0) - (b.usage.Facet.displayOrder || 0) ) as Array; return this.createAttributeFacetSet(records, facetAttributesSorted, 5); } } createDataSourceFacetSet(records: Record[]): FacetSet { const facetSet = { dataSourceId: "All", facets: [], } as FacetSet; const facet = { id: "DataSource", label: "Data Sources", type: FacetType.DataSource, position: 0, items: [], } as Facet; const dataSources = this.sina.dataSources.filter((ds) => ds.type === "BusinessObject"); for (const ds of dataSources) { facet.items.push({ description: ds.label, count: this.recordService.getRecordsByDataSource(ds.id, records).length, rawValueLow: ds.id, rawValueHigh: "", stringValueLow: ds.label, stringValueHigh: "", // type: "View", } as FacetItem); } facetSet.facets.push(facet); return facetSet; } createAttributeFacetSet( records: Record[], facetAttributes: Array, top?: number ): FacetSet { if (records.length === 0) { return undefined; } const dataSourceId = records[0].dataSourceId; const facetSet = { dataSourceId: dataSourceId, facets: [], } as FacetSet; for (const attribute of facetAttributes ? facetAttributes : []) { const facet = this.createAttributeFacet(records, attribute, top); if (facet) { facetSet.facets.push(facet); } else { facetSet.facets.push(null); } } return facetSet; } createAttributeFacet(records: Record[], attribute: AttributeMetadata, top?: number): Facet | undefined { const facet = { id: attribute.id, label: attribute.label, type: FacetType.Chart, position: attribute.usage?.Facet?.displayOrder || attribute.usage?.AdvancedSearch?.displayOrder || 0, items: [], facetTotalCount: 99999, // ToDo: Fill with total count of all facet items -> see getDataForPieChart of SearchFacetPieChart.ts } as Facet; if (attribute.usage?.Facet === undefined && attribute.usage?.AdvancedSearch === undefined) { facet.items = null; facet.facetTotalCount = 0; return facet; } // facet attribute may not be found in results, facet items and counts are not available if (records[0]?.valueMap[attribute.id] === undefined) { facet.items = null; facet.facetTotalCount = 0; return facet; } let itemsData = [] as FacetItemData[]; // create attribute facet according to attribute type switch (attribute.type) { case AttributeType.String: itemsData = this.getPointItemsData(records, attribute.id, top); for (const item of itemsData) { facet.items.push({ description: item.lowStringValue, count: item.count, rawValueLow: item.lowRawValue, rawValueHigh: "", stringValueLow: item.lowStringValue, stringValueHigh: "", // type: "AttributeValue", } as FacetItem); } break; case AttributeType.Double: case AttributeType.Integer: itemsData = this.getNumberRangeItemsData(records, attribute.id, top); for (const item of itemsData) { facet.items.push({ description: item.lowStringValue + " ... " + item.highStringValue, count: item.count, rawValueLow: item.lowRawValue, rawValueHigh: item.highRawValue, stringValueLow: item.lowStringValue, stringValueHigh: item.highStringValue, // type: "AttributeValue", } as FacetItem); } break; case AttributeType.Timestamp: case AttributeType.Date: case AttributeType.Time: itemsData = this.getDateRangeItemsData(records, attribute.id, top); for (const item of itemsData) { facet.items.push({ description: item.lowStringValue, count: item.count, rawValueLow: item.lowRawValue, rawValueHigh: item.highRawValue, stringValueLow: item.lowStringValue, stringValueHigh: item.highStringValue, // type: "AttributeValue", } as FacetItem); } break; default: // case AttributeType.ImageUrl // case AttributeType.ImageBlob // case AttributeType.GeoJson // case AttributeType.Group // case AttributeType.INAV2_SearchTerms // case AttributeType.INAV2_SuggestionTerms facet.items = null; facet.facetTotalCount = 0; } return facet; } private getPointItemsData(records: Record[], attributeId: string, top?: number): FacetItemData[] { const map = new Map(); const facetItemsData: FacetItemData[] = []; // 1. create hash map of attribute value and count for (const record of records) { const value = record?.valueMap[attributeId]?.stringValue; if (map.get(value) === undefined) { map.set(value, 1); } else { map.set(value, map.get(value) + 1); } } // 2. set facet items data for (const [value, count] of map) { if (count > 0) { facetItemsData.push({ lowStringValue: value, lowRawValue: value, highStringValue: undefined, highRawValue: undefined, count: count, }); } } // 3. sort facet items data by count descending, and limit to top if (Number.isInteger(top) && top > 0) { return facetItemsData.sort((a, b) => b.count - a.count).slice(0, top); } else { return facetItemsData.sort((a, b) => b.count - a.count); } } private getNumberRangeItemsData(records: Record[], attributeId: string, top?: number): FacetItemData[] { const facetItemsData: FacetItemData[] = []; const numberOfRanges = top || 100; // 1. sort records by attribute value, records with same attribute value following each other const sortedRecords = this.recordService.sortRecords(records, attributeId, "Ascending"); // 2. get minimal and maximal attribute raw value, and range size const minValue = sortedRecords[0].valueMap[attributeId].rawValue as number; const maxValue = sortedRecords[sortedRecords.length - 1].valueMap[attributeId].rawValue as number; const min10Power = format10Power(minValue, false); const range = format10Power((maxValue - minValue) / numberOfRanges, true); // 3. calculate range items data for (let i = 0; i < numberOfRanges; i++) { const startValue = min10Power + i * range; const endValue = startValue + range; // last item end Value is max attribute value const rds = records.filter((record) => { const value = record.valueMap[attributeId].rawValue as number; return value >= startValue && value < endValue; }); if (rds.length > 0) { facetItemsData.push({ lowStringValue: startValue.toFixed(2).toString(), lowRawValue: startValue, highStringValue: endValue.toFixed(2).toString(), highRawValue: endValue, count: rds.length, }); } } // 4. limit to top, don't sort by count if (Number.isInteger(top) && top > 0) { return facetItemsData.slice(0, top); } else { return facetItemsData; } } private getDateRangeItemsData(records: Record[], attributeId: string, top?: number): FacetItemData[] { const facetItemsData: FacetItemData[] = []; // 1. define possible date ranges const now = new Date(); const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999); const futureStart = new Date(todayEnd.getTime() + 1); const last7DaysStart = new Date( new Date(todayEnd.getTime() - 6 * 24 * 60 * 60 * 1000).setHours(0, 0, 0, 0) ); const last30DaysStart = new Date( new Date(todayEnd.getTime() - 29 * 24 * 60 * 60 * 1000).setHours(0, 0, 0, 0) ); const thisYearStart = new Date(todayEnd.getFullYear(), 0, 1, 0, 0, 0, 0); const thisYearEnd = new Date(new Date(todayEnd.getFullYear() + 1, 0, 1).getTime() - 1); const lastYearStart = new Date(todayEnd.getFullYear() - 1, 0, 1, 0, 0, 0, 0); const lastYearEnd = new Date(new Date(todayEnd.getFullYear(), 0, 1).getTime() - 1); const last3YearStart = new Date(todayEnd.getFullYear() - 3, 0, 1, 0, 0, 0, 0); const last5YearStart = new Date(todayEnd.getFullYear() - 5, 0, 1, 0, 0, 0, 0); const ranges = [ { label: "Future", startValue: futureStart, endValue: new Date(8.64e15) }, // futurist date object { label: "Last 7 Days", startValue: last7DaysStart, endValue: todayEnd }, { label: "Last 30 Days", startValue: last30DaysStart, endValue: todayEnd }, { label: "This Year", startValue: thisYearStart, endValue: thisYearEnd }, { label: "Last Year", startValue: lastYearStart, endValue: lastYearEnd }, { label: "Last 3 Years", startValue: last3YearStart, endValue: todayEnd }, { label: "Last 5 Years", startValue: last5YearStart, endValue: todayEnd }, { label: "Older Than 5 Years", startValue: new Date(-8640000000000000), // earliest date object endValue: new Date(last5YearStart.getTime() - 1), }, ]; // 2. calculate range items data for (let i = 0; i < ranges.length; i++) { const startValue = ranges[i].startValue; const endValue = ranges[i].endValue; // const rds = sortedRecords.filter((record) => { const rds = records.filter((record) => { const value = record.valueMap[attributeId].rawValue as Date; if (startValue != undefined) { return value >= startValue && value <= endValue; } else { return value <= endValue; } }); if (rds.length > 0) { facetItemsData.push({ lowStringValue: ranges[i].label, lowRawValue: startValue, highStringValue: undefined, highRawValue: endValue, count: rds.length, }); } } // 3. limit to top, Don't sort by count if (Number.isInteger(top) && top > 0) { return facetItemsData.slice(0, top); } else { return facetItemsData; } } }