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.
- package/.eslintrc.js +7 -0
- package/.vscode/extensions.json +3 -0
- package/.vscode/settings.json +3 -0
- package/README.md +26 -0
- package/build/2983859c0d77824ec8db.svg +1 -0
- package/build/4168d659d55fd94368bc.svg +1 -0
- package/build/641b2c17e92b0e885b9f.svg +1 -0
- package/build/882445a26a4f3964786f.svg +1 -0
- package/build/activitypack.json +1 -1
- package/build/b280e967312a55101032.svg +1 -0
- package/build/main.js +2 -2
- package/build/main.js.LICENSE.txt +99 -0
- package/certs/localhost+2-key.pem +28 -0
- package/certs/localhost+2.pem +26 -0
- package/index.html +58 -0
- package/package.json +43 -7
- package/public/img/background.svg +3 -3
- package/public/img/compass.svg +3 -3
- package/public/img/grab.svg +3 -3
- package/public/img/grabbing.svg +3 -3
- package/public/img/sprites.svg +14 -14
- package/src/activities/AppendObjects.ts +40 -0
- package/src/activities/Base64ToFile.ts +56 -0
- package/src/activities/CreateGraphicLayer.ts +85 -0
- package/src/activities/DrapGeometry.ts +99 -0
- package/src/activities/GetElementByDataLayoutID.ts +58 -0
- package/src/activities/GetSesionStorage.ts +36 -0
- package/src/activities/GetTaskStatus.ts +56 -0
- package/src/activities/GetUserToken.ts +102 -0
- package/src/activities/GetvGISDetails copy.ts +110 -0
- package/src/activities/GetvGISDetails.ts +110 -0
- package/src/activities/HTMLextractor.ts +188 -0
- package/src/activities/LegendCreator.ts +155 -0
- package/src/activities/MergeObjects.ts +61 -0
- package/src/activities/PDFMapGenerator.ts +729 -0
- package/src/activities/RequestChecker.ts +94 -0
- package/src/activities/Sample.ts +41 -0
- package/src/activities/SetSessionStorage.ts +57 -0
- package/src/activities/SliceLine.ts +61 -0
- package/src/elements/AttributeFilter.tsx +1867 -0
- package/src/elements/CircularSlider.tsx +67 -0
- package/src/elements/CsColorPicker.tsx +140 -0
- package/src/elements/DropDownMenu.tsx +282 -0
- package/src/elements/HeaderwithHelp.tsx +178 -0
- package/src/elements/ItemSearcher.tsx +1035 -0
- package/src/elements/ItemSearcher_v2.tsx +1071 -0
- package/src/elements/LineChartMui.tsx +50 -0
- package/src/elements/MenueBar.tsx +118 -0
- package/src/elements/MultipleAutoCompleteCS.tsx +145 -0
- package/src/elements/SingleAutoCompleteCS.tsx +133 -0
- package/src/elements/TableRadioGoup.tsx +163 -0
- package/src/elements/Timeline.tsx +104 -0
- package/src/elements/ToggleButtonGroup.tsx +129 -0
- package/src/elements/Viewer360.tsx +71 -0
- package/src/elements/modules/AttributeFilterEasyComp.tsx +253 -0
- package/src/index.ts +41 -0
- package/src/main.ts +15 -0
- package/src/styles/imageSlider.css +34 -0
- package/tsconfig.json +12 -0
- 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;
|