@lblod/ember-rdfa-editor-lblod-plugins 32.3.0 → 32.4.0
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/CHANGELOG.md +17 -0
- package/addon/components/document-validation-plugin/card.gts +137 -0
- package/addon/components/location-plugin/insert-location-placeholder.gts +76 -0
- package/addon/components/location-plugin/insert.gts +2 -1
- package/addon/components/location-plugin/nodeview.gts +1 -1
- package/addon/components/roadsign-regulation-plugin/expanded-measure.gts +5 -1
- package/addon/components/roadsign-regulation-plugin/roadsigns-modal.gts +1 -1
- package/addon/plugins/document-validation-plugin/index.ts +196 -0
- package/addon/plugins/location-plugin/_private/node-contents/address.ts +252 -0
- package/addon/plugins/location-plugin/_private/node-contents/area.ts +64 -0
- package/addon/plugins/location-plugin/_private/node-contents/index.ts +46 -0
- package/addon/plugins/location-plugin/_private/node-contents/place.ts +64 -0
- package/addon/plugins/location-plugin/_private/node-contents/point.ts +89 -0
- package/addon/plugins/location-plugin/_private/utils/address-helpers.ts +407 -0
- package/addon/plugins/location-plugin/_private/utils/geo-helpers.ts +210 -0
- package/addon/plugins/location-plugin/_private/utils/node-utils.ts +82 -0
- package/addon/plugins/location-plugin/node-contents/address.ts +2 -252
- package/addon/plugins/location-plugin/node-contents/area.ts +2 -64
- package/addon/plugins/location-plugin/node-contents/index.ts +2 -46
- package/addon/plugins/location-plugin/node-contents/place.ts +2 -64
- package/addon/plugins/location-plugin/node-contents/point.ts +2 -89
- package/addon/plugins/location-plugin/node.ts +48 -16
- package/addon/plugins/location-plugin/utils/address-helpers.ts +2 -407
- package/addon/plugins/location-plugin/utils/geo-helpers.ts +2 -210
- package/addon/plugins/location-plugin/utils/node-utils.ts +10 -61
- package/addon/plugins/roadsign-regulation-plugin/queries/mobility-measure-concept.ts +3 -1
- package/addon/utils/remove-quotes.ts +7 -0
- package/app/components/document-validation-plugin/card.js +1 -0
- package/app/components/location-plugin/insert-location-placeholder.js +1 -0
- package/app/styles/document-validation.scss +26 -0
- package/declarations/addon/components/document-validation-plugin/card.d.ts +22 -0
- package/declarations/addon/components/location-plugin/insert-location-placeholder.d.ts +20 -0
- package/declarations/addon/plugins/document-validation-plugin/index.d.ts +28 -0
- package/declarations/addon/plugins/location-plugin/_private/node-contents/address.d.ts +11 -0
- package/declarations/addon/plugins/location-plugin/_private/node-contents/area.d.ts +9 -0
- package/declarations/addon/plugins/location-plugin/_private/node-contents/index.d.ts +32 -0
- package/declarations/addon/plugins/location-plugin/_private/node-contents/place.d.ts +9 -0
- package/declarations/addon/plugins/location-plugin/_private/node-contents/point.d.ts +6 -0
- package/declarations/addon/plugins/location-plugin/_private/utils/address-helpers.d.ts +62 -0
- package/declarations/addon/plugins/location-plugin/_private/utils/geo-helpers.d.ts +82 -0
- package/declarations/addon/plugins/location-plugin/_private/utils/node-utils.d.ts +17 -0
- package/declarations/addon/plugins/location-plugin/node-contents/address.d.ts +2 -11
- package/declarations/addon/plugins/location-plugin/node-contents/area.d.ts +2 -9
- package/declarations/addon/plugins/location-plugin/node-contents/index.d.ts +2 -32
- package/declarations/addon/plugins/location-plugin/node-contents/place.d.ts +2 -9
- package/declarations/addon/plugins/location-plugin/node-contents/point.d.ts +2 -6
- package/declarations/addon/plugins/location-plugin/utils/address-helpers.d.ts +2 -62
- package/declarations/addon/plugins/location-plugin/utils/geo-helpers.d.ts +2 -82
- package/declarations/addon/plugins/location-plugin/utils/node-utils.d.ts +1 -0
- package/declarations/addon/utils/remove-quotes.d.ts +1 -0
- package/package.json +7 -3
- package/pnpm-lock.yaml +1527 -1163
- package/translations/en-US.yaml +9 -0
- package/translations/nl-BE.yaml +9 -0
- package/types/global.d.ts +4 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { unwrap } from '@lblod/ember-rdfa-editor-lblod-plugins/utils/option';
|
|
2
|
+
import { NodeContentsUtils } from '../../node-contents';
|
|
3
|
+
import {
|
|
4
|
+
convertLambertCoordsToWGS84,
|
|
5
|
+
parseLambert72GMLString,
|
|
6
|
+
Point,
|
|
7
|
+
} from './geo-helpers';
|
|
8
|
+
|
|
9
|
+
type Identificator = {
|
|
10
|
+
id: string;
|
|
11
|
+
naamruimte: string;
|
|
12
|
+
objectId: string;
|
|
13
|
+
versieId: string;
|
|
14
|
+
};
|
|
15
|
+
type GeographicalName = {
|
|
16
|
+
spelling: string;
|
|
17
|
+
taal: string;
|
|
18
|
+
};
|
|
19
|
+
type DetailLink = {
|
|
20
|
+
objectId: string;
|
|
21
|
+
detail: string;
|
|
22
|
+
};
|
|
23
|
+
type GeoCoordinate = {
|
|
24
|
+
Lat_WGS84: number;
|
|
25
|
+
Lon_WGS84: number;
|
|
26
|
+
X_Lambert72: number;
|
|
27
|
+
Y_Lambert72: number;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Specified [in the API docs]{@link https://geo.api.vlaanderen.be/geolocation/}.
|
|
31
|
+
* It is not specified which fields can be null, so these might be incorrect.
|
|
32
|
+
*/
|
|
33
|
+
type LocationRegisterSearchResult = {
|
|
34
|
+
LocationResult: [
|
|
35
|
+
{
|
|
36
|
+
ID: number;
|
|
37
|
+
Municipality: string;
|
|
38
|
+
Zipcode: string | null;
|
|
39
|
+
Thoroughfarename: string | null;
|
|
40
|
+
Housenumber: string | null;
|
|
41
|
+
FormattedAddress: string | null;
|
|
42
|
+
Location: GeoCoordinate;
|
|
43
|
+
LocationType: string | null;
|
|
44
|
+
BoundingBox: {
|
|
45
|
+
LowerLeft: GeoCoordinate;
|
|
46
|
+
UpperRight: GeoCoordinate;
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type StreetSearchResult = {
|
|
53
|
+
adresMatches: [
|
|
54
|
+
{
|
|
55
|
+
gemeente: {
|
|
56
|
+
gemeentenaam: { geografischeNaam: GeographicalName };
|
|
57
|
+
};
|
|
58
|
+
straatnaam?: {
|
|
59
|
+
straatnaam: {
|
|
60
|
+
geografischeNaam: GeographicalName;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type AddressSearchResult = {
|
|
68
|
+
adressen: {
|
|
69
|
+
identificator: Identificator;
|
|
70
|
+
detail: string;
|
|
71
|
+
huisnummer?: string;
|
|
72
|
+
volledigAdres: GeographicalName;
|
|
73
|
+
adresStatus: string;
|
|
74
|
+
}[];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* {@link https://docs.basisregisters.vlaanderen.be/docs/api-documentation.html#operation/GetAddressV2}
|
|
79
|
+
*/
|
|
80
|
+
type AddressDetailResult = {
|
|
81
|
+
identificator: Identificator;
|
|
82
|
+
gemeente: DetailLink & {
|
|
83
|
+
gemeentenaam: {
|
|
84
|
+
geografischeNaam: GeographicalName;
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
postinfo: DetailLink;
|
|
88
|
+
straatnaam: DetailLink & {
|
|
89
|
+
straatnaam: {
|
|
90
|
+
geografischeNaam: GeographicalName;
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
huisnummer: string;
|
|
94
|
+
busnummer?: string;
|
|
95
|
+
volledigAdres: GeographicalName;
|
|
96
|
+
officieelToegekend: boolean;
|
|
97
|
+
adresPositie: {
|
|
98
|
+
geometrie: {
|
|
99
|
+
type: 'Point' | string;
|
|
100
|
+
/** GML encoded coordinates using Lambert72 CRS */
|
|
101
|
+
gml: string;
|
|
102
|
+
};
|
|
103
|
+
positieGeometrieMethode: string;
|
|
104
|
+
positieSpecificatie: string;
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export class Address {
|
|
109
|
+
declare uri: string;
|
|
110
|
+
declare belgianAddressUri?: string;
|
|
111
|
+
declare street: string;
|
|
112
|
+
declare zipcode: string;
|
|
113
|
+
declare municipality: string;
|
|
114
|
+
declare housenumber?: string;
|
|
115
|
+
declare busnumber?: string;
|
|
116
|
+
declare location: Point;
|
|
117
|
+
declare truncated: boolean;
|
|
118
|
+
|
|
119
|
+
constructor(
|
|
120
|
+
args: Pick<
|
|
121
|
+
Address,
|
|
122
|
+
| 'street'
|
|
123
|
+
| 'housenumber'
|
|
124
|
+
| 'zipcode'
|
|
125
|
+
| 'municipality'
|
|
126
|
+
| 'uri'
|
|
127
|
+
| 'busnumber'
|
|
128
|
+
| 'location'
|
|
129
|
+
| 'belgianAddressUri'
|
|
130
|
+
| 'truncated'
|
|
131
|
+
>,
|
|
132
|
+
) {
|
|
133
|
+
Object.assign(this, args);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get formatted() {
|
|
137
|
+
if (this.truncated) {
|
|
138
|
+
if (this.housenumber && this.busnumber) {
|
|
139
|
+
return `${this.street} ${this.housenumber} bus ${this.busnumber}`;
|
|
140
|
+
} else if (this.housenumber) {
|
|
141
|
+
return `${this.street} ${this.housenumber}`;
|
|
142
|
+
} else {
|
|
143
|
+
return `${this.street}`;
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
if (this.housenumber && this.busnumber) {
|
|
147
|
+
return `${this.street} ${this.housenumber} bus ${this.busnumber}, ${this.zipcode} ${this.municipality}`;
|
|
148
|
+
} else if (this.housenumber) {
|
|
149
|
+
return `${this.street} ${this.housenumber}, ${this.zipcode} ${this.municipality}`;
|
|
150
|
+
} else {
|
|
151
|
+
return `${this.street}, ${this.zipcode} ${this.municipality}`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
sameAs(
|
|
157
|
+
other?: Pick<
|
|
158
|
+
Address,
|
|
159
|
+
'street' | 'housenumber' | 'busnumber' | 'municipality' | 'truncated'
|
|
160
|
+
> | null,
|
|
161
|
+
) {
|
|
162
|
+
return (
|
|
163
|
+
this.street === other?.street &&
|
|
164
|
+
this.housenumber === other?.housenumber &&
|
|
165
|
+
this.busnumber === other?.busnumber &&
|
|
166
|
+
this.municipality === other?.municipality &&
|
|
167
|
+
this.truncated === other?.truncated
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
get hasHouseNumber() {
|
|
172
|
+
return !!this.housenumber;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export class AddressError extends Error {
|
|
177
|
+
translation: string;
|
|
178
|
+
status?: number;
|
|
179
|
+
coords?: string;
|
|
180
|
+
alternativeAddress?: Address;
|
|
181
|
+
constructor({
|
|
182
|
+
message,
|
|
183
|
+
translation,
|
|
184
|
+
status,
|
|
185
|
+
coords,
|
|
186
|
+
alternativeAddress,
|
|
187
|
+
}: Pick<
|
|
188
|
+
AddressError,
|
|
189
|
+
'message' | 'translation' | 'status' | 'alternativeAddress' | 'coords'
|
|
190
|
+
>) {
|
|
191
|
+
super(message);
|
|
192
|
+
this.translation = translation;
|
|
193
|
+
this.status = status;
|
|
194
|
+
this.coords = coords;
|
|
195
|
+
this.alternativeAddress = alternativeAddress;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const LOC_GEOPUNT_ENDPOINT = `https://geo.api.vlaanderen.be/geolocation/v4/Location`;
|
|
200
|
+
const BASISREGISTER = 'https://api.basisregisters.vlaanderen.be/v2';
|
|
201
|
+
const BASISREGISTER_ADRESMATCH = `${BASISREGISTER}/adresmatch`;
|
|
202
|
+
const BASISREGISTER_ADRES = `${BASISREGISTER}/adressen`;
|
|
203
|
+
|
|
204
|
+
export const replaceAccents = (string: string) =>
|
|
205
|
+
string.normalize('NFD').replace(/([\u0300-\u036f])/g, '');
|
|
206
|
+
|
|
207
|
+
export async function fetchMunicipalities(term: string): Promise<string[]> {
|
|
208
|
+
const url = new URL(LOC_GEOPUNT_ENDPOINT);
|
|
209
|
+
url.searchParams.append('q', replaceAccents(term.replace(/^"(.*)"$/, '$1')));
|
|
210
|
+
url.searchParams.append('c', '10');
|
|
211
|
+
url.searchParams.append('type', 'Municipality');
|
|
212
|
+
const result = await fetch(url, {
|
|
213
|
+
method: 'GET',
|
|
214
|
+
});
|
|
215
|
+
if (result.ok) {
|
|
216
|
+
const jsonResult = (await result.json()) as LocationRegisterSearchResult;
|
|
217
|
+
const municipalities = jsonResult.LocationResult.map(
|
|
218
|
+
(entry) => entry.Municipality,
|
|
219
|
+
);
|
|
220
|
+
return municipalities;
|
|
221
|
+
} else {
|
|
222
|
+
throw new AddressError({
|
|
223
|
+
translation: 'editor-plugins.address.edit.errors.http-error',
|
|
224
|
+
message: `An error occured when querying the location register, status code: ${result.status}`,
|
|
225
|
+
status: result.status,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function fetchStreets(term: string, municipality: string) {
|
|
231
|
+
const url = new URL(BASISREGISTER_ADRESMATCH);
|
|
232
|
+
url.searchParams.append(
|
|
233
|
+
'straatnaam',
|
|
234
|
+
replaceAccents(term.replace(/^"(.*)"$/, '$1')),
|
|
235
|
+
);
|
|
236
|
+
url.searchParams.append('gemeentenaam', municipality);
|
|
237
|
+
const result = await fetch(url, {
|
|
238
|
+
method: 'GET',
|
|
239
|
+
});
|
|
240
|
+
if (result.ok) {
|
|
241
|
+
const jsonResult = (await result.json()) as StreetSearchResult;
|
|
242
|
+
|
|
243
|
+
const streetnames = jsonResult.adresMatches
|
|
244
|
+
.map((entry) => entry.straatnaam?.straatnaam.geografischeNaam.spelling)
|
|
245
|
+
.filter(Boolean);
|
|
246
|
+
|
|
247
|
+
return streetnames;
|
|
248
|
+
} else {
|
|
249
|
+
throw new AddressError({
|
|
250
|
+
translation: 'editor-plugins.address.edit.errors.http-error',
|
|
251
|
+
message: `An error occured when querying the address register, status code: ${result.status}`,
|
|
252
|
+
status: result.status,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
type StreetInfo = {
|
|
258
|
+
municipality: string;
|
|
259
|
+
street: string;
|
|
260
|
+
truncated: boolean;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
export async function resolveStreet(
|
|
264
|
+
info: StreetInfo,
|
|
265
|
+
nodeContentsUtils: NodeContentsUtils,
|
|
266
|
+
) {
|
|
267
|
+
const searchTerm = `${info.street}, ${info.municipality}`;
|
|
268
|
+
const url = new URL(LOC_GEOPUNT_ENDPOINT);
|
|
269
|
+
url.searchParams.append(
|
|
270
|
+
'q',
|
|
271
|
+
replaceAccents(searchTerm.replace(/^"(.*)"$/, '$1')),
|
|
272
|
+
);
|
|
273
|
+
url.searchParams.append('c', '1');
|
|
274
|
+
url.searchParams.append('type', 'ThoroughFarename');
|
|
275
|
+
const result = await fetch(url, {
|
|
276
|
+
method: 'GET',
|
|
277
|
+
});
|
|
278
|
+
if (result.ok) {
|
|
279
|
+
const jsonResult = (await result.json()) as LocationRegisterSearchResult;
|
|
280
|
+
const streetinfo = jsonResult.LocationResult[0];
|
|
281
|
+
if (streetinfo) {
|
|
282
|
+
return new Address({
|
|
283
|
+
uri: nodeContentsUtils.fallbackAddressUri(),
|
|
284
|
+
belgianAddressUri: `https://data.vlaanderen.be/id/straatnaam/${streetinfo.ID}`,
|
|
285
|
+
street: unwrap(streetinfo.Thoroughfarename),
|
|
286
|
+
municipality: streetinfo.Municipality,
|
|
287
|
+
zipcode: unwrap(streetinfo.Zipcode),
|
|
288
|
+
truncated: info.truncated,
|
|
289
|
+
location: new Point({
|
|
290
|
+
uri: nodeContentsUtils.fallbackGeometryUri(),
|
|
291
|
+
location: {
|
|
292
|
+
global: {
|
|
293
|
+
lng: streetinfo.Location.Lon_WGS84,
|
|
294
|
+
lat: streetinfo.Location.Lat_WGS84,
|
|
295
|
+
},
|
|
296
|
+
lambert: {
|
|
297
|
+
x: streetinfo.Location.X_Lambert72,
|
|
298
|
+
y: streetinfo.Location.Y_Lambert72,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
}),
|
|
302
|
+
});
|
|
303
|
+
} else {
|
|
304
|
+
throw new AddressError({
|
|
305
|
+
translation: 'editor-plugins.address.edit.errors.address-not-found',
|
|
306
|
+
message: `Could not find address in address register`,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
throw new AddressError({
|
|
311
|
+
translation: 'editor-plugins.address.edit.errors.http-error',
|
|
312
|
+
message: `An error occured when querying the location register, status code: ${result.status}`,
|
|
313
|
+
status: result.status,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
type AddressInfo = {
|
|
318
|
+
municipality: string;
|
|
319
|
+
street: string;
|
|
320
|
+
housenumber: string;
|
|
321
|
+
busnumber?: string;
|
|
322
|
+
truncated: boolean;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
export async function resolveAddress(
|
|
326
|
+
info: AddressInfo,
|
|
327
|
+
nodeContentsUtils: NodeContentsUtils,
|
|
328
|
+
) {
|
|
329
|
+
const addressSearchResult = await searchAddress(info, 1);
|
|
330
|
+
if (addressSearchResult.adressen.length) {
|
|
331
|
+
const addressDetailURL = addressSearchResult.adressen[0].detail;
|
|
332
|
+
const response = await fetch(addressDetailURL);
|
|
333
|
+
if (response.ok) {
|
|
334
|
+
const result = (await response.json()) as AddressDetailResult;
|
|
335
|
+
const { lambert } = parseLambert72GMLString(
|
|
336
|
+
result.adresPositie.geometrie.gml,
|
|
337
|
+
);
|
|
338
|
+
return new Address({
|
|
339
|
+
street: result.straatnaam.straatnaam.geografischeNaam.spelling,
|
|
340
|
+
housenumber: result.huisnummer,
|
|
341
|
+
busnumber: result.busnummer,
|
|
342
|
+
zipcode: result.postinfo.objectId,
|
|
343
|
+
municipality: result.gemeente.gemeentenaam.geografischeNaam.spelling,
|
|
344
|
+
uri: nodeContentsUtils.fallbackAddressUri(),
|
|
345
|
+
belgianAddressUri: result.identificator.id,
|
|
346
|
+
truncated: info.truncated,
|
|
347
|
+
location: new Point({
|
|
348
|
+
uri: `${result.identificator.id}/1`,
|
|
349
|
+
location: {
|
|
350
|
+
lambert,
|
|
351
|
+
global: convertLambertCoordsToWGS84(lambert),
|
|
352
|
+
},
|
|
353
|
+
}),
|
|
354
|
+
});
|
|
355
|
+
} else {
|
|
356
|
+
throw new AddressError({
|
|
357
|
+
translation: 'editor-plugins.address.edit.errors.http-error',
|
|
358
|
+
message: `An error occured when querying the address register, status code: ${response.status}`,
|
|
359
|
+
status: response.status,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
const alternativeAddress = await resolveStreet(
|
|
364
|
+
{
|
|
365
|
+
street: info.street,
|
|
366
|
+
municipality: info.municipality,
|
|
367
|
+
truncated: info.truncated,
|
|
368
|
+
},
|
|
369
|
+
nodeContentsUtils,
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
throw new AddressError({
|
|
373
|
+
translation: 'editor-plugins.address.edit.errors.address-not-found',
|
|
374
|
+
message: `Could not find address in address register`,
|
|
375
|
+
alternativeAddress,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export async function searchAddress(
|
|
381
|
+
{ municipality, street, housenumber, busnumber }: AddressInfo,
|
|
382
|
+
limit = 10,
|
|
383
|
+
) {
|
|
384
|
+
const url = new URL(BASISREGISTER_ADRES);
|
|
385
|
+
|
|
386
|
+
url.searchParams.append('GemeenteNaam', replaceAccents(municipality));
|
|
387
|
+
url.searchParams.append('Straatnaam', replaceAccents(street));
|
|
388
|
+
url.searchParams.append('limit', limit.toString());
|
|
389
|
+
url.searchParams.append('Huisnummer', replaceAccents(housenumber));
|
|
390
|
+
if (busnumber) {
|
|
391
|
+
url.searchParams.append('Busnummer', replaceAccents(busnumber));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const response = await fetch(url.toString(), {
|
|
395
|
+
method: 'GET',
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (response.ok) {
|
|
399
|
+
return (await response.json()) as AddressSearchResult;
|
|
400
|
+
} else {
|
|
401
|
+
throw new AddressError({
|
|
402
|
+
translation: 'editor-plugins.address.edit.errors.http-error',
|
|
403
|
+
message: `An error occured when querying the address register, status code: ${response.status}`,
|
|
404
|
+
status: response.status,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import proj from 'proj4';
|
|
2
|
+
import { AddressError } from './address-helpers';
|
|
3
|
+
|
|
4
|
+
/** A point represents a location as a geometry in the OSLO model. */
|
|
5
|
+
export class Point {
|
|
6
|
+
declare uri: string;
|
|
7
|
+
declare location: GeoPos;
|
|
8
|
+
constructor(args: Omit<Point, 'constructor' | 'formatted'>) {
|
|
9
|
+
Object.assign(this, args);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get formatted() {
|
|
13
|
+
return `[${this.location.lambert.x}, ${this.location.lambert.y}]`;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** A polygon represents a bounded area, a kind of geometry in the OSLO model. */
|
|
17
|
+
export class Polygon {
|
|
18
|
+
declare uri: string;
|
|
19
|
+
declare locations: GeoPos[];
|
|
20
|
+
constructor(args: Omit<Polygon, 'constructor' | 'formatted'>) {
|
|
21
|
+
this.uri = args.uri;
|
|
22
|
+
// The start and end points of a polygon should match
|
|
23
|
+
if (isSamePos(args.locations[0], args.locations.at(-1))) {
|
|
24
|
+
this.locations = args.locations;
|
|
25
|
+
} else {
|
|
26
|
+
this.locations = [...args.locations, args.locations[0]];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get formatted() {
|
|
31
|
+
return `[${this.locations
|
|
32
|
+
.map(({ lambert }) => `[${lambert.x}, ${lambert.y}]`)
|
|
33
|
+
.join(', ')}]`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A place represents the combination of a location and a name in the OSLO model. Only the name is
|
|
38
|
+
* displayed in the human readable text.
|
|
39
|
+
*/
|
|
40
|
+
export class Place {
|
|
41
|
+
declare uri: string;
|
|
42
|
+
declare name: string;
|
|
43
|
+
declare location: Point;
|
|
44
|
+
constructor(args: Omit<Place, 'constructor' | 'formatted'>) {
|
|
45
|
+
Object.assign(this, args);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get formatted() {
|
|
49
|
+
return this.name;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* An area represents multiple points which bound a part of the map, along with the human readable
|
|
54
|
+
* name of that it represents. This is used as a place for the oslo model.
|
|
55
|
+
*/
|
|
56
|
+
export class Area {
|
|
57
|
+
declare uri: string;
|
|
58
|
+
declare name: string;
|
|
59
|
+
declare shape: Polygon;
|
|
60
|
+
constructor(args: Omit<Area, 'constructor' | 'formatted'>) {
|
|
61
|
+
Object.assign(this, args);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get formatted() {
|
|
65
|
+
return this.name;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* This CRS definition is corrected from that on epsg.io/31370, as there is a known bug that their
|
|
71
|
+
* definitions invert some of the TOWGS84 parameters.
|
|
72
|
+
* See [this issue]{@link https://github.com/OSGeo/PROJ/issues/4170} for more details.
|
|
73
|
+
*/
|
|
74
|
+
const LAMBERT_CRS =
|
|
75
|
+
'PROJCS["BD72 / Belgian Lambert 72",GEOGCS["BD72",DATUM["Reseau_National_Belge_1972",SPHEROID["International 1924",6378388,297],TOWGS84[-106.8686,52.2978,-103.7239,0.3366,-0.457,1.8422,-1.2747]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4313"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["latitude_of_origin",90],PARAMETER["central_meridian",4.36748666666667],PARAMETER["standard_parallel_1",51.1666672333333],PARAMETER["standard_parallel_2",49.8333339],PARAMETER["false_easting",150000.013],PARAMETER["false_northing",5400088.438],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","31370"]]';
|
|
76
|
+
const lambertToWgs84 = proj(LAMBERT_CRS, 'EPSG:4326');
|
|
77
|
+
/**
|
|
78
|
+
* Convert between CRSs using the proj4js library.
|
|
79
|
+
*/
|
|
80
|
+
export function convertLambertCoordsToWGS84(
|
|
81
|
+
lambert: Lambert72Coordinates,
|
|
82
|
+
): GlobalCoordinates {
|
|
83
|
+
const { x, y } = lambertToWgs84.forward(lambert);
|
|
84
|
+
return { lat: y, lng: x };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Convert between CRSs using the proj4js library.
|
|
88
|
+
*/
|
|
89
|
+
export function convertWGS84CoordsToLambert(
|
|
90
|
+
wgs84: GlobalCoordinates,
|
|
91
|
+
): Lambert72Coordinates {
|
|
92
|
+
return lambertToWgs84.inverse({ x: wgs84.lng, y: wgs84.lat });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Representation of a location in the `WGS84` (globally applicable) Coordinate Reference System */
|
|
96
|
+
export type GlobalCoordinates = {
|
|
97
|
+
lat: number;
|
|
98
|
+
lng: number;
|
|
99
|
+
};
|
|
100
|
+
/** Representation of a location in the `BD72 / Belgian Lambert 72` Coordinate Reference System */
|
|
101
|
+
export type Lambert72Coordinates = {
|
|
102
|
+
x: number;
|
|
103
|
+
y: number;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Geographical position in both reference systems used in this plugin.
|
|
108
|
+
* Most mapping software assumes coordinates use the WGS84 CRS. The OSLO RDFa model as well as some
|
|
109
|
+
* of the APIs we use, use the Belgium-specific Lambert 72 CRS. To avoid the difficulties in
|
|
110
|
+
* supporting map positions and tiles following this CRS, we convert instead to WGS84, or use it
|
|
111
|
+
* directly when returned from an API.
|
|
112
|
+
*/
|
|
113
|
+
export type GeoPos = {
|
|
114
|
+
global?: GlobalCoordinates;
|
|
115
|
+
lambert: Lambert72Coordinates;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export function isSamePos(
|
|
119
|
+
a: GeoPos | undefined,
|
|
120
|
+
b: GeoPos | undefined,
|
|
121
|
+
): boolean {
|
|
122
|
+
return (
|
|
123
|
+
!!a && !!b && a.lambert.x === b.lambert.x && a.lambert.y === b.lambert.y
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function constructLambert72GMLString({ x, y }: Lambert72Coordinates) {
|
|
128
|
+
return `<gml:Point srsName="http://www.opengis.net/def/crs/EPSG/0/31370" xmlns:gml="http://www.opengis.net/gml/3.2"><gml:pos>${x} ${y}</gml:pos></gml:Point>`;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Use a regex to parse a simple point as a GML string and return the coordinates.
|
|
132
|
+
* Throws an error if the format or CRS are not recognised
|
|
133
|
+
*/
|
|
134
|
+
export function parseLambert72GMLString(gml: string): GeoPos {
|
|
135
|
+
// Parsers for GML exist in other libraries, but mostly within much larger projects (e.g.
|
|
136
|
+
// openlayers) in a way that is hard to extract due to the potential complexity of the geometries
|
|
137
|
+
// which can be represented. Since we handle only simple points, it's much less complex to just
|
|
138
|
+
// use a simple regex.
|
|
139
|
+
const [_, crs, x, y] =
|
|
140
|
+
/<gml.Point .*srsName="https?:\/\/www.opengis.net\/def\/crs\/([^"]+)".+<gml.pos>(\S+) ([^<]+)<\/gml:pos>/.exec(
|
|
141
|
+
gml,
|
|
142
|
+
) || [];
|
|
143
|
+
if (!crs || crs !== 'EPSG/0/31370') {
|
|
144
|
+
throw new AddressError({
|
|
145
|
+
translation: 'editor-plugins.address.edit.errors.http-error',
|
|
146
|
+
message: 'An error occured when querying the address register',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const lambert = { x: Number(x), y: Number(y) };
|
|
150
|
+
|
|
151
|
+
return { lambert };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Construct a string to represent a geolocation, using the Lambert 72 reference system according to
|
|
155
|
+
* [the GeoSPARQL spec]{@link https://docs.ogc.org/is/22-047r1/22-047r1.html#10-8-1-%C2%A0-well-known-text}
|
|
156
|
+
*/
|
|
157
|
+
export function constructLambert72WKTString(
|
|
158
|
+
coords: Lambert72Coordinates | Lambert72Coordinates[],
|
|
159
|
+
) {
|
|
160
|
+
if (!Array.isArray(coords)) {
|
|
161
|
+
const { x, y } = coords;
|
|
162
|
+
return `<http://www.opengis.net/def/crs/EPSG/0/31370> POINT(${x} ${y})`;
|
|
163
|
+
} else {
|
|
164
|
+
const points = coords.map(({ x, y }) => `${x} ${y}`).join(', ');
|
|
165
|
+
// Double brackets are not a mistake...
|
|
166
|
+
return `<http://www.opengis.net/def/crs/EPSG/0/31370> POLYGON((${points}))`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseWKTCoord(coordString: string): GeoPos | false {
|
|
171
|
+
const [x, y] = coordString.split(' ');
|
|
172
|
+
const lambert = { x: Number(x), y: Number(y) };
|
|
173
|
+
if (!Number.isNaN(lambert.x) && !Number.isNaN(lambert.y)) {
|
|
174
|
+
return { lambert };
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Use a regex to parse a simple point as a WKT string and return the coordinates.
|
|
180
|
+
* Throws an error if the format or CRS are not recognised
|
|
181
|
+
*/
|
|
182
|
+
export function parseLambert72WKTString(gml: string): GeoPos | GeoPos[] {
|
|
183
|
+
// Parsers for WKT exist in other libraries, but either within much larger projects (e.g.
|
|
184
|
+
// openlayers) in a way that is hard to extract due to the potential complexity of the geometries
|
|
185
|
+
// which can be represented or within untyped libraries (e.g. wicket). Since we handle only simple
|
|
186
|
+
// cases, it's much less complex to just use a simple regex.
|
|
187
|
+
|
|
188
|
+
const [_, crs, shape, dimensions] =
|
|
189
|
+
/<https?:\/\/www.opengis.net\/def\/crs\/([^"]+)> (POLYGON|POINT)\(\(?([\d,. ]+)\)\)?/.exec(
|
|
190
|
+
gml,
|
|
191
|
+
) || [];
|
|
192
|
+
if (crs && crs === 'EPSG/0/31370') {
|
|
193
|
+
if (shape === 'POINT') {
|
|
194
|
+
const point = parseWKTCoord(dimensions);
|
|
195
|
+
if (point) {
|
|
196
|
+
return point;
|
|
197
|
+
}
|
|
198
|
+
} else if (shape === 'POLYGON') {
|
|
199
|
+
const coords = dimensions.split(/, ?/).map(parseWKTCoord);
|
|
200
|
+
if (coords && coords.every(Boolean)) {
|
|
201
|
+
return coords as GeoPos[];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
throw new AddressError({
|
|
207
|
+
translation: 'editor-plugins.address.edit.errors.http-error',
|
|
208
|
+
message: 'An error occured when querying the address register',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { SayController, NodeSelection, PNode } from '@lblod/ember-rdfa-editor';
|
|
2
|
+
import { sayDataFactory } from '@lblod/ember-rdfa-editor/core/say-data-factory';
|
|
3
|
+
import {
|
|
4
|
+
PROV,
|
|
5
|
+
RDF,
|
|
6
|
+
} from '@lblod/ember-rdfa-editor-lblod-plugins/utils/constants';
|
|
7
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
8
|
+
import { findAncestors } from '@lblod/ember-rdfa-editor/utils/position-utils';
|
|
9
|
+
import {
|
|
10
|
+
Resource,
|
|
11
|
+
hasOutgoingNamedNodeTriple,
|
|
12
|
+
} from '@lblod/ember-rdfa-editor-lblod-plugins/utils/namespace';
|
|
13
|
+
import { Area, Place } from './geo-helpers';
|
|
14
|
+
import { Address } from './address-helpers';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates an 'OSLO location' node in place of the selection, along with the RDFa to create a triple
|
|
18
|
+
* with the nearest parent of one of the passed types as the subject and the predicate
|
|
19
|
+
* prov:atLocation. This doesn't work well with the RDFa tools, but since refactoring is required to
|
|
20
|
+
* clean up the RDFa structure inherited from variables and to make it work well with 'undo', this
|
|
21
|
+
* work was put off until then.
|
|
22
|
+
* @param controller - SayController
|
|
23
|
+
* @param toInsert - The object representing the location to insert
|
|
24
|
+
* @param subjectTypes - A list of Resources, each will be looked at in turn to compare the
|
|
25
|
+
* `rdf:type` of the resource, if no parent is found matching the first, then the second will be
|
|
26
|
+
* used, etc.
|
|
27
|
+
*/
|
|
28
|
+
export function replaceSelectionWithLocation(
|
|
29
|
+
controller: SayController,
|
|
30
|
+
subject: string,
|
|
31
|
+
toInsert?: Place | Address | Area,
|
|
32
|
+
subjectTypes?: Resource[],
|
|
33
|
+
) {
|
|
34
|
+
let resourceToLink: { pos: number; node: PNode } | undefined;
|
|
35
|
+
subjectTypes?.forEach((subjectType) => {
|
|
36
|
+
if (!resourceToLink) {
|
|
37
|
+
resourceToLink = findAncestors(
|
|
38
|
+
controller.mainEditorState.selection.$from,
|
|
39
|
+
(node) => {
|
|
40
|
+
if ('typeof' in node.attrs) {
|
|
41
|
+
return subjectType.matches(node.attrs.typeof);
|
|
42
|
+
}
|
|
43
|
+
return hasOutgoingNamedNodeTriple(
|
|
44
|
+
node.attrs,
|
|
45
|
+
RDF('type'),
|
|
46
|
+
subjectType,
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
)[0];
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const backlinks = !resourceToLink
|
|
53
|
+
? []
|
|
54
|
+
: [
|
|
55
|
+
{
|
|
56
|
+
predicate: PROV('atLocation').full,
|
|
57
|
+
subject: sayDataFactory.resourceNode(
|
|
58
|
+
resourceToLink.node.attrs.subject,
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
controller.withTransaction((tr) => {
|
|
64
|
+
tr.replaceSelectionWith(
|
|
65
|
+
controller.schema.node('oslo_location', {
|
|
66
|
+
subject: subject,
|
|
67
|
+
rdfaNodeType: 'resource',
|
|
68
|
+
__rdfaId: uuidv4(),
|
|
69
|
+
value: toInsert,
|
|
70
|
+
properties: [],
|
|
71
|
+
backlinks,
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
if (tr.selection.$anchor.nodeBefore) {
|
|
75
|
+
const resolvedPos = tr.doc.resolve(
|
|
76
|
+
tr.selection.anchor - tr.selection.$anchor.nodeBefore?.nodeSize,
|
|
77
|
+
);
|
|
78
|
+
tr.setSelection(new NodeSelection(resolvedPos));
|
|
79
|
+
}
|
|
80
|
+
return tr;
|
|
81
|
+
});
|
|
82
|
+
}
|