1
|
|
2
|
import L, { LatLngTuple, PointExpression } from 'leaflet'
|
3
|
import { CatalogItemDto, ExternalCatalogItemDto, PathDto } from '../../swagger/data-contracts'
|
4
|
import generateUuid from '../../utils/id/uuidGenerator'
|
5
|
|
6
|
// Type of map point for conditional rendering
|
7
|
export enum MapPointType {
|
8
|
ProcessedText, // From processed text
|
9
|
LocalCatalog, // Fetched from local catalog
|
10
|
ExternalCatalog, // Fetched from external catalog
|
11
|
FileImport, // From GeoJSON file
|
12
|
FromCoordinates, // From coordinates
|
13
|
}
|
14
|
|
15
|
|
16
|
// Represents a point on the map - wrapper for CatalogItemDto to make it easier to work with
|
17
|
export interface MapPoint {
|
18
|
id: string // unique id to identify the point on the map
|
19
|
reactId: string // unique id to identify item in React - e.g. rendering in a list
|
20
|
idx: number // index in the path
|
21
|
variantIdx: number, // index of the variant
|
22
|
variants?: CatalogItemDto[], // variants of the point
|
23
|
firstVariantCatalogItemId?: string // this id is only used for catalog items with multiple variants
|
24
|
addToPath: boolean // whether to add the point to the path
|
25
|
catalogItem: CatalogItemDto, // reference to CatalogItemDto
|
26
|
type: MapPointType // Type of the map point
|
27
|
hidden?: boolean // if true the point will not be displayed on the map
|
28
|
externalSource?: string // if the point is from external source, this is the source name
|
29
|
}
|
30
|
|
31
|
export interface ExternalMapPoint extends MapPoint {
|
32
|
catalogItem: ExternalCatalogItemDto
|
33
|
}
|
34
|
|
35
|
export type Path = MapPoint[]
|
36
|
|
37
|
export interface ExternalPath {
|
38
|
idx: number,
|
39
|
color: string,
|
40
|
path: Path
|
41
|
visible: boolean
|
42
|
filename: string
|
43
|
}
|
44
|
|
45
|
export const getExternalPathColor = (idx: number) => {
|
46
|
return `hsl(${idx * 360 / 10}, 100%, 50%)`
|
47
|
}
|
48
|
|
49
|
/**
|
50
|
* Returns true whether the map point is displayable - i.e. can be shown on the map
|
51
|
* @param mapPoint
|
52
|
* @returns true if the map point is displayable
|
53
|
*/
|
54
|
export const isMapPointDisplayable = (mapPoint: MapPoint): boolean =>
|
55
|
!!mapPoint.catalogItem.latitude && !!mapPoint.catalogItem.longitude && !mapPoint.hidden
|
56
|
|
57
|
/**
|
58
|
* Based on its type - either imported from local catalog, remote catalogs etc. each type has its own color to differentiate them
|
59
|
* @param item item to get color for
|
60
|
* @returns CSS color string
|
61
|
*/
|
62
|
export const getMapPointSemanticColor = (item: MapPoint) => {
|
63
|
switch (item.type) {
|
64
|
case MapPointType.LocalCatalog:
|
65
|
return 'inherit'
|
66
|
case MapPointType.FromCoordinates:
|
67
|
return '#21972D'
|
68
|
case MapPointType.ExternalCatalog:
|
69
|
return '#A72020'
|
70
|
case MapPointType.FileImport:
|
71
|
return '#967520'
|
72
|
}
|
73
|
}
|
74
|
|
75
|
/**
|
76
|
* Creates SVG icon for map marker with specific color
|
77
|
*/
|
78
|
const createMapMarkerSvg = (color: string) => {
|
79
|
return `data:image/svg+xml;utf8, ${encodeURIComponent(`
|
80
|
<svg width="16px" height="16px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" version="1.1" fill="blue"
|
81
|
stroke="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1">
|
82
|
<path
|
83
|
fill="${color}"
|
84
|
d="m13.25 7c0 3.75-5.25 7.25-5.25 7.25s-5.25-3.5-5.25-7.25c0-2.89949 2.35051-5.25 5.25-5.25 2.8995 0 5.25 2.35051 5.25 5.25z" />
|
85
|
<circle cx="8" cy="7" r="1.55" fill="white" />
|
86
|
</svg> `)}`
|
87
|
}
|
88
|
|
89
|
const mapMarkerSvgs = {
|
90
|
[MapPointType.ProcessedText]: createMapMarkerSvg('#285CAB'),
|
91
|
[MapPointType.LocalCatalog]: createMapMarkerSvg('#00B0FF'),
|
92
|
[MapPointType.ExternalCatalog]: createMapMarkerSvg('#A72020'),
|
93
|
[MapPointType.FileImport]: createMapMarkerSvg('#967520'),
|
94
|
[MapPointType.FromCoordinates]: createMapMarkerSvg('#21972D'),
|
95
|
}
|
96
|
|
97
|
const iconAnchor = [22, 22] as PointExpression
|
98
|
const iconSize = [35, 35] as PointExpression
|
99
|
|
100
|
const mapMarkers: any = {
|
101
|
[MapPointType.ProcessedText]: L.icon({
|
102
|
iconAnchor, iconSize,
|
103
|
iconUrl: mapMarkerSvgs[MapPointType.ProcessedText],
|
104
|
}),
|
105
|
[MapPointType.LocalCatalog]: L.icon({
|
106
|
iconAnchor, iconSize,
|
107
|
iconUrl: mapMarkerSvgs[MapPointType.LocalCatalog],
|
108
|
}),
|
109
|
[MapPointType.ExternalCatalog]: L.icon({
|
110
|
iconAnchor, iconSize,
|
111
|
iconUrl: mapMarkerSvgs[MapPointType.ExternalCatalog],
|
112
|
}),
|
113
|
|
114
|
[MapPointType.FileImport]: L.icon({
|
115
|
iconAnchor, iconSize,
|
116
|
iconUrl: mapMarkerSvgs[MapPointType.FileImport],
|
117
|
}),
|
118
|
[MapPointType.FromCoordinates]: L.icon({
|
119
|
iconAnchor, iconSize,
|
120
|
iconUrl: mapMarkerSvgs[MapPointType.FromCoordinates],
|
121
|
}),
|
122
|
}
|
123
|
|
124
|
export const getMapPointIcon = (item: MapPoint): L.Icon => mapMarkers[item.type]
|
125
|
export const getCustomMapPointIcon = (color: string) => {
|
126
|
if (mapMarkers.hasOwnProperty(color)) {
|
127
|
return mapMarkers[color] as L.Icon
|
128
|
}
|
129
|
|
130
|
const svg = createMapMarkerSvg(color)
|
131
|
mapMarkers[color] = L.icon({
|
132
|
iconAnchor, iconSize,
|
133
|
iconUrl: svg,
|
134
|
})
|
135
|
|
136
|
return mapMarkers[color] as L.Icon
|
137
|
}
|
138
|
|
139
|
export const calculateMapCenter = (path: Path): LatLngTuple | undefined => {
|
140
|
const displayableItems = path.filter((item) => isMapPointDisplayable(item))
|
141
|
if (displayableItems.length === 0) {
|
142
|
return undefined
|
143
|
}
|
144
|
|
145
|
return [
|
146
|
displayableItems
|
147
|
.map((item) => item.catalogItem.latitude ?? 0)
|
148
|
.reduce((a, b) => a + b, 0) / displayableItems.length,
|
149
|
displayableItems
|
150
|
.map((item) => item.catalogItem.longitude ?? 0)
|
151
|
.reduce((a, b) => a + b, 0) / displayableItems.length,
|
152
|
]
|
153
|
}
|
154
|
|
155
|
export const setMapPointIds = (pathVariant: Path): Path => pathVariant.map(mapPoint => {
|
156
|
// Get identifier - either catalog item identifier is used or generate a new UUID if the point does not have uuid
|
157
|
// - e.g. is not from the catalog
|
158
|
return { ...mapPoint, id: mapPoint.catalogItem.id ? mapPoint.catalogItem.id : generateUuid() }
|
159
|
})
|
160
|
|
161
|
/**
|
162
|
* Maps external catalog item to map point object
|
163
|
* @param externalCatalogItem external catalog item to map to map point
|
164
|
* @returns
|
165
|
*/
|
166
|
export const mapExternalCatalogItemToMapPoint = (externalCatalogItem: ExternalCatalogItemDto) => {
|
167
|
const coordinatesNotNull = externalCatalogItem.latitude !== null && externalCatalogItem.longitude !== null
|
168
|
return ({
|
169
|
id: externalCatalogItem.id,
|
170
|
reactId: generateUuid(),
|
171
|
idx: -1,
|
172
|
addToPath: coordinatesNotNull,
|
173
|
catalogItem: externalCatalogItem,
|
174
|
type: MapPointType.ExternalCatalog,
|
175
|
hidden: !coordinatesNotNull,
|
176
|
externalSource: externalCatalogItem.externalSource,
|
177
|
} as MapPoint)
|
178
|
}
|
179
|
|
180
|
/**
|
181
|
* Maps local catalog item to map point
|
182
|
* @param catalogItem catalog item to map to map point
|
183
|
* @returns
|
184
|
*/
|
185
|
export const mapLocalCatalogItemToMapPoint = (catalogItem: CatalogItemDto): ExternalMapPoint => {
|
186
|
const coordinatesNotNull = catalogItem.latitude !== null && catalogItem.longitude !== null
|
187
|
return ({
|
188
|
id: catalogItem.id as string,
|
189
|
reactId: generateUuid(),
|
190
|
idx: -1,
|
191
|
addToPath: coordinatesNotNull,
|
192
|
catalogItem,
|
193
|
type: MapPointType.LocalCatalog,
|
194
|
hidden: !coordinatesNotNull,
|
195
|
variantIdx: 0,
|
196
|
variants: undefined
|
197
|
})
|
198
|
}
|
199
|
|
200
|
/**
|
201
|
* Builds path from pathDto
|
202
|
* @param pathDto
|
203
|
* @returns
|
204
|
*/
|
205
|
export const buildPath = (pathDto: PathDto): Path => {
|
206
|
// Path dto contains an array of all variants of the catalog item in given path
|
207
|
// By default we use the first variant of the catalog item, map the catalog item to MapPoint and add all the variants
|
208
|
// to the map point + set the index to 0
|
209
|
const path: Path = []
|
210
|
pathDto.foundCatalogItems?.forEach(catalogItemVariants => {
|
211
|
if (catalogItemVariants.length === 0) {
|
212
|
return // This should never happen but should backend fail we will ignore empty arrays
|
213
|
}
|
214
|
|
215
|
const catalogItem = catalogItemVariants[0]
|
216
|
const coordinatesNotNull = catalogItem.latitude !== null && catalogItem.longitude !== null // backend sends null for unknown coords
|
217
|
path.push({
|
218
|
id: catalogItem.id, // id should be the same as catalog item's id so that we do not render the same point multiple times
|
219
|
reactId: generateUuid(), // react id should always be unique so that we can move the point in the list
|
220
|
idx: path.length, // index in the result array
|
221
|
variantIdx: 0, // we always pick the first variant
|
222
|
addToPath: coordinatesNotNull,
|
223
|
catalogItem,
|
224
|
type: MapPointType.ProcessedText,
|
225
|
hidden: !coordinatesNotNull,
|
226
|
variants: catalogItemVariants.length === 1 ? undefined : catalogItemVariants,
|
227
|
firstVariantCatalogItemId: catalogItem.id,
|
228
|
} as MapPoint)
|
229
|
})
|
230
|
|
231
|
return path
|
232
|
}
|
233
|
|
234
|
/**
|
235
|
* Updates map point if it has catalog item set or is in its variants
|
236
|
*/
|
237
|
export const updateMapPointIfCatalogItemPresent = (mapPoint: MapPoint, catalogItem: CatalogItemDto): MapPoint => {
|
238
|
if (mapPoint.id !== catalogItem.id) {
|
239
|
// If the id does not match catalog item id we need to check if the catalog item is in the variants
|
240
|
if (!mapPoint.variants) {
|
241
|
return mapPoint // if there are no variants we do not need to update the map point
|
242
|
}
|
243
|
|
244
|
const variantIdx = mapPoint.variants.findIndex(variant => variant.id === catalogItem.id)
|
245
|
if (variantIdx === -1) {
|
246
|
return mapPoint // if the catalog item is not in the variants we do not need to update the map point
|
247
|
}
|
248
|
|
249
|
// If the catalog item is in the variants we need to update the map point
|
250
|
return {
|
251
|
...mapPoint,
|
252
|
variants: mapPoint.variants.map((variant, idx) => idx === variantIdx ? catalogItem : variant),
|
253
|
}
|
254
|
}
|
255
|
|
256
|
// Otherwise we have found map point that has same id as our catalog item
|
257
|
// which means it will always have it in its variants if there are some
|
258
|
const coordinatesNotNull = catalogItem.latitude !== null && catalogItem.longitude !== null
|
259
|
if (!mapPoint.variants) {
|
260
|
// If there are no variants simply update the map point
|
261
|
return {
|
262
|
...mapPoint,
|
263
|
catalogItem,
|
264
|
addToPath: mapPoint.addToPath && coordinatesNotNull,
|
265
|
hidden: mapPoint.hidden && !coordinatesNotNull,
|
266
|
}
|
267
|
}
|
268
|
|
269
|
// If there are variants we need to update the map point in the variants as well
|
270
|
const variantIdx = mapPoint.variants.findIndex(variant => variant.id === catalogItem.id)
|
271
|
return {
|
272
|
...mapPoint,
|
273
|
catalogItem,
|
274
|
variants: mapPoint.variants.map((variant, idx) => idx === variantIdx ? catalogItem : variant),
|
275
|
addToPath: mapPoint.addToPath && coordinatesNotNull,
|
276
|
hidden: mapPoint.hidden && !coordinatesNotNull,
|
277
|
}
|
278
|
}
|