ebta-workflow-prod 1.1.2 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.eslintrc.js +7 -0
  2. package/.vscode/extensions.json +3 -0
  3. package/.vscode/settings.json +3 -0
  4. package/README.md +26 -0
  5. package/build/2983859c0d77824ec8db.svg +1 -0
  6. package/build/4168d659d55fd94368bc.svg +1 -0
  7. package/build/641b2c17e92b0e885b9f.svg +1 -0
  8. package/build/882445a26a4f3964786f.svg +1 -0
  9. package/build/activitypack.json +1 -1
  10. package/build/b280e967312a55101032.svg +1 -0
  11. package/build/main.js +2 -2
  12. package/build/main.js.LICENSE.txt +99 -0
  13. package/certs/localhost+2-key.pem +28 -0
  14. package/certs/localhost+2.pem +26 -0
  15. package/index.html +58 -0
  16. package/package.json +43 -7
  17. package/public/img/background.svg +3 -3
  18. package/public/img/compass.svg +3 -3
  19. package/public/img/grab.svg +3 -3
  20. package/public/img/grabbing.svg +3 -3
  21. package/public/img/sprites.svg +14 -14
  22. package/src/activities/AppendObjects.ts +40 -0
  23. package/src/activities/Base64ToFile.ts +56 -0
  24. package/src/activities/CreateGraphicLayer.ts +85 -0
  25. package/src/activities/DrapGeometry.ts +99 -0
  26. package/src/activities/GetElementByDataLayoutID.ts +58 -0
  27. package/src/activities/GetSesionStorage.ts +36 -0
  28. package/src/activities/GetTaskStatus.ts +56 -0
  29. package/src/activities/GetUserToken.ts +102 -0
  30. package/src/activities/GetvGISDetails copy.ts +110 -0
  31. package/src/activities/GetvGISDetails.ts +110 -0
  32. package/src/activities/HTMLextractor.ts +188 -0
  33. package/src/activities/LegendCreator.ts +155 -0
  34. package/src/activities/MergeObjects.ts +61 -0
  35. package/src/activities/PDFMapGenerator.ts +729 -0
  36. package/src/activities/RequestChecker.ts +94 -0
  37. package/src/activities/Sample.ts +41 -0
  38. package/src/activities/SetSessionStorage.ts +57 -0
  39. package/src/activities/SliceLine.ts +61 -0
  40. package/src/elements/AttributeFilter.tsx +1867 -0
  41. package/src/elements/CircularSlider.tsx +67 -0
  42. package/src/elements/CsColorPicker.tsx +140 -0
  43. package/src/elements/DropDownMenu.tsx +282 -0
  44. package/src/elements/HeaderwithHelp.tsx +178 -0
  45. package/src/elements/ItemSearcher.tsx +1035 -0
  46. package/src/elements/ItemSearcher_v2.tsx +1071 -0
  47. package/src/elements/LineChartMui.tsx +50 -0
  48. package/src/elements/MenueBar.tsx +118 -0
  49. package/src/elements/MultipleAutoCompleteCS.tsx +145 -0
  50. package/src/elements/SingleAutoCompleteCS.tsx +133 -0
  51. package/src/elements/TableRadioGoup.tsx +163 -0
  52. package/src/elements/Timeline.tsx +104 -0
  53. package/src/elements/ToggleButtonGroup.tsx +129 -0
  54. package/src/elements/Viewer360.tsx +71 -0
  55. package/src/elements/modules/AttributeFilterEasyComp.tsx +253 -0
  56. package/src/index.ts +41 -0
  57. package/src/main.ts +15 -0
  58. package/src/styles/imageSlider.css +34 -0
  59. package/tsconfig.json +12 -0
  60. package/uuid.js +7 -0
@@ -0,0 +1,1867 @@
1
+ /* eslint-disable prefer-const */
2
+ import * as React from "react";
3
+ import { FormElementProps, FormElementRegistration } from "@vertigis/workflow";
4
+ import IconButton from '@vertigis/web/ui/IconButton';
5
+ import { Clear as ClearIcon } from '@mui/icons-material';
6
+ import List from '@vertigis/web/ui/List';
7
+ import ListItemButton from '@vertigis/web/ui/ListItemButton';
8
+ import ListItemIcon from '@vertigis/web/ui/ListItemIcon';
9
+ import ListItemText from '@vertigis/web/ui/ListItemText';
10
+ import Collapse from '@vertigis/web/ui/Collapse';
11
+ import FolderOpenIcon from '@mui/icons-material/FolderOpen';
12
+ import ExpandLess from '@mui/icons-material/ExpandLess';
13
+ import ExpandMore from '@mui/icons-material/ExpandMore';
14
+ import SearchIcon from '@vertigis/web/ui/icons/Search';
15
+ import Box from "@vertigis/web/ui/Box"
16
+ import Checkbox from "@vertigis/web/ui/Checkbox"
17
+ import { ThemeProvider } from "@emotion/react";
18
+ import Typography from '@vertigis/web/ui/Typography';
19
+ import { DateRangePicker } from '@mui/x-date-pickers-pro/DateRangePicker';
20
+ import dayjs, { Dayjs } from 'dayjs';
21
+ import timezone from 'dayjs/plugin/timezone';
22
+ import utc from 'dayjs/plugin/utc';
23
+ import customParseFormat from 'dayjs/plugin/customParseFormat'
24
+ import { DemoContainer } from '@mui/x-date-pickers/internals/demo';
25
+ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
26
+ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
27
+ import 'dayjs/locale/en-au';
28
+ import Chip from '@mui/material/Chip';
29
+ import Button from '@vertigis/web/ui/Button';
30
+ import Radio from '@vertigis/web/ui/Radio';
31
+ import RadioGroup from '@vertigis/web/ui/RadioGroup';
32
+ import FormControlLabel from '@vertigis/web/ui/FormControlLabel';
33
+ import AttributeFilterEasyComp from './modules/AttributeFilterEasyComp';
34
+ import Stack from '@vertigis/web/ui/Stack';
35
+ import Graphic from "esri/Graphic";
36
+ const { Search, SearchIconWrapper, StyledInputBase, StyledBadge, LightTooltip, defaultTheme, CustomNumberInput } = AttributeFilterEasyComp;
37
+ dayjs.extend(utc);
38
+ dayjs.extend(timezone);
39
+ dayjs.extend(customParseFormat);
40
+ /**
41
+ * The generic type argument provided to `FormElementProps<T>` controls the type
42
+ * of `props.value` and `props.setValue(value)`. If your element doesn't need a
43
+ * `value`, you can omit the type argument.
44
+ *
45
+ * You can also declare additional public properties of your element by adding
46
+ * properties to this interface. The properties will be managed by the Workflow
47
+ * runtime, and passed in as `props`. You can update the properties by using
48
+ * `props.setProperty(key, value)`.
49
+ */
50
+ interface AttributeFilterProps extends FormElementProps<string> {
51
+ features?: Graphic[],//Provide when local query rather web query
52
+ fields: {
53
+ name: string;
54
+ alias: string;
55
+ type: string;
56
+ [key: string]: any;
57
+ }[],
58
+ dataOld?: Attribute[],
59
+ //field names & type only at beginning
60
+ openedAttribute?: { data: Attribute, timestamp: number },
61
+ //Data communication purpose
62
+ newSum?: { sum: number, attri: string, timestamp: number },
63
+ //Data communication purpose
64
+ buttonNaming?: string[],
65
+ //List of buttons, if name ends with () will show summary number. Except first button, always has () ,and will be apply if this prop not given. If the button is enabled only when there is selection, put <> at end
66
+ // allFeatures?: object[]
67
+ //GRAPHICS TO BE HERE
68
+ footerButton?: boolean,//if true, use the button of default form footer
69
+ request?: { requestName: string, timestamp: number },
70
+ //Following attributes are for keep property in Vertigis only when switching tab
71
+ searchQuery: string;
72
+ attributeList?: string[],
73
+ data: Attribute[];
74
+ searchValue: string;
75
+ summary: number;
76
+ sqlRule: string;
77
+ allButtonText: string;
78
+ rule: string;
79
+ layerName: string;
80
+ minValue: number;
81
+ maxValue: number;
82
+ startDate: Dayjs;
83
+ endDate: Dayjs;
84
+ filteredValues: fieldValue[];
85
+ }
86
+
87
+ interface fieldValue {
88
+ value: string,
89
+ count: number
90
+ }
91
+ interface Attribute {
92
+ name: string,
93
+ alias?: string,
94
+ type?: string,
95
+ uniqValue?: fieldValue[],
96
+ display?: boolean,
97
+ img?: string,
98
+ visible?: boolean,
99
+ selection?: string[],
100
+ timeType?: string,
101
+ hidden?: boolean,
102
+ }
103
+
104
+ /**
105
+ * Provide strictly structured item data to display them and search.
106
+ * @displayName AttributeFilter
107
+ * @description For any layer, dynamicly build your SQL query based on all attributes.
108
+ * @param props Should provide strictly structured Data/openedAttribute
109
+ */
110
+
111
+
112
+
113
+
114
+ function AttributeFilter(props: AttributeFilterProps): React.ReactElement {
115
+
116
+ const defaultNumber = 4
117
+ const { raiseEvent, searchQuery, data, searchValue, summary, minValue, maxValue, filteredValues,
118
+ startDate, endDate, sqlRule, allButtonText, rule, layerName, setProperty } = props;
119
+ const defaultStart = dayjs('2020-01-01')
120
+ const defaultEnd = dayjs('2050-01-01')
121
+
122
+
123
+
124
+ const prevSearchValue = React.useRef<string>('')
125
+ const openAttriName = React.useRef('')
126
+ const tickedBeforeNewQuery = React.useRef<string[] | undefined>(undefined)
127
+ const openAttriValues = React.useRef<Attribute | null>(null);
128
+ const openAttriTime = React.useRef<number | null>(null);
129
+ const requestTime = React.useRef<number | null>(null);
130
+ const numberRecord = React.useRef<Record<string, number>>({});
131
+ const tempSummary = React.useRef(0)
132
+ const prevSum = React.useRef<{ sum: number, attri: string, timestamp: number } | undefined>(undefined)
133
+ const tempPreviousSummary = React.useRef(0)
134
+ const minDate = React.useRef(946700187)
135
+ const maxDate = React.useRef(2209004187)
136
+ //const [filteredValues, setFilteredValues] = React.useState<fieldValue[]>([]); // New state for filtered data
137
+ const [showAll, setShowAll] = React.useState(false);
138
+ // const [minValue, setMinValue] = React.useState<number>(-Infinity)
139
+ // const [maxValue, setMaxValue] = React.useState<number>(Infinity)
140
+ // const [startDate, setStartDate] = React.useState<Dayjs>(defaultStart)
141
+ // const [endDate, setEndDate] = React.useState<Dayjs>(defaultEnd)
142
+ const [folderOpen, setFolderOpen] = React.useState(false);
143
+ const stickyStyle = {
144
+ position: 'sticky' as const,
145
+ top: 0,
146
+ zIndex: 1000,
147
+ backgroundColor: 'var(--primaryBackground)'
148
+ };
149
+
150
+
151
+ const attributeList: string[] = React.useMemo(() => {
152
+ return props.attributeList || props.fields.map(a => a.name)
153
+ },
154
+ [props.attributeList, props.fields])
155
+
156
+
157
+ const RawOriginalData: Attribute[] = React.useMemo(() => {
158
+ if (!props.features) return [];
159
+ return features2Data(props.features, attributeList) || [];
160
+ function features2Data(features: Graphic[], shortListOfAttributes: string[]): Attribute[] {
161
+ // Create an object to store the statistical summary for each attribute
162
+ const attributeSummary: { [key: string]: { [value: string]: number } } = {};
163
+
164
+ // Iterate through the features array
165
+ features.forEach((feature) => {
166
+ // Iterate through each attribute in the 'attributes' object of the feature
167
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
168
+ for (const [key, value] of Object.entries(feature.attributes)) {
169
+ // Check if the attribute is in the short list
170
+ if (shortListOfAttributes.includes(key)) {
171
+ // If it's not already in the attributeSummary, initialize it
172
+ if (!attributeSummary[key]) {
173
+ attributeSummary[key] = {};
174
+ }
175
+ let valueKey
176
+ if (props.fields.find(obj => obj.name == key)?.type == "date" && Number(value) > 0) {
177
+ valueKey = dayjs.unix(Number(value) / 1000).format("DD/MM/YYYY")
178
+ }
179
+ // Count the occurrence of each value for the attribute
180
+ else { valueKey = String(value); }
181
+ if (!attributeSummary[key][valueKey]) {
182
+ attributeSummary[key][valueKey] = 0;
183
+ }
184
+ attributeSummary[key][valueKey]++;
185
+ }
186
+ }
187
+ });
188
+
189
+ // Now we need to structure the data according to the `Attribute` interface
190
+ const result: Attribute[] = [];
191
+
192
+ // Convert the summary into the desired format
193
+ for (const [attributeName, valueCountMap] of Object.entries(attributeSummary)) {
194
+ const uniqValues: fieldValue[] = Object.entries(valueCountMap).map(([value, count]) => ({
195
+ value: value,
196
+ count: count,
197
+ }));
198
+
199
+ // Create the Attribute object
200
+ const attribute: Attribute = {
201
+ name: attributeName,
202
+ uniqValue: uniqValues,
203
+ };
204
+ result.push(attribute);
205
+ }
206
+ return result;
207
+ }
208
+ },
209
+ [attributeList, props.features, props.fields])
210
+
211
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
212
+ const originalData: Attribute[] = React.useMemo(() => {
213
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
214
+ let result = props.fields.filter(obj => !attributeList || attributeList.length === 0 || attributeList.includes(obj.name)) // Filter only if nameList is valid
215
+ .filter(obj => {
216
+ if (RawOriginalData.length==0){return true}
217
+ let matchingRawDataValues = RawOriginalData.find(item => item.name === obj.name)?.uniqValue;
218
+ let meanless = ['{}', ' ', 'null']
219
+ return (matchingRawDataValues && matchingRawDataValues.length > 0 && !meanless.includes(matchingRawDataValues[0].value))
220
+ })
221
+ .map(obj => {
222
+ let newObj: Attribute = { name: obj.name };
223
+ if (obj.alias) {
224
+ newObj.alias = obj.alias;
225
+ }
226
+ if (obj.type !== "string") {
227
+ newObj.type = obj.type;
228
+ }
229
+ newObj.visible = true;
230
+ newObj.display = false;
231
+ if (RawOriginalData?.length > 0) {
232
+ let matchingRawData = RawOriginalData.find(item => item.name === obj.name);
233
+ if (matchingRawData && matchingRawData.uniqValue) {
234
+ matchingRawData.uniqValue.sort((a, b) => {
235
+ if (a.count === b.count) {
236
+ return a.value.localeCompare(b.value);
237
+ }
238
+ return b.count - a.count;
239
+ });
240
+ newObj.uniqValue = matchingRawData.uniqValue;
241
+ }
242
+ }
243
+ return newObj;
244
+ });
245
+ return result;
246
+ }, [RawOriginalData, attributeList, props.fields])
247
+
248
+
249
+ const DataRef = React.useRef<Attribute[]>(originalData)
250
+
251
+ const recordTable: object[] = React.useMemo(() => {
252
+ return props.features?.map(feature => {
253
+ const filteredAttributes = {};
254
+ for (const key of attributeList) {
255
+ if (key in feature.attributes) {
256
+ filteredAttributes[key] = originalData.find(attri => attri.name === key)?.type == 'date' ? epoch2MelbDate(Number(feature.attributes[key]) / 1000) : feature.attributes[key];
257
+ }
258
+ }
259
+ return filteredAttributes;
260
+ }) || [];
261
+ },
262
+ [attributeList, originalData, props.features])
263
+
264
+
265
+ if (data === undefined) {
266
+ setProperty("data" as any, originalData);
267
+ }
268
+
269
+ const findEarliestAndLatestDatesForAttribute = (attribute: Attribute) => {
270
+ if (attribute.type !== "date") {
271
+ // Return null if the attribute is not of type 'date'
272
+ return null;
273
+ }
274
+
275
+ let earliestDate: number, latestDate: number
276
+ if (attribute.uniqValue && Number(attribute.uniqValue[0].value) && Number(attribute.uniqValue[0].value) > 0) {
277
+ const values = attribute.uniqValue.map(obj => Number(obj.value));
278
+ earliestDate = Math.min(...values);
279
+ latestDate = Math.max(...values);
280
+ }
281
+ else {
282
+ const dates = attribute.uniqValue?.map((entry) => entry.value) || []; // Extract date strings
283
+
284
+ // Convert to Date objects for comparison
285
+ const dateObjects: Date[] = dates.map((date) => {
286
+ const [day, month, year] = date.split('/').map(Number);
287
+ return new Date(year, month - 1, day); // JS months are 0-indexed
288
+ });
289
+
290
+ // Find the earliest and latest dates by converting to numeric timestamps
291
+ earliestDate = Math.min(...dateObjects.map((date) => date.getTime()));
292
+ latestDate = Math.max(...dateObjects.map((date) => date.getTime()));
293
+ }
294
+
295
+
296
+ // Return results as epoch time numbers
297
+ return {
298
+ name: attribute.name,
299
+ earliest: earliestDate,
300
+ latest: latestDate,
301
+ }
302
+ };
303
+
304
+ // Handle search input changes
305
+ const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
306
+ setProperty("searchQuery" as any, event.target.value); // Update search query
307
+ };
308
+
309
+ function layerNameCreator(data: Attribute[], sqlConnector: string) {
310
+ const clauses = data.map((attribute) => {
311
+ if (attribute.selection && attribute.selection.length > 0) {
312
+ const values = attribute.selection.join(',');
313
+ return `${attribute.name}: ${values}`;
314
+ }
315
+ return null;
316
+ }).filter(clause => clause !== null);
317
+
318
+ const result = clauses.join(` ${sqlConnector == 'AND' ? "&" : "+"} `);
319
+ return result
320
+ }
321
+
322
+ function IsOtherRulesEmpty(specificAttributeName: string) {
323
+ return data && data.every(attribute => {
324
+ if (attribute.name === specificAttributeName) {
325
+ return true;
326
+ }
327
+ return (!attribute.selection || attribute.selection.length == 0)
328
+ });
329
+ }
330
+
331
+ const handleSqlRule = (event: React.ChangeEvent<HTMLInputElement>) => {
332
+ //openAttriName.current = ''
333
+ setProperty("sqlRule" as any, (event.target as HTMLInputElement).value);
334
+ const allSelectionsEmpty = data && data.every(attribute =>
335
+ attribute.selection === null ||
336
+ attribute.selection === undefined ||
337
+ (Array.isArray(attribute.selection) && attribute.selection.length === 0)
338
+ );
339
+ if (allSelectionsEmpty) { return }
340
+
341
+ if ((event.target as HTMLInputElement).value == 'OR') {
342
+ //converting from AND to OR,
343
+ //1. summary needs to be recaculated from backend OR engine, and get a larger number
344
+ //2. if any atttribute is displayed, get original uniq values and re-render
345
+
346
+ if (summary !== 0) {
347
+ if (recordTable !== undefined && recordTable.length > 0) {
348
+ const newCount = filterFeatures(recordTable, data, 'OR').length
349
+ setProperty("summary" as any, newCount)
350
+ }
351
+ else {
352
+ raiseEvent("clicked", JSON.stringify({ request: 'sumOnly', rule: generateSQLSummary(data, 'OR') }))
353
+ }
354
+ }
355
+
356
+ if (openAttriName.current !== '') {
357
+ if (recordTable !== undefined && recordTable.length > 0) {
358
+ localDisplayLayer(originalData.find(obj => obj.name == openAttriName.current) || data[0], undefined, true)
359
+ }
360
+ else {
361
+ raiseEvent("clicked", JSON.stringify({ request: 'displayLayer', value: openAttriName.current, rule: '1=1' }));
362
+ }
363
+
364
+ }
365
+ }
366
+ else {
367
+ //converting from OR to AND,
368
+ //1. get new sum
369
+ //2. if some attri open, reload unqivalues
370
+ if (openAttriName.current == '') {
371
+ if (summary !== 0 || !allSelectionsEmpty) {
372
+ if (recordTable !== undefined && recordTable.length > 0) {
373
+ const newCount = filterFeatures(recordTable, data, 'AND').length
374
+ setProperty("summary" as any, newCount)
375
+ }
376
+ else {
377
+ raiseEvent("clicked", JSON.stringify({ request: 'sumOnly', rule: generateSQLSummary(data, 'AND') }))
378
+ }
379
+ }
380
+ }
381
+ else {
382
+ if (recordTable !== undefined && recordTable.length > 0) {
383
+ localDisplayLayer(data?.find(obj => obj.name == openAttriName.current) || data[0], undefined, false, 'AND')
384
+ }
385
+ else {
386
+ raiseEvent("clicked", JSON.stringify({ request: 'displayLayer', value: openAttriName.current, rule: generateSQLSummary(data, 'AND', openAttriName.current) }))
387
+ }
388
+
389
+ if (summary !== 0 || !allSelectionsEmpty) {
390
+ if (recordTable !== undefined && recordTable.length > 0) {
391
+ const newCount = filterFeatures(recordTable, data, 'AND').length
392
+ setProperty("summary" as any, newCount)
393
+ }
394
+ else {
395
+ raiseEvent("clicked", JSON.stringify({ request: 'sumOnly', rule: generateSQLSummary(data, 'AND') }))
396
+ }
397
+ }
398
+ }
399
+ }
400
+
401
+ raiseEvent("clicked", JSON.stringify({ request: 'updateMap', rule: generateSQLSummary(data, (event.target as HTMLInputElement).value) }))
402
+ };
403
+
404
+ // const handleSearchValueChange = (attri:Attribute,event: React.ChangeEvent<HTMLInputElement>) => {
405
+ const handleSearchValueChange = (attri: Attribute, event: React.ChangeEvent<HTMLInputElement>) => {
406
+ // setProperty("searchValue" as any,event.target.value); // Update search query
407
+ if (event.target.value) {
408
+ const searchValueResults = filterValue(attri, event.target.value);
409
+ setProperty("filteredValues" as any, searchValueResults)
410
+ setProperty("searchValue" as any, event.target.value)
411
+ }
412
+ else {
413
+ setProperty("searchValue" as any, event.target.value)
414
+ setProperty("filteredValues" as any, [])
415
+ }
416
+ };
417
+
418
+ const handleSelectAll = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>, attributeName: string) => {
419
+ event.stopPropagation(); // Prevents the ListItemButton click event
420
+ const selectAll = (allButtonText == 'Select All')
421
+ const newText = selectAll ? 'Deselect All' : 'Select All'
422
+ setProperty("allButtonText" as any, newText)
423
+ const dataCopy = structuredClone(data)
424
+ let updatedDataCopy: Attribute[]
425
+ if (selectAll) {
426
+ if (filteredValues.length > 0) {
427
+ //select all search result
428
+ updatedDataCopy = dataCopy.map((attribute) => {
429
+ if (attribute.name === attributeName) {
430
+ const filteredValuesSet = new Set(filteredValues.map(item => item.value));
431
+ const allValues = attribute.uniqValue
432
+ ?.filter(item => filteredValuesSet.has(item.value))
433
+ .map(item => item.value) || [];
434
+
435
+ const totalCount = attribute.uniqValue?.reduce((sum, item) => {
436
+ return (!attribute.selection?.includes(item.value) && filteredValuesSet.has(item.value)) ? sum + (item.count || 0) : sum;
437
+ }, 0) || 0;
438
+ if (sqlRule == 'AND') {
439
+
440
+ const result = (attribute.selection && attribute.selection.length > 0) ?
441
+ (summary + totalCount) : IsOtherRulesEmpty(attributeName) ? totalCount : Math.min(summary, totalCount)
442
+ tempSummary.current = result
443
+ setProperty("summary" as any, result);
444
+ numberRecord.current[attributeName] = tempSummary.current
445
+ }
446
+ else {
447
+ tempSummary.current = Math.max(summary, totalCount)
448
+ const newSummary = Math.max(summary, totalCount)
449
+ setProperty("summary" as any, newSummary)
450
+ numberRecord.current[attributeName] = tempSummary.current
451
+ }
452
+ return {
453
+ ...attribute,
454
+ selection: Array.from(new Set([...(allValues ?? []), ...(attribute.selection ?? [])])),
455
+ };
456
+ }
457
+ return attribute;
458
+ })
459
+
460
+ setProperty("data" as any, updatedDataCopy);
461
+ }
462
+ else {
463
+ //No search or range - real select all
464
+ updatedDataCopy = dataCopy.map((attribute) => {
465
+ if (attribute.name === attributeName) {
466
+ const allValues = attribute.uniqValue?.map((item) => item.value) || [];
467
+
468
+ const totalCount = attribute.uniqValue?.reduce((sum, obj) => sum + obj.count, 0) || 0;
469
+
470
+ if (IsOtherRulesEmpty(attributeName) || sqlRule == 'AND') {
471
+ tempSummary.current = totalCount
472
+ setProperty("summary" as any, totalCount);
473
+ }
474
+ else if (!IsOtherRulesEmpty(attributeName)) {
475
+ if (recordTable !== undefined && recordTable.length > 0) {
476
+ const newCount = filterFeatures(recordTable, data, sqlRule).length
477
+ setProperty("summary" as any, newCount)
478
+ }
479
+ else {
480
+ raiseEvent("clicked", JSON.stringify({ request: 'sumOnly', rule: generateSQLSummary(data, sqlRule) }))
481
+ }
482
+ }
483
+
484
+ return {
485
+ ...attribute,
486
+ selection: allValues,
487
+ };
488
+ }
489
+ return attribute;
490
+ })
491
+
492
+ setProperty("data" as any, updatedDataCopy);
493
+ }
494
+ raiseEvent("clicked", JSON.stringify({ request: 'updateMap', rule: generateSQLSummary(updatedDataCopy, sqlRule) }))
495
+ }
496
+ else {
497
+ if (filteredValues.length > 0) {
498
+
499
+ const filteredValuesSet = new Set(filteredValues.map(item => item.value));
500
+
501
+ updatedDataCopy = dataCopy.map((attribute) => {
502
+ if (attribute.name === attributeName) {
503
+ const totalCount = attribute.uniqValue?.reduce((sum, item) => {
504
+ return (filteredValuesSet.has(item.value) && attribute.selection?.includes(item.value)) ? sum + (item.count || 0) : sum;
505
+ }, 0) || 0;
506
+
507
+ if (!IsOtherRulesEmpty(attributeName) && sqlRule == 'AND' && totalCount == summary) {
508
+ setProperty("summary" as any, tempPreviousSummary.current)
509
+ numberRecord.current[attributeName] = tempSummary.current
510
+ }
511
+ else {
512
+ tempSummary.current = summary - totalCount
513
+ const newSummary = summary - totalCount
514
+ setProperty("summary" as any, newSummary)
515
+ numberRecord.current[attributeName] = tempSummary.current
516
+ }
517
+ return {
518
+ ...attribute,
519
+ selection: attribute.selection?.filter(item => !filteredValuesSet.has(item)) || [],
520
+ };
521
+ }
522
+ return attribute;
523
+ })
524
+
525
+ setProperty("data" as any, updatedDataCopy);
526
+ if (generateSQLSummary(data, sqlRule, attributeName) == '1=1') {
527
+ tempSummary.current = 0;
528
+ tempPreviousSummary.current = 0;
529
+ }
530
+ else {
531
+ if (recordTable !== undefined && recordTable.length > 0) {
532
+ const newCount = filterFeatures(recordTable, updatedDataCopy, sqlRule).length
533
+ setProperty("summary" as any, newCount)
534
+ }
535
+ else {
536
+ raiseEvent("clicked", JSON.stringify({ request: 'sumOnly', rule: generateSQLSummary(updatedDataCopy, sqlRule) }))
537
+ }
538
+ }
539
+ raiseEvent("clicked", JSON.stringify({ request: 'updateMap', rule: generateSQLSummary(updatedDataCopy, sqlRule, attributeName) }))
540
+ }
541
+ else {
542
+ deleteSelection(data?.find(attri => attri.name == attributeName) || data[0])
543
+ raiseEvent("clicked", JSON.stringify({ request: 'updateMap', rule: generateSQLSummary(data, sqlRule, attributeName) }))
544
+ }
545
+ }
546
+ };
547
+
548
+ const handleBtnClick = (requestName: string) => {
549
+ raiseEvent("clicked", JSON.stringify({ request: requestName, rule: generateSQLSummary(data, sqlRule), layerName: layerNameCreator(data, sqlRule) }))
550
+ }
551
+
552
+ const handleClearRulesClick = () => {
553
+ const updatedData = data?.map((attribute) => {
554
+ if (attribute.selection && attribute.selection.length > 0) {
555
+ attribute.selection = undefined
556
+ }
557
+ return attribute
558
+ }
559
+ )
560
+ setProperty("data" as any, updatedData);
561
+ setProperty("summary" as any, 0)
562
+ raiseEvent("clicked", JSON.stringify({ request: 'Clear' }))
563
+ if (recordTable !== undefined && recordTable.length > 0) {
564
+ const updateCopy = originalData.map((attribute) => {
565
+ if (attribute.name == openAttriName.current) {
566
+ attribute.display = true
567
+ }
568
+ else {
569
+ attribute.display = false
570
+ }
571
+ return attribute
572
+ })
573
+ setProperty("data" as any, updateCopy)
574
+ }
575
+ }
576
+
577
+ // Handle click on items to raise an event
578
+ const handleItemClick = (currentAttribute: Attribute, fieldValue: fieldValue) => {
579
+ const dataCopy = structuredClone(data)
580
+ const updatedDataCopy = dataCopy.map((attribute) => {
581
+ if (currentAttribute.name === attribute.name) {
582
+ const isSelected = attribute.selection?.includes(fieldValue.value);
583
+ return {
584
+ ...attribute,
585
+ selection: isSelected
586
+ ? (attribute.selection || []).filter((value) => value !== fieldValue.value)
587
+ : [...(attribute.selection || []), fieldValue.value]
588
+ };
589
+ }
590
+ return attribute;
591
+ });
592
+ setProperty("data" as any, updatedDataCopy);
593
+
594
+ if (sqlRule == 'AND') {
595
+ if (!IsOtherRulesEmpty(currentAttribute.name)) {
596
+ if (!currentAttribute.selection || currentAttribute.selection.length == 0) {
597
+ tempSummary.current = 0
598
+ }
599
+ if (currentAttribute.selection?.includes(fieldValue.value)) {
600
+ tempSummary.current -= fieldValue.count
601
+
602
+ setProperty("summary" as any, tempSummary.current == 0 ? tempPreviousSummary.current : tempSummary.current);
603
+ numberRecord.current[currentAttribute.name] = tempSummary.current
604
+ }
605
+ else {
606
+ tempSummary.current += fieldValue.count
607
+ setProperty("summary" as any, tempSummary.current);
608
+ numberRecord.current[currentAttribute.name] = tempSummary.current
609
+ }
610
+ }
611
+ else {
612
+ if (currentAttribute.selection?.includes(fieldValue.value)) {
613
+ tempSummary.current = summary - fieldValue.count
614
+ setProperty("summary" as any, summary - fieldValue.count);
615
+ numberRecord.current[currentAttribute.name] = tempSummary.current
616
+ }
617
+ else {
618
+ tempSummary.current = summary + fieldValue.count
619
+ setProperty("summary" as any, summary + fieldValue.count);
620
+ numberRecord.current[currentAttribute.name] = tempSummary.current
621
+ }
622
+ }
623
+ }
624
+ else {
625
+ if (recordTable !== undefined && recordTable.length > 0) {
626
+ const newCount = filterFeatures(recordTable, updatedDataCopy, 'OR').length
627
+ setProperty("summary" as any, newCount)
628
+ }
629
+ else {
630
+ if (!currentAttribute.selection || currentAttribute.selection.length == 0) {
631
+ tempSummary.current = 0
632
+ const values: number[] = Object.values(numberRecord.current);
633
+ const maxNumber = Math.max(...values);
634
+ tempPreviousSummary.current = maxNumber
635
+
636
+
637
+ }
638
+ if (currentAttribute.selection?.includes(fieldValue.value)) {
639
+ tempSummary.current -= fieldValue.count
640
+ setProperty("summary" as any,
641
+ tempSummary.current > tempPreviousSummary.current ? tempSummary.current : tempPreviousSummary.current);
642
+ numberRecord.current[currentAttribute.name] = tempSummary.current
643
+ }
644
+ else {
645
+ tempSummary.current += fieldValue.count
646
+ if (tempSummary.current > summary) {
647
+ setProperty("summary" as any, tempSummary.current);
648
+ }
649
+ numberRecord.current[currentAttribute.name] = tempSummary.current
650
+
651
+ }
652
+
653
+
654
+ }
655
+
656
+ }
657
+
658
+ raiseEvent("clicked", JSON.stringify({ request: 'updateMap', rule: generateSQLSummary(updatedDataCopy, sqlRule) }))
659
+
660
+ };
661
+
662
+
663
+
664
+ const generateSQLSummary = (data: Attribute[], logicalOperator: string, exception?: string | string[]) => {
665
+
666
+
667
+ const isException = (name: string) => {
668
+ if (Array.isArray(exception)) {
669
+ return exception.includes(name);
670
+ }
671
+ return name === exception;
672
+ };
673
+
674
+ function generateDateSQL(melbourneDates: string[], epochField: string) {
675
+ const convertToUTC = (melbourneTime: string) => {
676
+ return dayjs.tz(melbourneTime, 'Australia/Melbourne').utc().format('YYYY-MM-DD HH:mm:ss');
677
+ };
678
+ const sqlConditions = melbourneDates.map(dateStr => {
679
+ const startTimestamp = convertToUTC(`${dateStr} 00:00:00`);
680
+ const endTimestamp = convertToUTC(`${dateStr} 23:59:59`);
681
+ return `(${epochField} BETWEEN TIMESTAMP '${startTimestamp}' AND TIMESTAMP '${endTimestamp}')`;
682
+ });
683
+ const combinedConditions = "(" + sqlConditions.join(' OR ') + ")";
684
+ return combinedConditions
685
+ }
686
+
687
+
688
+ const clauses = data.map((attribute) => {
689
+ if (!isException(attribute.name) && attribute.selection && attribute.selection.length > 0) {
690
+ let values: string[] | string;
691
+ if (attribute.type === 'date') {
692
+ if (attribute.selection.length < 11) {
693
+ values = attribute.selection.map((value) => { return dayjs(value, 'DD/MM/YYYY').format('YYYY-MM-DD') })
694
+ return generateDateSQL(values, attribute.name)
695
+ }
696
+ else {
697
+ const convertToUTC = (melbourneTime: string) => {
698
+ return dayjs.tz(melbourneTime, 'Australia/Melbourne').utc().format('YYYY-MM-DD HH:mm:ss');
699
+ };
700
+ const timeArray = attribute.selection.map((value) => { return dayjs(value, 'DD/MM/YYYY') })
701
+ const minDate = timeArray.reduce((min, current) => (current.isBefore(min) ? current : min)).format('YYYY-MM-DD');
702
+ const maxDate = timeArray.reduce((max, current) => (current.isAfter(max) ? current : max)).format('YYYY-MM-DD');
703
+
704
+ const startTimestamp = convertToUTC(`${minDate} 00:00:00`);
705
+ const endTimestamp = convertToUTC(`${maxDate} 23:59:59`);
706
+ return `${attribute.name} >= '${startTimestamp}' AND ${attribute.name} <= '${endTimestamp}'`;
707
+ }
708
+
709
+ } else {
710
+ const likeConditions: string[] = [];
711
+ const inValues: string[] = [];
712
+
713
+ attribute.selection.forEach(value => {
714
+ if (value.length > 72) {
715
+ const first72Chars = value.slice(0, 72); // Get the first 72 characters
716
+ likeConditions.push(`${attribute.name} LIKE '${first72Chars}%'`);
717
+ } else {
718
+ inValues.push(`'${value}'`); // Use value as-is for short strings
719
+ }
720
+ });
721
+
722
+ const inClause = inValues.length > 0 ? `${attribute.name} IN (${inValues.join(', ')})` : '';
723
+ const likeClause = likeConditions.join(' OR ');
724
+
725
+ if (inClause && likeClause) {
726
+ return `(${inClause} OR ${likeClause})`;
727
+ } else if (inClause) {
728
+ return `(${inClause})`;
729
+ } else if (likeClause) {
730
+ return `(${likeClause})`;
731
+ } else {
732
+ return null; // Return an empty string if there are no conditions
733
+ }
734
+ }
735
+ }
736
+ return null;
737
+ }).filter(clause => clause !== null);
738
+
739
+ const result = clauses.length > 1 ? clauses.join(` ${logicalOperator} `) : clauses.length > 0 ? clauses[0].replace(/^\(|\)$/g, '') : ''
740
+ return result.length > 2 ? result : '1=1'
741
+ };
742
+
743
+ interface Condition {
744
+ type: 'IN' | 'LIKE' | 'BETWEEN'; // Condition type: IN, LIKE, or BETWEEN
745
+ field: string; // The field (column) to apply the condition on
746
+ values?: any[]; // Values for the IN condition
747
+ pattern?: string; // Pattern for the LIKE condition
748
+ start?: number | string | Date; // Start value for BETWEEN condition (number, string, or Date)
749
+ end?: number | string | Date; // End value for BETWEEN condition (number, string, or Date)
750
+ }
751
+
752
+ class SimpleSQLWhereEngine {
753
+ private data: object[]; // Data is an array of objects
754
+
755
+ constructor(data: Record<string, any>[]) {
756
+ this.data = data;
757
+ }
758
+
759
+ // Utility function to convert date strings to epoch time (milliseconds)
760
+ private parseDate(dateString: string): { start: number, end: number } {
761
+ // Format: dd/mm/yyyy (like '25/01/2024')
762
+ const [day, month, year] = dateString.split('/').map(Number);
763
+
764
+ // Create a date for the start of the day (00:00:00)
765
+ const startOfDay = new Date(year, month - 1, day, 0, 0, 0, 0).getTime();
766
+
767
+ // Create a date for the end of the day (23:59:59)
768
+ const endOfDay = new Date(year, month - 1, day, 23, 59, 59, 999).getTime();
769
+
770
+ return { start: startOfDay, end: endOfDay };
771
+ }
772
+
773
+ // WHERE IN (specific values) condition
774
+ private in(field: string, values: any[], isDate: boolean = false): Record<string, any>[] {
775
+ return this.data.filter(row => {
776
+ const fieldValue = row[field];
777
+
778
+ if (isDate && typeof fieldValue === 'number') {
779
+ // If isDate is true and the field is a number (epoch milliseconds), compare as dates
780
+ return values.some(value => {
781
+ if (typeof value === 'string') {
782
+ const { start, end } = this.parseDate(value);
783
+ return fieldValue >= start && fieldValue <= end; // Check if the field value is within the day range
784
+ }
785
+ return false;
786
+ });
787
+ }
788
+
789
+ // Default behavior for non-date fields or when isDate is false
790
+ return values.includes(fieldValue);
791
+ });
792
+ }
793
+
794
+ // WHERE LIKE (for string matching)
795
+ private like(field: string, pattern: string): Record<string, any>[] {
796
+ const regex = new RegExp(pattern.replace('%', '.*')); // Convert 'sth%' to regex pattern
797
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
798
+ return this.data.filter(row => regex.test(row[field]));
799
+ }
800
+
801
+ // WHERE BETWEEN (for timestamps or numerical ranges)
802
+ private between(field: string, start: number | string | Date, end: number | string | Date): Record<string, any>[] {
803
+ return this.data.filter(row => {
804
+ const value = row[field];
805
+ return value >= start && value <= end;
806
+ });
807
+ }
808
+
809
+ // General WHERE function that can handle all conditions
810
+ public where(condition: Condition, isDate: boolean = false): Record<string, any>[] {
811
+ if (condition.type === 'IN' && condition.values) {
812
+ return this.in(condition.field, condition.values, isDate);
813
+ } else if (condition.type === 'LIKE' && condition.pattern) {
814
+ return this.like(condition.field, condition.pattern);
815
+ } else if (condition.type === 'BETWEEN' && condition.start !== undefined && condition.end !== undefined) {
816
+ return this.between(condition.field, condition.start, condition.end);
817
+ }
818
+ return this.data; // Default return if no condition is met
819
+ }
820
+ }
821
+
822
+ function filterFeatures(features: Record<string, any>[], attributes: Attribute[], logic: string, exception?: string | string[]): Record<string, any>[] {
823
+ // eslint-disable-next-line prefer-const
824
+ let engine = new SimpleSQLWhereEngine(features);
825
+ let filteredFeatures = features; // Start with all features
826
+ const hasValidSelections = attributes.some(attribute => attribute.name !== exception && attribute.selection && attribute.selection.length > 0);
827
+
828
+ if (!hasValidSelections) {
829
+ return features; // Display all features if no valid selections
830
+ }
831
+
832
+ if (logic === 'AND') {
833
+ attributes.forEach(attribute => {
834
+
835
+ if ((attribute.name !== exception && !exception?.includes(attribute.name)) && attribute.selection && attribute.selection.length > 0) {
836
+ const condition: Condition = {
837
+ type: 'IN',
838
+ field: attribute.name,
839
+ values: attribute.selection
840
+ };
841
+ filteredFeatures = engine.where(condition, attribute.type === 'date'); // Filter progressively on each condition
842
+ engine = new SimpleSQLWhereEngine(filteredFeatures);
843
+ }
844
+ });
845
+ } else if (logic === 'OR') {
846
+ let tempFeatures: Record<string, any>[] = [];
847
+ if (attributes.some(obj => obj.selection && obj.selection.length > 0)) {
848
+ attributes.forEach(attribute => {
849
+ if (attribute.name !== exception && !exception?.includes(attribute.name) && attribute.selection && attribute.selection.length > 0) {
850
+ const condition: Condition = {
851
+ type: 'IN',
852
+ field: attribute.name,
853
+ values: attribute.selection
854
+ };
855
+ tempFeatures = tempFeatures.concat(engine.where(condition)); // Concatenate results
856
+ }
857
+ });
858
+ // Remove duplicates
859
+ filteredFeatures = tempFeatures.filter((item, index) => tempFeatures.indexOf(item) === index);
860
+ }
861
+ else {
862
+ filteredFeatures = []
863
+ }
864
+
865
+ }
866
+
867
+ return filteredFeatures;
868
+ }
869
+
870
+ function generateAttributeSummary(features: object[], attributeName: string, type: string): fieldValue[] {
871
+ // Create an object to store the statistical summary for the attribute
872
+ const attributeSummary: { [key: string]: number } = {};
873
+
874
+ // Iterate through the features array
875
+ features.forEach((feature) => {
876
+ const value = feature[attributeName];
877
+ if (value) {
878
+ // Count the occurrence of each value for the attribute
879
+ if (!attributeSummary[value]) {
880
+ attributeSummary[value] = 0;
881
+ }
882
+ attributeSummary[value]++;
883
+ }
884
+ });
885
+
886
+
887
+ // Convert the summary object to the desired array format
888
+ const rawResult: fieldValue[] = Object.entries(attributeSummary).map(([value, count]) => ({
889
+ value: type == 'date' && Number(value) > 0 ? epoch2MelbDate(Number(value) / 1000) : value,
890
+ count: count
891
+ }));
892
+ if (type !== 'date' || !Number(rawResult[0].value)) {
893
+ return rawResult
894
+ }
895
+ else {
896
+ const result = groupByMelbourneDate(rawResult)
897
+ return result
898
+ }
899
+ }
900
+
901
+ function epoch2MelbDate(epoch: any) {
902
+ return dayjs.unix(Number(epoch)).tz('Australia/Melbourne').format('DD/MM/YYYY');
903
+ }
904
+
905
+
906
+ function formatUniqValues(attri: Attribute) {
907
+ let newAttriData: fieldValue[]
908
+ if (attri.type == 'date' && attri.uniqValue && Number(attri.uniqValue[0].value) > 0) {
909
+ const values = attri.uniqValue.map(obj => Number(obj.value));
910
+ minDate.current = Math.min(...values) / 1000;
911
+ maxDate.current = Math.max(...values) / 1000;
912
+ setProperty("startDate" as any, dayjs.unix(Math.min(...values) / 1000))
913
+ setProperty("endDate" as any, dayjs.unix(Math.max(...values) / 1000))
914
+ newAttriData = groupByMelbourneDate(attri.uniqValue)
915
+ }
916
+ else {
917
+ newAttriData = attri.uniqValue || []
918
+ }
919
+ return newAttriData
920
+ }
921
+
922
+ function localDisplayLayer(attri: Attribute, exception?: string, noLimit?: boolean, Rule?: string) {
923
+ if (recordTable !== undefined && recordTable.length > 0) {
924
+ let result: Attribute[]
925
+ if (noLimit) {
926
+ const dataCopy = structuredClone(data)
927
+ result = dataCopy.map(item =>
928
+ item.name === attri.name ?
929
+ {
930
+ ...item,
931
+ display: true,
932
+ uniqValue: item.selection && item.selection.length > 0 ?
933
+ sortItemsBySelection(formatUniqValues(originalData.find(attribute => attribute.name == item.name) || data[0]), item.selection)
934
+ : formatUniqValues(originalData.find(attribute => attribute.name == item.name) || data[0])
935
+ }
936
+ : { ...item, display: false }
937
+ );
938
+ setProperty("data" as any, result);
939
+ }
940
+ else {
941
+ let queryFeatures: object[] = []
942
+ if (exception) {
943
+ queryFeatures = filterFeatures(recordTable, data, Rule || sqlRule, [exception, attri.name])
944
+ } else {
945
+ queryFeatures = filterFeatures(recordTable, data, Rule || sqlRule, attri.name)
946
+ }
947
+ const queryUniqValue = generateAttributeSummary(queryFeatures, attri.name, attri.type || 'string')
948
+ queryUniqValue.sort((a, b) => {
949
+ if (a.count === b.count) {
950
+ return a.value.localeCompare(b.value);
951
+ }
952
+ return b.count - a.count;
953
+ });
954
+ const dataCopy = structuredClone(data)
955
+ result = dataCopy.map(item =>
956
+ item.name === attri.name ?
957
+ {
958
+ ...item, display: true,
959
+ uniqValue: item.selection && item.selection.length > 0 ?
960
+ sortItemsBySelection(queryUniqValue, item.selection) : queryUniqValue
961
+ } : { ...item, display: false }
962
+ );
963
+ setProperty("data" as any, result);
964
+ }
965
+
966
+ if (attri.type == 'date') {
967
+ minDate.current = Math.floor((findEarliestAndLatestDatesForAttribute(attri)?.earliest || 946700000) / 1000)
968
+ maxDate.current = Math.floor((findEarliestAndLatestDatesForAttribute(attri)?.latest || 2209004000) / 1000)
969
+ setProperty("startDate" as any, dayjs.unix(minDate.current))
970
+ setProperty("endDate" as any, dayjs.unix(maxDate.current))
971
+ }
972
+ openAttriName.current = attri.name
973
+
974
+ // const oldSelection = result.find(obj => obj.name === openAttriName.current)?.selection || []
975
+ // const updatedAttri = result.find(obj => obj.name === attri.name) || result[0]
976
+ // if (openAttriName.current == attri.name && oldSelection.length > 0 && searchValue.length > 0) {
977
+ // const searchValueResults = filterValue(updatedAttri, searchValue);
978
+ // setProperty("filteredValues" as any, searchValueResults)
979
+ // }
980
+
981
+
982
+ // openAttriValues.current = updatedAttri
983
+
984
+ }
985
+ }
986
+
987
+ // // Handle category toggle open/close manually
988
+ const handleAttributeClick = (attri: Attribute) => {
989
+ setProperty("filteredValues" as any, [])
990
+ setShowAll(false)
991
+ setProperty("searchValue" as any, '');
992
+ setProperty("minValue" as any, -Infinity)
993
+ setProperty("maxValue" as any, Infinity)
994
+ tempPreviousSummary.current = summary
995
+ if (!attri.display) {
996
+ if (recordTable !== undefined && recordTable.length > 0) {
997
+ localDisplayLayer(attri, undefined, sqlRule === 'OR')
998
+ }
999
+ else {
1000
+ raiseEvent("clicked", JSON.stringify({ request: 'displayLayer', value: attri.name, rule: sqlRule == 'AND' ? generateSQLSummary(data, sqlRule, attri.name) : '1=1' }))
1001
+
1002
+ }
1003
+
1004
+ }
1005
+ else if (attri.display) {
1006
+ const dataCopy = structuredClone(data)
1007
+ let result: Attribute[]
1008
+ if (recordTable !== undefined && recordTable.length > 0) {
1009
+ result = dataCopy.map(item =>
1010
+ item.name === attri.name ? { ...item, display: false } : item
1011
+ );
1012
+ }
1013
+ else {
1014
+ result = dataCopy.map(item =>
1015
+ item.name === attri.name ? { ...item, display: false, uniqValue: undefined } : item
1016
+ );
1017
+ }
1018
+ setProperty("data" as any, result);
1019
+ openAttriName.current = ''
1020
+ props.openedAttribute = undefined
1021
+
1022
+ }
1023
+ };
1024
+ const handleShowAllClick = () => {
1025
+ setShowAll(!showAll);
1026
+ };
1027
+ function deleteSelection(attri: Attribute) {
1028
+
1029
+ function removeAnotherRule() {
1030
+ const updatedData = data?.map((attribute) => {
1031
+ if (attribute.name === attri.name) {
1032
+ return {
1033
+ ...attribute,
1034
+ selection: undefined,
1035
+ uniqValue: recordTable && recordTable.length > 0 ? attri.uniqValue : undefined
1036
+ };
1037
+ }
1038
+ if (attribute.name === openAttriName.current && tickedBeforeNewQuery.current !== undefined) {
1039
+ return {
1040
+ ...attribute,
1041
+ selection: tickedBeforeNewQuery.current,
1042
+ };
1043
+ }
1044
+ return attribute;
1045
+ })
1046
+ setProperty("data" as any, updatedData);
1047
+ }
1048
+
1049
+
1050
+ if (IsOtherRulesEmpty(attri.name)) {
1051
+ setProperty("summary" as any, 0);
1052
+ tempSummary.current = 0
1053
+ numberRecord.current[attri.name] = 0
1054
+
1055
+ if (openAttriName.current !== '') {
1056
+ if (recordTable !== undefined && recordTable.length > 0) {
1057
+ localDisplayLayer(data?.find(attribute => attribute.name == openAttriName.current) || data[0], undefined, true)
1058
+ }
1059
+ else {
1060
+ raiseEvent("clicked", JSON.stringify({ request: 'displayLayer', value: openAttriName.current, rule: '1=1' }))
1061
+ }
1062
+ }
1063
+ const updatedData = data?.map((attribute) => {
1064
+ if (attribute.name === attri.name) {
1065
+ return {
1066
+ ...attribute,
1067
+ selection: [],
1068
+ };
1069
+ }
1070
+ return attribute;
1071
+ })
1072
+ setProperty("data" as any, updatedData);
1073
+
1074
+ }
1075
+ else {
1076
+ if (openAttriName.current == attri.name) {
1077
+ if (data?.find(attribute => attribute.name == attri.name)) {
1078
+ const updatedData = data?.map((obj) => {
1079
+ if (obj.name === attri.name) {
1080
+ return { ...obj, selection: undefined };
1081
+ }
1082
+ return obj
1083
+ })
1084
+ setProperty("data" as any, updatedData);
1085
+
1086
+ }
1087
+ if (recordTable !== undefined && recordTable.length > 0) {
1088
+ const newCount = filterFeatures(recordTable, data, sqlRule, attri.name).length
1089
+ setProperty("summary" as any, newCount)
1090
+ }
1091
+ else {
1092
+ raiseEvent("clicked", JSON.stringify({ request: 'sumOnly', rule: generateSQLSummary(data, sqlRule, attri.name) }))
1093
+ }
1094
+ }
1095
+ else {
1096
+ const targetObject = data?.find(obj => obj.name == openAttriName.current);
1097
+
1098
+
1099
+ if (targetObject && targetObject.uniqValue) {
1100
+ tickedBeforeNewQuery.current = targetObject.selection
1101
+ if (recordTable !== undefined && recordTable.length > 0) {
1102
+ localDisplayLayer(data?.find(obj => obj.name == openAttriName.current) || data[0], attri.name)
1103
+ const newCount = filterFeatures(recordTable, data, sqlRule, attri.name).length
1104
+ setProperty("summary" as any, newCount)
1105
+ }
1106
+ else {
1107
+ raiseEvent("clicked", JSON.stringify({ request: 'displayLayer', value: openAttriName.current, rule: generateSQLSummary(data, sqlRule, [openAttriName.current, attri.name]) }))
1108
+ }
1109
+ //origianlly newQueery
1110
+ removeAnotherRule()
1111
+ }
1112
+ else {
1113
+ if (recordTable !== undefined && recordTable.length > 0) {
1114
+ const newCount = filterFeatures(recordTable, data, sqlRule, attri.name).length
1115
+ setProperty("summary" as any, newCount)
1116
+ const updatedData = data?.map((obj) => {
1117
+ if (obj.name === attri.name) {
1118
+ return { ...obj, selection: undefined };
1119
+ }
1120
+ return obj
1121
+ })
1122
+ setProperty("data" as any, updatedData);
1123
+ }
1124
+ else {
1125
+ raiseEvent("clicked", JSON.stringify({ request: 'sumOnly', value: attri.name, rule: generateSQLSummary(data, sqlRule, attri.name) }))
1126
+ }
1127
+ }
1128
+ }
1129
+
1130
+ }
1131
+
1132
+ }
1133
+
1134
+ const handleDeleteSelection = (attri: Attribute) => {
1135
+ deleteSelection(attri)
1136
+ raiseEvent("clicked", JSON.stringify({ request: 'updateMap', rule: generateSQLSummary(data, sqlRule, attri.name) }))
1137
+ };
1138
+
1139
+ function detectNumberAttribute(attribute) {
1140
+ const name = attribute.name.toLowerCase();;
1141
+ if (name.includes("id") || name.includes("name") || name.includes("mobile") || name.includes("phone")) {
1142
+ return false;
1143
+ }
1144
+ if (attribute.uniqValue) {
1145
+ const phoneNumberPattern = /^04\d{8}$/;
1146
+ let allPhoneNumbers = true;
1147
+
1148
+ for (let i = 0; i < 5; i++) {
1149
+
1150
+ if (i >= attribute.uniqValue.length) break; // In case there are less than 5 elements
1151
+ const value: string = attribute.uniqValue[i].value;
1152
+ if (isNaN(Number(value))) {
1153
+ return false;
1154
+
1155
+ }
1156
+ if (!phoneNumberPattern.test(value) && allPhoneNumbers == true) {
1157
+ allPhoneNumbers = false
1158
+ }
1159
+ }
1160
+ if (allPhoneNumbers) { return false }
1161
+
1162
+ return true;
1163
+ }
1164
+ return false;
1165
+
1166
+ }
1167
+
1168
+
1169
+ function findMinMax(attribute) {
1170
+ let min = Infinity;
1171
+ let max = -Infinity;
1172
+
1173
+ if (attribute.uniqValue) {
1174
+ attribute.uniqValue.forEach(item => {
1175
+ const num = Number(item.value);
1176
+ if (!isNaN(num)) {
1177
+ if (num < min) min = num;
1178
+ if (num > max) max = num;
1179
+ }
1180
+ });
1181
+
1182
+ return { min: Math.floor(min * 100) / 100, max: Math.floor(max * 100) / 100 };
1183
+ }
1184
+ return { min: -Infinity, max: Infinity };
1185
+ }
1186
+
1187
+ const toggleFolder = () => setFolderOpen(!folderOpen);
1188
+
1189
+
1190
+ const renderAttribute = (attribute: Attribute): React.ReactNode => {
1191
+
1192
+ let ISnumberAttribute = false
1193
+ let attributeMin = -Infinity
1194
+ let attributeMax = Infinity
1195
+
1196
+ if (attribute.uniqValue && attribute.uniqValue.length > 0) {
1197
+ attributeMin = findMinMax(attribute).min
1198
+ attributeMax = findMinMax(attribute).max
1199
+ const numberTypes = ['integer', 'number', 'float', 'short', 'long', 'big integer'];
1200
+ ISnumberAttribute = numberTypes.includes(attribute?.type || '--') || detectNumberAttribute(attribute)
1201
+ }
1202
+
1203
+
1204
+ let displayedValues: fieldValue[] = []
1205
+ let displayedFilterValues: fieldValue[] = []
1206
+ let limitInUse
1207
+ if (recordTable !== undefined && recordTable.length > 0) {
1208
+ limitInUse = (attribute.type == 'date' && (dayjs.unix(minDate.current).format('DD/MM/YYYY') != startDate.format('DD/MM/YYYY') || (dayjs.unix(maxDate.current).format('DD/MM/YYYY') != endDate.format('DD/MM/YYYY')))) || searchValue.length > 0 || (minValue !== -Infinity || maxValue !== Infinity)
1209
+ }
1210
+ else {
1211
+ limitInUse = (attribute.type == 'date' && (dayjs.unix(minDate.current).format('DD/MM/YYYY') != startDate.format('DD/MM/YYYY') || (dayjs.unix(maxDate.current).format('DD/MM/YYYY') != endDate.format('DD/MM/YYYY')))) || searchValue.length > 0 || (minValue !== -Infinity || maxValue !== Infinity)
1212
+ }
1213
+ if (attribute.uniqValue && attribute.uniqValue.length > 0) {
1214
+ displayedValues = (attribute.uniqValue.length < defaultNumber + 2) || showAll ? attribute.uniqValue : attribute.uniqValue.slice(0, defaultNumber);
1215
+ }
1216
+
1217
+ if (limitInUse && filteredValues.length > 0) {
1218
+ displayedFilterValues = ((filteredValues.length < defaultNumber + 2) || showAll) ? filteredValues : filteredValues.slice(0, defaultNumber)
1219
+ }
1220
+
1221
+ const handleDateRangeChange = (newStart: Dayjs, newEnd: Dayjs) => {
1222
+ setProperty("startDate" as any, newStart || defaultStart);
1223
+ setProperty("endDate" as any, newEnd || defaultEnd)
1224
+ const filterrangeResult = filterDateRange(attribute, newStart, newEnd)
1225
+ setProperty("filteredValues" as any, filterrangeResult)
1226
+ };
1227
+
1228
+ const maxStringLength = 28
1229
+ function itemText(item: fieldValue, type?: string) {
1230
+ let rawResult: string = ''
1231
+ if (type == 'number') {
1232
+ rawResult = ((Math.floor(Number(item.value) * 100) / 100)).toString()
1233
+ }
1234
+ else { rawResult = item.value }
1235
+
1236
+ return rawResult.length > maxStringLength ? rawResult.substring(0, maxStringLength - 3) + '...' : rawResult;
1237
+ }
1238
+
1239
+ const chipLabel = attribute.selection && attribute.selection.length > 1
1240
+ ? `${attribute.selection.length} selected`
1241
+ : attribute.selection && attribute.selection.length > 0 && attribute.type === 'date'
1242
+ ? attribute.selection[0]
1243
+ : attribute.selection && attribute.selection.length > 0 && attribute.selection[0].length > 8
1244
+ ? `${attribute.selection[0].substring(0, 6)}..`
1245
+ : attribute.selection ? attribute.selection[0] : '';
1246
+
1247
+ const chipElement = (
1248
+ <Chip
1249
+ label={chipLabel}
1250
+ sx={{
1251
+ color: 'var(--accentIconBackground)',
1252
+ backgroundColor: 'var(--alertAmberBackground)',
1253
+ '& .MuiChip-deleteIcon': { color: 'var(--accentIconBackground)' }
1254
+ }}
1255
+ onDelete={() => handleDeleteSelection(attribute)}
1256
+ />
1257
+ );
1258
+
1259
+ const exceptions = ['OBJECTID', 'Shape__Area', 'Shape__Length'];
1260
+ if (!exceptions.includes(attribute.name)) {
1261
+
1262
+ return (
1263
+ <React.Fragment key={attribute.name}>
1264
+ <ListItemButton onClick={() => handleAttributeClick(attribute)}
1265
+ sx={{
1266
+ // Conditionally change background color based on attribute.display
1267
+ backgroundColor: attribute.display ? 'rgba(197, 197, 197, 0.65)' : 'transparent',
1268
+ }}
1269
+ >
1270
+ { // render icon image
1271
+ attribute.name ? (
1272
+ <Box
1273
+ component="img"
1274
+ sx={{
1275
+ height: 18,
1276
+ width: 18,
1277
+ }}
1278
+ alt="The category icon"
1279
+ src={attribute.img ? attribute.img :
1280
+ attribute.type == 'date' ? 'https://cdn-icons-png.flaticon.com/512/6706/6706193.png' :
1281
+ ISnumberAttribute ? 'https://cdn-icons-png.flaticon.com/512/5141/5141667.png' :
1282
+ "https://cdn-icons-png.flaticon.com/24/2303/2303496.png"}// Use the item's img property or based on type
1283
+ />
1284
+ ) :
1285
+ //on purpose keep not in use code in case
1286
+ (<ListItemIcon sx={{ minWidth: '30px' }}>
1287
+ <FolderOpenIcon sx={{ color: 'var(--accentIconBorder)' }} />
1288
+ </ListItemIcon>)
1289
+ }
1290
+ <ListItemText primary={attribute.alias || attribute.name} sx={{ pl: 3 }} primaryTypographyProps={{ style: { fontWeight: 800 } }} />
1291
+
1292
+ {//selection chip
1293
+ attribute.selection && attribute.selection.length > 0 ?
1294
+ attribute.selection.length > 1 ? <LightTooltip
1295
+ title={attribute.selection.join(',')}
1296
+ sx={{ fontSize: '2rem' }} arrow>
1297
+ {chipElement}
1298
+ </LightTooltip>
1299
+ : chipElement : <></>}
1300
+ {attribute.display ? <ExpandLess /> : <ExpandMore />}
1301
+
1302
+ </ListItemButton>
1303
+ {attribute.uniqValue ?
1304
+ (<Collapse in={attribute.display} timeout="auto" unmountOnExit>
1305
+ {attribute.type && attribute.type == 'date' && (attribute.uniqValue.length > 6) ?
1306
+ <Box display="flex" alignItems="center" >
1307
+ <LocalizationProvider dateAdapter={AdapterDayjs} >
1308
+ <DemoContainer components={['DatePicker', 'DatePicker']} >
1309
+ <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="en-au">
1310
+ <DateRangePicker defaultValue={[dayjs.unix(minDate.current), dayjs.unix(maxDate.current)]} maxDate={dayjs.unix(maxDate.current)} minDate={dayjs.unix(minDate.current)}
1311
+ value={[startDate, endDate]}
1312
+ onChange={([newstart, newend]) => handleDateRangeChange(newstart || startDate, newend || endDate)}
1313
+ sx={{
1314
+ // Customize the date text color
1315
+ '& .MuiInputBase-input': {
1316
+ color: 'var(--accentIconBorder)', // Change text color for the selected date
1317
+ },
1318
+
1319
+ // Customize the start & end labels
1320
+ '& .MuiInputLabel-root': {
1321
+ color: 'var(--accentIconBorder)', // Change label color for both start and end dates
1322
+ },
1323
+
1324
+ // Customize the border color of the input
1325
+ '& .MuiOutlinedInput-root': {
1326
+ '& fieldset': {
1327
+ borderColor: 'var(--accentIconBorder)', // Change border color when the input is not focused
1328
+ },
1329
+ '&:hover fieldset': {
1330
+ borderColor: 'var(--accentIconBorder)', // Change border color on hover
1331
+ },
1332
+ '&.Mui-focused fieldset': {
1333
+ borderColor: 'var(--accentIconBorder)', // Change border color when focused
1334
+ },
1335
+ },
1336
+ }}
1337
+
1338
+ />
1339
+ </LocalizationProvider>
1340
+ </DemoContainer>
1341
+ </LocalizationProvider>
1342
+ </Box>
1343
+ :
1344
+ ISnumberAttribute && (attribute.uniqValue.length > 6) ?
1345
+ //Display String search or number Range , don't display if value amount is limited
1346
+ <Box display="flex" alignItems="center">
1347
+ <CustomNumberInput aria-label="min value input" placeholder={`Min (${attributeMin} ~ ${attributeMax})`} min={attributeMin - 1} max={attributeMax + 1}
1348
+ value={minValue == -Infinity ? undefined : minValue} step={0.1}
1349
+ onChange={(event, val) => {
1350
+ if (((val || val == 0) && (val <= maxValue))) { setProperty("minValue" as any, val) };
1351
+ if (val == undefined || val == null) { setProperty("minValue" as any, -Infinity) };
1352
+
1353
+ if ((val || val == 0) && val > maxValue) {
1354
+ setProperty("minValue" as any, maxValue - 1)
1355
+ const filterrangeResult = filterRange(attribute, maxValue - 1, maxValue)
1356
+ setProperty("filteredValues" as any, filterrangeResult)
1357
+ }
1358
+ else {
1359
+ const filterrangeResult = filterRange(attribute, val !== null ? val : -Infinity, maxValue !== Infinity ? maxValue : Infinity)
1360
+ setProperty("filteredValues" as any, filterrangeResult)
1361
+ }
1362
+ }}
1363
+ />
1364
+ <CustomNumberInput aria-label="max value input" placeholder={`Max (${attributeMin} ~ ${attributeMax})`} min={attributeMin - 1} max={attributeMax + 1}
1365
+ value={maxValue == Infinity ? undefined : maxValue} step={0.1}
1366
+ onChange={(_event, val) => {
1367
+ if ((val || val == 0) && (val >= minValue)) { setProperty("maxValue" as any, val) };
1368
+ if (val == undefined) { setProperty("maxValue" as any, Infinity) };
1369
+ if ((val || val == 0) && val < minValue) {
1370
+ setProperty("maxValue" as any, minValue + 1)
1371
+ const filterrangeResult = filterRange(attribute, minValue, minValue + 1)
1372
+ setProperty("filteredValues" as any, filterrangeResult)
1373
+ }
1374
+ else {
1375
+ const filterrangeResult = filterRange(attribute, minValue !== -Infinity ? minValue : -Infinity, val !== null ? val : Infinity || Infinity)
1376
+ setProperty("filteredValues" as any, filterrangeResult)
1377
+ }
1378
+ }}
1379
+ />
1380
+ </Box>
1381
+ :
1382
+ (attribute.uniqValue.length > 6) || searchValue ?
1383
+ <Search sx={{ border: 0.5, borderColor: 'var(--alertGrayForegroundHover)', marginLeft: '0px !important', marginRight: '0px' }} >
1384
+ <SearchIconWrapper>
1385
+ <SearchIcon />
1386
+ </SearchIconWrapper>
1387
+ <StyledInputBase
1388
+ placeholder="Search specific values..."
1389
+ inputProps={{ 'aria-label': 'search' }}
1390
+ value={searchValue}
1391
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => handleSearchValueChange(attribute, event)}
1392
+ sx={{ padding: '1px', fontSize: '1.2rem', color: 'var(--alertGrayBackground)', width: '100%' }}
1393
+
1394
+ />
1395
+ {searchValue && (
1396
+ <IconButton
1397
+ onClick={() => {
1398
+ setProperty("searchValue" as any, '');
1399
+ setProperty("filteredValues" as any, [])
1400
+ }}
1401
+ size="small"
1402
+ sx={{ position: 'absolute', right: '8px', color: 'var(--alertGrayBackground)' }}
1403
+ >
1404
+ <ClearIcon />
1405
+ </IconButton>
1406
+ )}
1407
+ </Search> : <></>
1408
+ }
1409
+ {((!limitInUse && attribute.uniqValue && attribute.uniqValue.length > 2) ||
1410
+ (limitInUse && filteredValues.length > 2)) ?
1411
+
1412
+ <ListItemButton sx={{ pl: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }} onClick={handleShowAllClick}>
1413
+ <div style={{ display: 'flex', alignItems: 'center', flexGrow: 1 }}>
1414
+ {
1415
+ // Display SHOW-ALL or not
1416
+ ((!limitInUse && attribute.uniqValue && attribute.uniqValue.length >= defaultNumber + 2) ||
1417
+ (limitInUse && filteredValues.length >= defaultNumber + 2)) ?
1418
+ <ListItemText primary={`Show ${showAll ? 'less' : 'all others▼'} ${showAll ? '▲' : `(${filteredValues.length > defaultNumber && limitInUse ? filteredValues.length - defaultNumber : attribute.uniqValue.length - defaultNumber})`}`} primaryTypographyProps={{ style: { fontWeight: '500' } }} />
1419
+ : null
1420
+ }
1421
+ </div>
1422
+ <Button style={{ color: 'var(--accentIconBorder) !important' }} variant="text" onClick={(event) => handleSelectAll(event, attribute.name)}>
1423
+ {allButtonText}
1424
+ </Button>
1425
+ </ListItemButton> : <></>}
1426
+ {
1427
+ // Situation1: Search or range & search result found, only show 'DISPLAYED FILTER VALUE' which is controlled by showall to be all or first 4
1428
+ (limitInUse && filteredValues.length > 0) ?
1429
+ (<List component="div" disablePadding>
1430
+ {
1431
+ displayedFilterValues.map((item, idx) => (
1432
+ item.count > 0 ?
1433
+ item.value.length > maxStringLength ?//control tooltip exist or not
1434
+ <ListItemButton sx={{ pl: 3, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }} key={idx} onClick={() => handleItemClick(attribute, item)}>
1435
+ <LightTooltip title={`${item.value} (${item.count})`} sx={{ fontSize: '2rem' }} arrow>
1436
+ <div style={{ display: 'flex', alignItems: 'center' }}>
1437
+ <Checkbox sx={{ color: 'var(--accentIconBorder) !important' }} checked={Boolean(attribute.selection?.includes(item.value))} />
1438
+ <ListItemText primary={itemText(item, attribute.type)} primaryTypographyProps={{ style: { fontWeight: '500' } }} />
1439
+ </div>
1440
+ </LightTooltip>
1441
+ <StyledBadge max={9999} badgeContent={item.count} color="primary" />
1442
+ </ListItemButton> :
1443
+ <ListItemButton sx={{ pl: 3, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }} key={idx} onClick={() => handleItemClick(attribute, item)}>
1444
+ <div style={{ display: 'flex', alignItems: 'center' }}>
1445
+ <Checkbox sx={{ color: 'var(--accentIconBorder) !important' }} checked={Boolean(attribute.selection?.includes(item.value))} />
1446
+ <ListItemText primary={itemText(item, attribute.type)} primaryTypographyProps={{ style: { fontWeight: '500' } }} />
1447
+ </div>
1448
+ <StyledBadge max={9999} badgeContent={item.count} color="primary" />
1449
+ </ListItemButton>
1450
+ : <></>
1451
+ ))}
1452
+ </List>) : (limitInUse && (filteredValues.length == 0)) ?
1453
+ // Situation2: search or range but no result
1454
+ <Typography variant="body1" sx={{ padding: '10px', fontSize: '1.5rem' }}>No matching values</Typography>
1455
+ :
1456
+ attribute.uniqValue?.length == 0 ?
1457
+ // Situation3: No data at beginning
1458
+ <Typography variant="body1" sx={{ padding: '10px', fontSize: '1.5rem', color: 'red' }}>No any value for this attribute.</Typography>
1459
+ // Situation4: no searching or range, show default displayed Values, controlled by showall
1460
+ : <List component="div" disablePadding>
1461
+ {attribute.uniqValue && attribute.uniqValue.length > 0 && displayedValues.map((item, idx) => (
1462
+ item.count > 0 ?
1463
+ item.value.length > maxStringLength ?
1464
+ <ListItemButton sx={{ pl: 3, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }} key={idx} onClick={() => handleItemClick(attribute, item)}>
1465
+ <LightTooltip title={`${item.value} (${item.count})`} sx={{ fontSize: '2rem' }} arrow>
1466
+ <div style={{ display: 'flex', alignItems: 'center' }}>
1467
+ <Checkbox sx={{ color: 'var(--accentIconBorder) !important' }}
1468
+ checked={Boolean(attribute.selection?.includes(item.value))}
1469
+ onChange={() => handleItemClick(attribute, item)}
1470
+ />
1471
+ <ListItemText primary={itemText(item, attribute.type)} primaryTypographyProps={{ style: { fontWeight: '500' } }} />
1472
+ </div>
1473
+ </LightTooltip>
1474
+ <StyledBadge badgeContent={item.count} max={9999} color="primary" />
1475
+ </ListItemButton> :
1476
+ <ListItemButton sx={{ pl: 3, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }} key={idx} onClick={() => handleItemClick(attribute, item)}>
1477
+ <div style={{ display: 'flex', alignItems: 'center' }}>
1478
+ <Checkbox sx={{ color: 'var(--accentIconBorder) !important' }}
1479
+ checked={Boolean(attribute.selection?.includes(item.value))}
1480
+ onChange={() => handleItemClick(attribute, item)}
1481
+ />
1482
+ <ListItemText primary={itemText(item, attribute.type)} primaryTypographyProps={{ style: { fontWeight: '500' } }} />
1483
+ </div>
1484
+ <StyledBadge badgeContent={item.count} max={9999} color="primary" />
1485
+ </ListItemButton>
1486
+ : <></>
1487
+ ))
1488
+ }
1489
+ </List>
1490
+ }
1491
+ </Collapse>) : <></>}
1492
+ </React.Fragment>
1493
+ );
1494
+ }
1495
+ };
1496
+
1497
+ function filterData(data: Attribute[], query: string) {
1498
+ const keywords = query.split(/[\s,+;]+/).map((word) => word.trim().toLowerCase()).filter(Boolean);
1499
+ const dataCopy = structuredClone(data)
1500
+ const matchedItems = dataCopy.filter(item =>
1501
+ keywords.every(keyword => item.name.toLowerCase().includes(keyword)
1502
+ || item.alias?.toLowerCase().includes(keyword))
1503
+ );
1504
+
1505
+ return matchedItems;
1506
+ }
1507
+
1508
+ function sortItemsBySelection(items: fieldValue[] | undefined, selection: string[] | undefined) {
1509
+ const selectedSet = new Set(selection);
1510
+
1511
+ const selected: fieldValue[] = [];
1512
+ const unselected: fieldValue[] = [];
1513
+ if (items) {
1514
+ for (const item of items) {
1515
+ if (selectedSet.has(item.value)) {
1516
+ selected.push(item);
1517
+ } else {
1518
+ unselected.push(item);
1519
+ }
1520
+ }
1521
+ return [...selected, ...unselected];
1522
+ }
1523
+ else return []
1524
+ }
1525
+
1526
+
1527
+ function filterValue(attri: Attribute, query: string) {
1528
+ const keywords = query.split(/[\s,+;]+/).map((word) => word.trim().toLowerCase()).filter(Boolean);
1529
+ const matchedItems = attri.uniqValue?.filter(item =>
1530
+ keywords.every(keyword => item.value.toLowerCase().includes(keyword))
1531
+ );
1532
+ return sortItemsBySelection(matchedItems,attri.selection) || [];
1533
+ }
1534
+
1535
+ function filterRange(attri: Attribute, min: number, max: number) {
1536
+ const matchedItems = attri.uniqValue?.filter(item =>
1537
+ Number(item.value) <= max && Number(item.value) >= min
1538
+ );
1539
+ return sortItemsBySelection(matchedItems,attri.selection) || []
1540
+ }
1541
+
1542
+ const groupByMelbourneDate = (data: fieldValue[]) => {
1543
+ const groupedData = new Map();
1544
+
1545
+ data.forEach(({ value: value, count: count }) => {
1546
+ const melbourneDate = dayjs(value, "DD/MM/YYYY").tz('Australia/Melbourne').format('DD/MM/YYYY');
1547
+ if (!groupedData.has(melbourneDate)) {
1548
+ groupedData.set(melbourneDate, { value: melbourneDate, count: 0 });
1549
+ }
1550
+ groupedData.get(melbourneDate).count += count;
1551
+ });
1552
+ // Convert the map back to an array of objects
1553
+ return Array.from(groupedData.values());
1554
+ };
1555
+
1556
+ function filterDateRange(attri: Attribute, start: Dayjs, end: Dayjs) {
1557
+ if (!attri.uniqValue) return []; // Handle null or undefined attri.uniqValue
1558
+
1559
+ const matchedItems = attri.uniqValue.filter((item) => {
1560
+ const itemDate = dayjs(item.value, 'DD/MM/YYYY'); // Parse the date once
1561
+ if (!itemDate.isValid()) return false; // Handle invalid dates gracefully
1562
+ return itemDate.isAfter(start, 'day') && itemDate.isBefore(end, 'day') ||
1563
+ itemDate.isSame(start, 'day') || itemDate.isSame(end, 'day'); // Inclusive comparison
1564
+ });
1565
+
1566
+ return sortItemsBySelection(matchedItems,attri.selection);
1567
+ }
1568
+
1569
+
1570
+ React.useEffect(() => {
1571
+
1572
+ function filterValue2(attri: Attribute, query: string) {
1573
+ const keywords = query.split(/[\s,+;]+/).map((word) => word.trim().toLowerCase()).filter(Boolean);
1574
+ const matchedItems = attri.uniqValue?.filter(item =>
1575
+ keywords.every(keyword => item.value.toLowerCase().includes(keyword))
1576
+ );
1577
+ return sortItemsBySelection(matchedItems,attri.selection) || [];
1578
+ }
1579
+ if (props.openedAttribute !== undefined && (openAttriTime.current) !== (props.openedAttribute.timestamp)) {
1580
+ let newAttriData: fieldValue[] | undefined
1581
+
1582
+ if (props.openedAttribute.data.type == 'date' && props.openedAttribute.data.uniqValue) {
1583
+ const values = props.openedAttribute.data.uniqValue.map(obj => Number(obj.value));
1584
+ minDate.current = Math.min(...values) / 1000;
1585
+ maxDate.current = Math.max(...values) / 1000;
1586
+ setProperty("startDate" as any, dayjs.unix(Math.min(...values) / 1000))
1587
+ setProperty("endDate" as any, dayjs.unix(Math.max(...values) / 1000))
1588
+ newAttriData = groupByMelbourneDate(props.openedAttribute.data.uniqValue)
1589
+ }
1590
+ else {
1591
+ newAttriData = props.openedAttribute.data.uniqValue
1592
+ }
1593
+ if (newAttriData) {
1594
+ newAttriData.sort((a, b) => {
1595
+ if (a.count === b.count) {
1596
+ return a.value.localeCompare(b.value);
1597
+ }
1598
+ return b.count - a.count;
1599
+ });
1600
+ }
1601
+
1602
+ const updatedData = data?.map((obj) => {
1603
+ if (obj.name === props.openedAttribute?.data.name) {
1604
+ return {
1605
+ ...props.openedAttribute?.data,
1606
+ uniqValue: obj.selection && obj.selection.length > 0 ? sortItemsBySelection(newAttriData, obj.selection) : newAttriData,
1607
+ selection: obj.selection, // Keep the selection attribute from the previous data
1608
+ hidden: obj.hidden,
1609
+ display: true,
1610
+ };
1611
+ }
1612
+ return { ...obj, display: false, uniqValue: undefined };
1613
+ })
1614
+ setProperty("data" as any, updatedData);
1615
+
1616
+ const oldSelection = DataRef.current.find(obj => obj.name === openAttriName.current)?.selection || []
1617
+
1618
+ if (openAttriName.current == props.openedAttribute?.data.name && oldSelection.length > 0 && searchValue.length > 0) {
1619
+ const searchValueResults = filterValue2(props.openedAttribute.data, searchValue);
1620
+ setProperty("filteredValues" as any, searchValueResults)
1621
+ }
1622
+ const totalDifference = props.openedAttribute.data.uniqValue?.filter(value => oldSelection?.includes(value.value))?.reduce((acc, item) => {
1623
+ const originalItemValues = openAttriValues.current?.uniqValue
1624
+ const originalItem = originalItemValues?.find(oldItem => oldItem.value == item.value);
1625
+ if (originalItem) {
1626
+ acc += item.count - originalItem.count;
1627
+ }
1628
+ return acc;
1629
+ }, 0) || 0;
1630
+
1631
+ setProperty("summary" as any, summary + totalDifference)
1632
+ openAttriName.current = props.openedAttribute?.data.name
1633
+ openAttriTime.current = props.openedAttribute?.timestamp
1634
+ openAttriValues.current = props.openedAttribute.data
1635
+
1636
+
1637
+
1638
+
1639
+ }
1640
+ }, [data, props.openedAttribute, searchValue, setProperty, summary])
1641
+
1642
+ React.useEffect(() => {
1643
+ const selectedAttribute = data?.find(attribute => attribute.name === openAttriName.current);
1644
+ if (selectedAttribute && (!selectedAttribute.selection || selectedAttribute.selection.length === 0)) {
1645
+ setProperty("allButtonText" as any, 'Select All');
1646
+ }
1647
+ else if (selectedAttribute && selectedAttribute.selection?.length == selectedAttribute.uniqValue?.length) {
1648
+ setProperty("allButtonText" as any, 'Deselect All');
1649
+ }
1650
+ }, [data, setProperty]);
1651
+
1652
+
1653
+ React.useEffect(() => {
1654
+ if (props.request !== undefined && (requestTime.current) !== (props.request.timestamp)) {
1655
+ if (props.request.requestName == "Clear") {
1656
+ const updatedData = data?.map((attribute) => {
1657
+ if (attribute.selection && attribute.selection.length > 0) {
1658
+ attribute.selection = undefined
1659
+ }
1660
+ return attribute
1661
+ }
1662
+ )
1663
+ setProperty("data" as any, updatedData);
1664
+ setProperty("summary" as any, 0)
1665
+ }
1666
+ setProperty("rule" as any, generateSQLSummary(data, sqlRule))
1667
+ setProperty("layerName" as any, layerNameCreator(data, sqlRule))
1668
+ raiseEvent("clicked", JSON.stringify({ request: props.request.requestName, rule: generateSQLSummary(data, sqlRule), layerName: layerNameCreator(data, sqlRule) }))
1669
+ requestTime.current = props.request.timestamp
1670
+ }
1671
+ }, [data, props.request, raiseEvent, setProperty, sqlRule]);
1672
+
1673
+
1674
+ React.useEffect(() => {
1675
+ if (searchQuery == undefined) { setProperty("searchQuery" as any, '') }
1676
+ if (searchValue == undefined) { setProperty("searchValue" as any, '') }
1677
+ if (summary == undefined) { setProperty("summary" as any, 0) }
1678
+ if (sqlRule == undefined) { setProperty("sqlRule" as any, "AND") }
1679
+ if (rule == undefined) { setProperty("rule" as any, "") }
1680
+ if (layerName == undefined) { setProperty("layerName" as any, "") }
1681
+ if (minValue == undefined) { setProperty("minValue" as any, -Infinity) }
1682
+ if (maxValue == undefined) { setProperty("maxValue" as any, Infinity) }
1683
+ if (startDate == undefined) { setProperty("startDate" as any, defaultStart) }
1684
+ if (endDate == undefined) { setProperty("endDate" as any, defaultEnd) }
1685
+ if (filteredValues == undefined) { setProperty("filteredValues" as any, []) }
1686
+ if (sqlRule== undefined) {props.setProperty("sqlRule" as any, "AND") }
1687
+ }, [defaultEnd, defaultStart, endDate, filteredValues, layerName, maxValue, minValue, props, rule, searchQuery, searchValue, setProperty, sqlRule, startDate, summary]);
1688
+
1689
+ React.useEffect(() => {
1690
+ if (searchQuery && searchQuery.length > 0) {
1691
+ const searchResults = filterData(data, searchQuery);
1692
+ const dataCopy = structuredClone(data)
1693
+ const result = dataCopy.map(item => {
1694
+ const isVisible = searchResults.some(filteredItem => filteredItem.name === item.name);
1695
+ return { ...item, visible: isVisible };
1696
+ });
1697
+
1698
+ setProperty("data" as any, result);
1699
+ }
1700
+
1701
+ else if (prevSearchValue.current !== '') {
1702
+ const dataCopy = structuredClone(data)
1703
+ const result = dataCopy.map(item => {
1704
+ return { ...item, visible: true };
1705
+ });
1706
+ setProperty("data" as any, result);
1707
+
1708
+ }
1709
+ prevSearchValue.current = searchQuery
1710
+ }, [data, searchQuery, setProperty])
1711
+
1712
+
1713
+ React.useEffect(() => {
1714
+ if (props.newSum !== undefined && props.newSum !== prevSum.current) {
1715
+ setProperty("summary" as any, props.newSum.sum)
1716
+ numberRecord.current['total'] = props.newSum.sum
1717
+ if (!DataRef.current.find(obj => obj.name == openAttriName.current)?.selection || DataRef.current.find(obj => obj.name == openAttriName.current)?.selection?.length == 0) {
1718
+ tempPreviousSummary.current = props.newSum.sum
1719
+ }
1720
+ prevSum.current = props.newSum
1721
+ openAttriName.current == ''
1722
+ const updatedData = data?.map((obj) => {
1723
+ if (obj.name === props.newSum?.attri) {
1724
+ return { ...obj, display: false, uniqValue: undefined, selection: undefined };
1725
+ }
1726
+ return obj
1727
+ })
1728
+ setProperty("data" as any, updatedData);
1729
+ }
1730
+ }, [data, props.newSum, setProperty])
1731
+
1732
+ return (
1733
+ <>
1734
+ <Box style={stickyStyle}>
1735
+ {data && data.length > 9 ? <Search sx={{ border: 0.5, borderColor: 'var(--alertGrayForegroundHover)', marginLeft: '0px !important', marginRight: '0px' }} >
1736
+ <SearchIconWrapper>
1737
+ <SearchIcon />
1738
+ </SearchIconWrapper>
1739
+ <StyledInputBase
1740
+ placeholder="Search attribute name..."
1741
+ inputProps={{ 'aria-label': 'search' }}
1742
+ value={searchQuery}
1743
+ onChange={handleSearchChange}
1744
+ sx={{ padding: '1px', fontSize: '1.4rem', color: 'var(--alertGrayBackground)', width: '100%' }}
1745
+ />
1746
+ {searchQuery && (
1747
+ <IconButton
1748
+ onClick={() => { setProperty("searchQuery" as any, '') }}
1749
+ size="large"
1750
+ sx={{ position: 'absolute', right: '8px', color: 'var(--alertGrayBackground)' }}
1751
+ >
1752
+ <ClearIcon />
1753
+ </IconButton>
1754
+ )}
1755
+ </Search> : <></>}
1756
+ {sqlRule && (
1757
+ <RadioGroup
1758
+ //Radio Group
1759
+ row
1760
+ aria-labelledby="demo-row-radio-buttons-group-label"
1761
+ name="row-radio-buttons-group"
1762
+ value={sqlRule}
1763
+ onChange={handleSqlRule}
1764
+ >
1765
+ <FormControlLabel sx={{ fontSize: 15 }} value="AND" control={<Radio sx={{ color: 'var(--accentIconBorder) !important' }}
1766
+ />} label={<span style={{ fontSize: '1.2rem' }}>Meet ALL Conditions</span>} />
1767
+ <FormControlLabel sx={{ fontSize: 15 }} value="OR" control={<Radio sx={{ color: 'var(--accentIconBorder) !important' }} />} label={<span style={{ fontSize: '1.2rem' }}>Meet ANY Conditon</span>} />
1768
+ </RadioGroup>
1769
+ )}
1770
+ {
1771
+ props.footerButton ?
1772
+ <Typography variant="body1" sx={{ fontSize: '1.5rem' }}>Selected items amount: {summary}</Typography>
1773
+ : <Stack spacing={2} direction="row">
1774
+ {props.buttonNaming?.[0] == " " ? <></> : <Button style={{ backgroundColor: summary <= 0 ? 'var(--alertForegroundDisabled)' : 'var(--accentIconForeground)' }} onClick={() => handleBtnClick(props.buttonNaming?.[0] || 'Apply')} variant="contained" size="medium" disabled={summary <= 0}>{(sqlRule == 'AND' || summary == 0 || recordTable?.length > 0) ? (props.buttonNaming?.[0] || 'Apply') + '(' + summary + ")" : (props.buttonNaming?.[0] || 'Apply') + '(' + summary + "+)"}</Button>
1775
+ }
1776
+ {props.buttonNaming?.slice(1).map((name, index) =>
1777
+ name.length > 0 ? (
1778
+ <Button
1779
+ key={index + 1}
1780
+ style={{ backgroundColor: (name.endsWith("()") || name.endsWith("<>")) ? summary <= 0 ? 'var(--alertForegroundDisabled)' : 'var(--accentIconForeground)' : 'var(--accentIconForeground)' }}
1781
+ onClick={() => handleBtnClick(name)}
1782
+ variant="contained" size="medium"
1783
+ disabled={(name.endsWith("()") || name.endsWith("<>"))
1784
+ ? summary <= 0 : false}
1785
+ >
1786
+ {name.endsWith("()")
1787
+ ? (sqlRule == 'AND' || summary == 0 ? `${name.slice(0, -2)}(${summary})` : `${name.slice(0, -2)}(${summary}+)`)
1788
+ :
1789
+ name.endsWith("<>") ? name.slice(0, -2) : name}
1790
+ </Button>
1791
+ ) : null
1792
+ )}
1793
+ <Button style={{ backgroundColor: 'var(--accentIconForeground)' }} onClick={() => handleClearRulesClick()} variant="contained" size="medium" >Clear</Button>
1794
+ </Stack>
1795
+ }
1796
+
1797
+ </Box>
1798
+ <ThemeProvider theme={defaultTheme}>
1799
+ {
1800
+ data && data.every(item => item.visible === false) ?
1801
+ <Typography variant="body1" sx={{ padding: '10px', fontSize: '2rem' }}>No matching attribute name</Typography>
1802
+ :
1803
+ searchQuery && searchQuery.length > 0 ?
1804
+ <List sx={{ width: '100%' }} component="nav">
1805
+ {data?.filter(attribute => attribute.visible).map((attribute) => renderAttribute(attribute))
1806
+ }
1807
+ </List> :
1808
+ <List sx={{ width: '100%' }} component="nav">
1809
+ {data && data.filter(attribute => !attribute.hidden).map((attribute) => renderAttribute(attribute))}
1810
+ {data && data.every(item => !item.hidden) ?
1811
+ null :
1812
+ <><ListItemButton onClick={toggleFolder}>
1813
+ <Box
1814
+ component="img"
1815
+ sx={{
1816
+ height: 22,
1817
+ width: 22,
1818
+ }}
1819
+ alt="The category icon"
1820
+ src={"https://cdn-icons-png.flaticon.com/512/3767/3767084.png"}// Use the item's img property or based on type
1821
+ />
1822
+ <ListItemText primary={folderOpen ? 'Hide' : 'Show all other Attributes..'}
1823
+ sx={{ pl: 5 }} primaryTypographyProps={{ style: { fontWeight: 600, color: "grey" } }} />
1824
+ {folderOpen ? <ExpandLess /> : <ExpandMore />}
1825
+ </ListItemButton>
1826
+ <Collapse in={folderOpen} timeout="auto" unmountOnExit>
1827
+ <List component="div" disablePadding>
1828
+ {data?.filter(attribute => attribute.hidden).map((attribute) => renderAttribute(attribute))}
1829
+ </List>
1830
+ </Collapse></>
1831
+ }
1832
+ </List>
1833
+ }
1834
+ </ThemeProvider>
1835
+ </>
1836
+ );
1837
+ }
1838
+
1839
+ const AttributeFilterElementRegistration: FormElementRegistration<AttributeFilterProps> = {
1840
+ component: AttributeFilter,
1841
+ getInitialProperties: () => ({
1842
+ fields: [{
1843
+ alias: "OBJECTID",
1844
+ display: false,
1845
+ name: "OBJECTID",
1846
+ type: "oid",
1847
+ visible: true
1848
+ },
1849
+ {
1850
+ alias: "Start Date",
1851
+ display: false,
1852
+ name: "StartDate",
1853
+ type: "date",
1854
+ visible: true
1855
+ },
1856
+ {
1857
+ alias: "Requestor",
1858
+ display: false,
1859
+ name: "Requestor",
1860
+ type: "string",
1861
+ visible: true
1862
+ }],
1863
+ }),
1864
+ id: "AttributeFilter",
1865
+ };
1866
+
1867
+ export default AttributeFilterElementRegistration;