@ulb-darmstadt/shacl-form 1.6.2 → 1.6.4

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.
@@ -0,0 +1,196 @@
1
+ import * as L from 'leaflet'
2
+ import 'leaflet-editable/src/Leaflet.Editable.js'
3
+ import leafletCss from 'leaflet/dist/leaflet.css?raw'
4
+ import leafletFullscreenCss from 'leaflet.fullscreen/Control.FullScreen.css?raw'
5
+ import 'leaflet.fullscreen/Control.FullScreen.js'
6
+ import { Term } from '@rdfjs/types'
7
+
8
+ import { Plugin, PluginOptions } from '../plugin'
9
+ import { Editor, fieldFactory } from '../theme'
10
+ import { ShaclPropertyTemplate } from '../property-template'
11
+ import { Geometry, geometryToWkt, wktToGeometry, worldBounds } from './map-util'
12
+
13
+ const css = `
14
+ #shaclMapDialog .closeButton { position: absolute; right: 0; top: 0; z-index: 1; padding: 6px 8px; cursor: pointer; border: 0; background-color: #FFFA; font-size: 24px; z-index: 1000; }
15
+ #shaclMapDialog { padding: 0; width:90vw; height: 90vh; margin: auto; }
16
+ #shaclMapDialog::backdrop { background-color: #0007; }
17
+ #shaclMapDialog .closeButton:hover { background-color: #FFF }
18
+ #shaclMapDialog .hint { position: absolute; right: 60px; top: 3px; z-index: 1; padding: 4px 6px; background-color: #FFFA; border-radius: 4px; z-index: 1000; pointer-events: none; }
19
+ .leaflet-container { min-height: 300px; }
20
+ .fullscreen-icon { background-image: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjYgNTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTIwLjYgMzYuN0gxNmEuOS45IDAgMCAxLS44LS44di00LjVjMC0uMi4yLS40LjQtLjRoMS40Yy4zIDAgLjUuMi41LjR2M2gzYy4yIDAgLjQuMi40LjV2MS40YzAgLjItLjIuNC0uNC40em0tOS45LS44di00LjVjMC0uMi0uMi0uNC0uNC0uNEg4LjljLS4zIDAtLjUuMi0uNS40djNoLTNjLS4yIDAtLjQuMi0uNC41djEuNGMwIC4yLjIuNC40LjRIMTBjLjQgMCAuOC0uNC44LS44em0wIDEwLjdWNDJjMC0uNC0uNC0uOC0uOC0uOEg1LjRjLS4yIDAtLjQuMi0uNC40djEuNGMwIC4zLjIuNS40LjVoM3YzYzAgLjIuMi40LjUuNGgxLjRjLjIgMCAuNC0uMi40LS40em02LjkgMHYtM2gzYy4yIDAgLjQtLjIuNC0uNXYtMS40YzAtLjItLjItLjQtLjQtLjRIMTZjLS40IDAtLjguNC0uOC44djQuNWMwIC4yLjIuNC40LjRoMS40Yy4zIDAgLjUtLjIuNS0uNHpNNSAxMC4zVjUuOWMwLS41LjQtLjkuOS0uOWg0LjRjLjIgMCAuNC4yLjQuNFY3YzAgLjItLjIuNC0uNC40aC0zdjNjMCAuMi0uMi40LS40LjRINS40YS40LjQgMCAwIDEtLjQtLjR6bTEwLjMtNC45VjdjMCAuMi4yLjQuNC40aDN2M2MwIC4yLjIuNC40LjRoMS41Yy4yIDAgLjQtLjIuNC0uNFY1LjljMC0uNS0uNC0uOS0uOS0uOWgtNC40Yy0uMiAwLS40LjItLjQuNHptNS4zIDkuOUgxOWMtLjIgMC0uNC4yLS40LjR2M2gtM2MtLjIgMC0uNC4yLS40LjR2MS41YzAgLjIuMi40LjQuNGg0LjRjLjUgMCAuOS0uNC45LS45di00LjRjMC0uMi0uMi0uNC0uNC0uNHptLTkuOSA1LjNWMTljMC0uMi0uMi0uNC0uNC0uNGgtM3YtM2MwLS4yLS4yLS40LS40LS40SDUuNGMtLjIgMC0uNC4yLS40LjR2NC40YzAgLjUuNC45LjkuOWg0LjRjLjIgMCAuNC0uMi40LS40eiIgZmlsbD0iY3VycmVudENvbG9yIi8+PC9zdmc+); }
21
+ #shaclMapDialogContainer { width:100%; height: 100%; }
22
+ `
23
+ const dialogTemplate = `
24
+ <dialog id="shaclMapDialog" onclick="event.target==this && this.close()">
25
+ <div id="shaclMapDialogContainer"></div>
26
+ <div class="hint">&#x24D8; Draw a polygon or marker, then close dialog</div>
27
+ <button class="closeButton" type="button" onclick="this.parentElement.close()">&#x2715;</button>
28
+ </dialog>`
29
+
30
+ const defaultCenter = { lng: 8.657238961696038, lat: 49.87627570549512 }
31
+ const attribution = '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
32
+ const tileSource = 'https://tile.openstreetmap.de/{z}/{x}/{y}.png'
33
+ // const tileSource = 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'
34
+
35
+ const markerIcon = L.icon({
36
+ iconUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAFgUlEQVR4Aa1XA5BjWRTN2oW17d3YaZtr2962HUzbDNpjszW24mRt28p47v7zq/bXZtrp/lWnXr337j3nPCe85NcypgSFdugCpW5YoDAMRaIMqRi6aKq5E3YqDQO3qAwjVWrD8Ncq/RBpykd8oZUb/kaJutow8r1aP9II0WmLKLIsJyv1w/kqw9Ch2MYdB++12Onxee/QMwvf4/Dk/Lfp/i4nxTXtOoQ4pW5Aj7wpici1A9erdAN2OH64x8OSP9j3Ft3b7aWkTg/Fm91siTra0f9on5sQr9INejH6CUUUpavjFNq1B+Oadhxmnfa8RfEmN8VNAsQhPqF55xHkMzz3jSmChWU6f7/XZKNH+9+hBLOHYozuKQPxyMPUKkrX/K0uWnfFaJGS1QPRtZsOPtr3NsW0uyh6NNCOkU3Yz+bXbT3I8G3xE5EXLXtCXbbqwCO9zPQYPRTZ5vIDXD7U+w7rFDEoUUf7ibHIR4y6bLVPXrz8JVZEql13trxwue/uDivd3fkWRbS6/IA2bID4uk0UpF1N8qLlbBlXs4Ee7HLTfV1j54APvODnSfOWBqtKVvjgLKzF5YdEk5ewRkGlK0i33Eofffc7HT56jD7/6U+qH3Cx7SBLNntH5YIPvODnyfIXZYRVDPqgHtLs5ABHD3YzLuespb7t79FY34DjMwrVrcTuwlT55YMPvOBnRrJ4VXTdNnYug5ucHLBjEpt30701A3Ts+HEa73u6dT3FNWwflY86eMHPk+Yu+i6pzUpRrW7SNDg5JHR4KapmM5Wv2E8Tfcb1HoqqHMHU+uWDD7zg54mz5/2BSnizi9T1Dg4QQXLToGNCkb6tb1NU+QAlGr1++eADrzhn/u8Q2YZhQVlZ5+CAOtqfbhmaUCS1ezNFVm2imDbPmPng5wmz+gwh+oHDce0eUtQ6OGDIyR0uUhUsoO3vfDmmgOezH0mZN59x7MBi++WDL1g/eEiU3avlidO671bkLfwbw5XV2P8Pzo0ydy4t2/0eu33xYSOMOD8hTf4CrBtGMSoXfPLchX+J0ruSePw3LZeK0juPJbYzrhkH0io7B3k164hiGvawhOKMLkrQLyVpZg8rHFW7E2uHOL888IBPlNZ1FPzstSJM694fWr6RwpvcJK60+0HCILTBzZLFNdtAzJaohze60T8qBzyh5ZuOg5e7uwQppofEmf2++DYvmySqGBuKaicF1blQjhuHdvCIMvp8whTTfZzI7RldpwtSzL+F1+wkdZ2TBOW2gIF88PBTzD/gpeREAMEbxnJcaJHNHrpzji0gQCS6hdkEeYt9DF/2qPcEC8RM28Hwmr3sdNyht00byAut2k3gufWNtgtOEOFGUwcXWNDbdNbpgBGxEvKkOQsxivJx33iow0Vw5S6SVTrpVq11ysA2Rp7gTfPfktc6zhtXBBC+adRLshf6sG2RfHPZ5EAc4sVZ83yCN00Fk/4kggu40ZTvIEm5g24qtU4KjBrx/BTTH8ifVASAG7gKrnWxJDcU7x8X6Ecczhm3o6YicvsLXWfh3Ch1W0k8x0nXF+0fFxgt4phz8QvypiwCCFKMqXCnqXExjq10beH+UUA7+nG6mdG/Pu0f3LgFcGrl2s0kNNjpmoJ9o4B29CMO8dMT4Q5ox8uitF6fqsrJOr8qnwNbRzv6hSnG5wP+64C7h9lp30hKNtKdWjtdkbuPA19nJ7Tz3zR/ibgARbhb4AlhavcBebmTHcFl2fvYEnW0ox9xMxKBS8btJ+KiEbq9zA4RthQXDhPa0T9TEe69gWupwc6uBUphquXgf+/FrIjweHQS4/pduMe5ERUMHUd9xv8ZR98CxkS4F2n3EUrUZ10EYNw7BWm9x1GiPssi3GgiGRDKWRYZfXlON+dfNbM+GgIwYdwAAAAASUVORK5CYII=',
37
+ shadowUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACkAAAApCAQAAAACach9AAACMUlEQVR4Ae3ShY7jQBAE0Aoz/f9/HTMzhg1zrdKUrJbdx+Kd2nD8VNudfsL/Th///dyQN2TH6f3y/BGpC379rV+S+qqetBOxImNQXL8JCAr2V4iMQXHGNJxeCfZXhSRBcQMfvkOWUdtfzlLgAENmZDcmo2TVmt8OSM2eXxBp3DjHSMFutqS7SbmemzBiR+xpKCNUIRkdkkYxhAkyGoBvyQFEJEefwSmmvBfJuJ6aKqKWnAkvGZOaZXTUgFqYULWNSHUckZuR1HIIimUExutRxwzOLROIG4vKmCKQt364mIlhSyzAf1m9lHZHJZrlAOMMztRRiKimp/rpdJDc9Awry5xTZCte7FHtuS8wJgeYGrex28xNTd086Dik7vUMscQOa8y4DoGtCCSkAKlNwpgNtphjrC6MIHUkR6YWxxs6Sc5xqn222mmCRFzIt8lEdKx+ikCtg91qS2WpwVfBelJCiQJwvzixfI9cxZQWgiSJelKnwBElKYtDOb2MFbhmUigbReQBV0Cg4+qMXSxXSyGUn4UbF8l+7qdSGnTC0XLCmahIgUHLhLOhpVCtw4CzYXvLQWQbJNmxoCsOKAxSgBJno75avolkRw8iIAFcsdc02e9iyCd8tHwmeSSoKTowIgvscSGZUOA7PuCN5b2BX9mQM7S0wYhMNU74zgsPBj3HU7wguAfnxxjFQGBE6pwN+GjME9zHY7zGp8wVxMShYX9NXvEWD3HbwJf4giO4CFIQxXScH1/TM+04kkBiAAAAAElFTkSuQmCC',
38
+
39
+ iconSize: [25, 41], // size of the icon
40
+ shadowSize: [41, 41], // size of the shadow
41
+ iconAnchor: [12, 41], // point of the icon which will correspond to marker's location
42
+ shadowAnchor: [14, 41], // the same for the shadow
43
+ popupAnchor: [-3, -76] // point from which the popup should open relative to the iconAnchor
44
+ })
45
+
46
+ export class LeafletPlugin extends Plugin {
47
+ map: L.Map | undefined
48
+ currentEditor: Editor | undefined
49
+ createdGeometry: Geometry | undefined
50
+ displayedShape: L.Polygon | L.Marker | undefined
51
+
52
+ constructor(options: PluginOptions) {
53
+ super(options, leafletCss + '\n' + leafletFullscreenCss + '\n' + css)
54
+ }
55
+
56
+ initEditMode(form: HTMLElement): HTMLDialogElement {
57
+ form.insertAdjacentHTML('beforeend', dialogTemplate)
58
+ const container = form.querySelector('#shaclMapDialogContainer') as HTMLElement
59
+ this.map = L.map(container, {
60
+ fullscreenControl: true,
61
+ editable: true,
62
+ layers: [ L.tileLayer(tileSource) ],
63
+ zoom: 5,
64
+ maxBounds: worldBounds,
65
+ center: defaultCenter
66
+ })
67
+ this.map.attributionControl.addAttribution(attribution)
68
+
69
+ const EditControl = L.Control.extend({ options: { position: 'topleft', callback: null, kind: '', html: '' },
70
+ onAdd: function (map: L.Map) {
71
+ let container = L.DomUtil.create('div', 'leaflet-control leaflet-bar')
72
+ let link = L.DomUtil.create('a', '', container)
73
+ link.href = '#';
74
+ link.title = 'Create a new ' + this.options.kind;
75
+ link.innerHTML = this.options.html;
76
+ L.DomEvent.on(link, 'click', L.DomEvent.stop).on(link, 'click', () => {
77
+ // @ts-ignore
78
+ window.LAYER = this.options.callback.call(map.editTools)
79
+ }, this)
80
+ return container
81
+ }
82
+ })
83
+ this.map.addControl(new (EditControl.extend({
84
+ options: {
85
+ callback: () => {
86
+ this.displayedShape?.remove()
87
+ this.displayedShape = this.map?.editTools.startPolygon()
88
+ },
89
+ kind: 'polygon',
90
+ html: '▰'
91
+ }
92
+ }))())
93
+ this.map.addControl(new (EditControl.extend({
94
+ options: {
95
+ callback: () => {
96
+ this.displayedShape?.remove()
97
+ this.displayedShape = this.map?.editTools.startMarker(undefined, { icon: markerIcon })
98
+ },
99
+ kind: 'marker',
100
+ html: '•'
101
+ }
102
+ }))())
103
+ this.map.on('editable:drawing:end', () => { this.saveChanges() })
104
+ this.map.on('editable:vertex:dragend', () => { this.saveChanges() })
105
+
106
+ const dialog = form.querySelector('#shaclMapDialog') as HTMLDialogElement
107
+ dialog.addEventListener('close', () => {
108
+ const scrollY = document.body.style.top
109
+ document.body.style.position = ''
110
+ document.body.style.top = ''
111
+ window.scrollTo(0, parseInt(scrollY || '0') * -1)
112
+ // set wkt in editor
113
+ if (this.currentEditor && this.createdGeometry) {
114
+ this.currentEditor.value = geometryToWkt(this.createdGeometry)
115
+ this.currentEditor.dispatchEvent(new Event('change', { bubbles: true }))
116
+ }
117
+ })
118
+ return dialog
119
+ }
120
+
121
+ createEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement {
122
+ let dialog = template.config.form.querySelector('#shaclMapDialog') as HTMLDialogElement
123
+ if (!dialog) {
124
+ dialog = this.initEditMode(template.config.form)
125
+ }
126
+ const button = template.config.theme.createButton('Open&#160;map...', false)
127
+ button.style.marginLeft = '5px'
128
+ button.classList.add('open-map-button')
129
+ button.onclick = () => {
130
+ this.currentEditor = instance.querySelector('.editor') as Editor
131
+ this.createdGeometry = undefined
132
+ this.displayedShape?.remove()
133
+ this.drawAndZoomToGeometry(wktToGeometry(this.currentEditor.value || ''), this.map!)
134
+
135
+ document.body.style.top = `-${window.scrollY}px`
136
+ document.body.style.position = 'fixed'
137
+ dialog.showModal()
138
+ }
139
+ const instance = fieldFactory(template, value || null)
140
+ instance.appendChild(button)
141
+ return instance
142
+ }
143
+
144
+ createViewer(template: ShaclPropertyTemplate, value: Term): HTMLElement {
145
+ const container = document.createElement('div')
146
+ const geometry = wktToGeometry(value.value)
147
+ if (geometry?.coordinates?.length) {
148
+ const map = L.map(container, {
149
+ fullscreenControl: true,
150
+ layers: [ L.tileLayer(tileSource) ],
151
+ zoom: 5,
152
+ center: defaultCenter,
153
+ maxBounds: worldBounds
154
+ })
155
+ map.attributionControl.addAttribution(attribution)
156
+ this.drawAndZoomToGeometry(geometry, map)
157
+ }
158
+ return container
159
+ }
160
+
161
+ drawAndZoomToGeometry(geometry: Geometry | undefined, map: L.Map) {
162
+ setTimeout(() => { map.invalidateSize() })
163
+ if (geometry?.type === 'Point') {
164
+ const coords = { lng: geometry.coordinates[0], lat: geometry.coordinates[1] }
165
+ this.displayedShape = L.marker(coords, { icon: markerIcon }).addTo(map)
166
+ map.setView(coords, 15, { animate: false })
167
+ } else if (geometry?.type === 'Polygon') {
168
+ const coords = geometry.coordinates[0].map((pos) => { return { lng: pos[0], lat: pos[1] }})
169
+ const polygon = L.polygon(coords).addTo(map)
170
+ this.displayedShape = polygon
171
+ map.fitBounds(polygon.getBounds(), { animate: false })
172
+ setTimeout(() => {
173
+ map.fitBounds(polygon.getBounds(), { animate: false })
174
+ map.setView(polygon.getCenter(), undefined, { animate: false })
175
+ }, 1)
176
+ } else {
177
+ map.setZoom(5)
178
+ }
179
+ }
180
+
181
+ saveChanges() {
182
+ if (this.displayedShape instanceof L.Marker) {
183
+ const pos = this.displayedShape.getLatLng()
184
+ this.createdGeometry = { type: 'Point', coordinates: [pos.lng, pos.lat] }
185
+ } else if (this.displayedShape instanceof L.Polygon) {
186
+ const positions = this.displayedShape.getLatLngs() as L.LatLng[][]
187
+ // force closed polygon
188
+ if (!positions[0][0].equals(positions[0][positions[0].length - 1])) {
189
+ positions[0].push(positions[0][0])
190
+ }
191
+ this.createdGeometry = { type: 'Polygon', coordinates: [positions[0].map((pos) => { return [ pos.lng, pos.lat ] })] }
192
+ } else {
193
+ this.createdGeometry = undefined
194
+ }
195
+ }
196
+ }
@@ -0,0 +1,41 @@
1
+ import { Point, Polygon } from 'geojson'
2
+
3
+ export type Geometry = Point | Polygon
4
+
5
+ export const worldBounds: [number, number][] = [[-90, -180], [90, 180]]
6
+
7
+ export function wktToGeometry(wkt: string): Geometry | undefined {
8
+ const pointCoords = wkt.match(/^POINT\((.*)\)$/)
9
+ if (pointCoords?.length == 2) {
10
+ const xy = pointCoords[1].split(' ')
11
+ if (xy.length === 2) {
12
+ return { type: 'Point', coordinates: [parseFloat(xy[0]), parseFloat(xy[1])] }
13
+ }
14
+ }
15
+ const polygonCoords = wkt.match(/^POLYGON[(]{2}(.*)[)]{2}$/)
16
+ if (polygonCoords?.length == 2) {
17
+ const split = polygonCoords[1].split(',')
18
+ if (split.length > 2) {
19
+ const coords: number[][][] = []
20
+ const outer: number[][] = []
21
+ coords.push(outer)
22
+ for (const coord of split) {
23
+ const xy = coord.split(' ')
24
+ if (xy.length === 2) {
25
+ outer.push([parseFloat(xy[0]), parseFloat(xy[1])])
26
+ }
27
+ }
28
+ return { type: 'Polygon', coordinates: coords }
29
+ }
30
+ }
31
+ }
32
+
33
+ export function geometryToWkt(geometry: Geometry): string {
34
+ if (geometry.type === 'Point') {
35
+ return `POINT(${geometry.coordinates.join(' ')})`
36
+ } else if (geometry.type === 'Polygon') {
37
+ return `POLYGON((${geometry.coordinates[0].map(item => { return item.join(' ') }).join(',')}))`
38
+ } else {
39
+ return ''
40
+ }
41
+ }
@@ -0,0 +1,157 @@
1
+ import { Term } from '@rdfjs/types'
2
+ import { Plugin, PluginOptions } from '../plugin'
3
+ import { ShaclPropertyTemplate } from '../property-template'
4
+ import { Editor, fieldFactory } from '../theme'
5
+ import { Map, NavigationControl, FullscreenControl, LngLatBounds, LngLatLike } from 'mapbox-gl'
6
+ import MapboxDraw from '@mapbox/mapbox-gl-draw'
7
+ import mapboxGlCss from 'mapbox-gl/dist/mapbox-gl.css'
8
+ import mapboxGlDrawCss from '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'
9
+ import { Geometry, geometryToWkt, wktToGeometry } from './map-util'
10
+
11
+ const css = `
12
+ #shaclMapDialog .closeButton { position: absolute; right: 0; top: 0; z-index: 1; padding: 6px 8px; cursor: pointer; border: 0; background-color: #FFFA; font-size: 24px; }
13
+ #shaclMapDialog { padding: 0; width:90vw; height: 90vh; margin: auto; }
14
+ #shaclMapDialog::backdrop { background-color: #0007; }
15
+ #shaclMapDialog .closeButton:hover { background-color: #FFF }
16
+ #shaclMapDialog .hint { position: absolute; right: 60px; top: 3px; z-index: 1; padding: 4px 6px; background-color: #FFFA; border-radius: 4px; }
17
+ .mapboxgl-map { min-height: 300px; }
18
+ #shaclMapDialogContainer { width:100%; height: 100% }
19
+ `
20
+ const dialogTemplate = `
21
+ <dialog id="shaclMapDialog" onclick="event.target==this && this.close()">
22
+ <div id="shaclMapDialogContainer"></div>
23
+ <div class="hint">&#x24D8; Draw a polygon or point, then close dialog</div>
24
+ <button class="closeButton" type="button" onclick="this.parentElement.close()">&#x2715;</button>
25
+ </dialog>`
26
+
27
+
28
+ export class MapboxPlugin extends Plugin {
29
+ map: Map | undefined
30
+ draw: MapboxDraw | undefined
31
+ currentEditor: Editor | undefined
32
+ apiKey: string
33
+
34
+ constructor(options: PluginOptions, apiKey: string) {
35
+ super(options, mapboxGlCss + '\n' + mapboxGlDrawCss + '\n' + css)
36
+ this.apiKey = apiKey
37
+ }
38
+
39
+ initEditMode(form: HTMLElement): HTMLDialogElement {
40
+ form.insertAdjacentHTML('beforeend', dialogTemplate)
41
+ const container = form.querySelector('#shaclMapDialogContainer') as HTMLElement
42
+ this.map = new Map({
43
+ container: container,
44
+ style: 'mapbox://styles/mapbox/satellite-streets-v11',
45
+ zoom: 5,
46
+ center: { lng: 8.657238961696038, lat: 49.87627570549512 },
47
+ attributionControl: false,
48
+ accessToken: this.apiKey
49
+ })
50
+
51
+ this.draw = new MapboxDraw({
52
+ displayControlsDefault: false,
53
+ controls: { point: true, polygon: true }
54
+ })
55
+ this.map.addControl(new NavigationControl(), 'top-left')
56
+ this.map.addControl(this.draw, 'top-left')
57
+
58
+ this.map.on('idle', () => {
59
+ // this fixes wrong size of canvas
60
+ this.map!.resize()
61
+ })
62
+ // @ts-ignore
63
+ this.map.on('draw.create', () => this.deleteAllButLastDrawing())
64
+
65
+ const dialog = form.querySelector('#shaclMapDialog') as HTMLDialogElement
66
+ dialog.addEventListener('close', () => {
67
+ const scrollY = document.body.style.top
68
+ document.body.style.position = ''
69
+ document.body.style.top = ''
70
+ window.scrollTo(0, parseInt(scrollY || '0') * -1)
71
+ // set wkt in editor
72
+ const data = this.draw!.getAll()
73
+ if (data && data.features.length && this.currentEditor) {
74
+ const geometry = data.features[0].geometry as Geometry
75
+ if (geometry.coordinates?.length) {
76
+ const wkt = geometryToWkt(geometry)
77
+ this.currentEditor.value = wkt
78
+ this.currentEditor.dispatchEvent(new Event('change', { bubbles: true }))
79
+ }
80
+ }
81
+ })
82
+ return dialog
83
+ }
84
+
85
+ createEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement {
86
+ let dialog = template.config.form.querySelector('#shaclMapDialog') as HTMLDialogElement
87
+ if (!dialog) {
88
+ dialog = this.initEditMode(template.config.form)
89
+ }
90
+ const button = template.config.theme.createButton('Open&#160;map...', false)
91
+ button.style.marginLeft = '5px'
92
+ button.classList.add('open-map-button')
93
+ button.onclick = () => {
94
+ this.currentEditor = instance.querySelector('.editor') as Editor
95
+ this.draw?.deleteAll()
96
+
97
+ const wkt = this.currentEditor.value || ''
98
+ const geometry = wktToGeometry(wkt)
99
+ if (geometry && geometry.coordinates?.length) {
100
+ this.draw?.add(geometry)
101
+ this.fitToGeometry(this.map!, geometry)
102
+ } else {
103
+ this.map?.setZoom(5)
104
+ }
105
+ document.body.style.top = `-${window.scrollY}px`
106
+ document.body.style.position = 'fixed'
107
+ dialog.showModal()
108
+ }
109
+ const instance = fieldFactory(template, value || null)
110
+ instance.appendChild(button)
111
+ return instance
112
+ }
113
+
114
+ createViewer(template: ShaclPropertyTemplate, value: Term): HTMLElement {
115
+ const container = document.createElement('div')
116
+ const geometry = wktToGeometry(value.value)
117
+ if (geometry?.coordinates?.length) {
118
+ // wait for container to be available in DOM
119
+ setTimeout(() => {
120
+ const draw = new MapboxDraw({ displayControlsDefault: false })
121
+ const map = new Map({
122
+ container: container,
123
+ style: 'mapbox://styles/mapbox/satellite-streets-v11',
124
+ zoom: 5,
125
+ attributionControl: false,
126
+ accessToken: this.apiKey
127
+ })
128
+ map.addControl(draw)
129
+ map.addControl(new FullscreenControl())
130
+ draw.add(geometry)
131
+ this.fitToGeometry(map, geometry)
132
+ })
133
+ }
134
+ return container
135
+ }
136
+
137
+ fitToGeometry(map: Map, geometry: Geometry) {
138
+ if (typeof geometry.coordinates[0] === 'number') {
139
+ // e.g. Point
140
+ map.setCenter(geometry.coordinates as LngLatLike)
141
+ map.setZoom(15)
142
+ } else {
143
+ // e.g. Polygon
144
+ const bounds = geometry.coordinates[0].reduce((bounds, coord) => {
145
+ return bounds.extend(coord as mapboxgl.LngLatLike)
146
+ }, new LngLatBounds(geometry.coordinates[0][0] as mapboxgl.LngLatLike, geometry.coordinates[0][0] as mapboxgl.LngLatLike))
147
+ map.fitBounds(bounds, { padding: 20, animate: false })
148
+ }
149
+ }
150
+
151
+ deleteAllButLastDrawing() {
152
+ const data = this.draw!.getAll()
153
+ for (let i = 0; i < data.features.length - 1; i++) {
154
+ this.draw!.delete(data.features[i].id as string)
155
+ }
156
+ }
157
+ }
@@ -0,0 +1,132 @@
1
+ import { Literal, NamedNode, Quad, DataFactory } from 'n3'
2
+ import { Term } from '@rdfjs/types'
3
+ import { OWL_PREDICATE_IMPORTS, PREFIX_DASH, PREFIX_OA, PREFIX_RDF, PREFIX_SHACL, SHACL_PREDICATE_CLASS, SHACL_PREDICATE_TARGET_CLASS } from './constants'
4
+ import { Config } from './config'
5
+ import { findLabel, prioritizeByLanguage, removePrefixes } from './util'
6
+ import { ShaclNode } from './node'
7
+
8
+ const mappers: Record<string, (template: ShaclPropertyTemplate, term: Term) => void> = {
9
+ [`${PREFIX_SHACL}name`]: (template, term) => { const literal = term as Literal; template.name = prioritizeByLanguage(template.config.languages, template.name, literal) },
10
+ [`${PREFIX_SHACL}description`]: (template, term) => { const literal = term as Literal; template.description = prioritizeByLanguage(template.config.languages, template.description, literal) },
11
+ [`${PREFIX_SHACL}path`]: (template, term) => { template.path = term.value },
12
+ [`${PREFIX_SHACL}node`]: (template, term) => { template.node = term as NamedNode },
13
+ [`${PREFIX_SHACL}datatype`]: (template, term) => { template.datatype = term as NamedNode },
14
+ [`${PREFIX_SHACL}nodeKind`]: (template, term) => { template.nodeKind = term as NamedNode },
15
+ [`${PREFIX_SHACL}minCount`]: (template, term) => { template.minCount = parseInt(term.value) },
16
+ [`${PREFIX_SHACL}maxCount`]: (template, term) => { template.maxCount = parseInt(term.value) },
17
+ [`${PREFIX_SHACL}minLength`]: (template, term) => { template.minLength = parseInt(term.value) },
18
+ [`${PREFIX_SHACL}maxLength`]: (template, term) => { template.maxLength = parseInt(term.value) },
19
+ [`${PREFIX_SHACL}minInclusive`]: (template, term) => { template.minInclusive = parseInt(term.value) },
20
+ [`${PREFIX_SHACL}maxInclusive`]: (template, term) => { template.maxInclusive = parseInt(term.value) },
21
+ [`${PREFIX_SHACL}minExclusive`]: (template, term) => { template.minExclusive = parseInt(term.value) },
22
+ [`${PREFIX_SHACL}maxExclusive`]: (template, term) => { template.maxExclusive = parseInt(term.value) },
23
+ [`${PREFIX_SHACL}pattern`]: (template, term) => { template.pattern = term.value },
24
+ [`${PREFIX_SHACL}order`]: (template, term) => { template.order = parseInt(term.value) },
25
+ [`${PREFIX_DASH}singleLine`]: (template, term) => { template.singleLine = term.value === 'true' },
26
+ [`${PREFIX_OA}styleClass`]: (template, term) => { template.cssClass = term.value },
27
+ [`${PREFIX_SHACL}and`]: (template, term) => { template.shaclAnd = term.value },
28
+ [`${PREFIX_SHACL}in`]: (template, term) => { template.shaclIn = term.value },
29
+ // sh:datatype might be undefined, but sh:languageIn defined. this is undesired. the spec says, that strings without a lang tag are not valid if sh:languageIn is set. but the shacl validator accepts these as valid. to prevent this, we just set the datatype here to 'langString'.
30
+ [`${PREFIX_SHACL}languageIn`]: (template, term) => { template.languageIn = template.config.lists[term.value]; template.datatype = DataFactory.namedNode(PREFIX_RDF + 'langString') },
31
+ [`${PREFIX_SHACL}defaultValue`]: (template, term) => { template.defaultValue = term },
32
+ [`${PREFIX_SHACL}hasValue`]: (template, term) => { template.hasValue = term },
33
+ [OWL_PREDICATE_IMPORTS.id]: (template, term) => { template.owlImports.push(term as NamedNode) },
34
+ [SHACL_PREDICATE_CLASS.id]: (template, term) => {
35
+ template.class = term as NamedNode
36
+ // try to find node shape that has requested target class
37
+ const nodeShapes = template.config.shapesGraph.getSubjects(SHACL_PREDICATE_TARGET_CLASS, term, null)
38
+ if (nodeShapes.length > 0) {
39
+ template.node = nodeShapes[0] as NamedNode
40
+ }
41
+ },
42
+ [`${PREFIX_SHACL}or`]: (template, term) => {
43
+ const list = template.config.lists[term.value]
44
+ if (list?.length) {
45
+ template.shaclOr = list
46
+ } else {
47
+ console.error('list not found:', term.value, 'existing lists:', template.config.lists)
48
+ }
49
+ }
50
+ }
51
+
52
+ export class ShaclPropertyTemplate {
53
+ parent: ShaclNode
54
+ label = ''
55
+ name: Literal | undefined
56
+ description: Literal | undefined
57
+ path: string | undefined
58
+ node: NamedNode | undefined
59
+ class: NamedNode | undefined
60
+ minCount: number | undefined
61
+ maxCount: number | undefined
62
+ minLength: number | undefined
63
+ maxLength: number | undefined
64
+ minInclusive: number | undefined
65
+ maxInclusive: number | undefined
66
+ minExclusive: number | undefined
67
+ maxExclusive: number | undefined
68
+ singleLine: boolean | undefined
69
+ cssClass: string | undefined
70
+ defaultValue: Term | undefined
71
+ pattern: string | undefined
72
+ order: number | undefined
73
+ nodeKind: NamedNode | undefined
74
+ shaclAnd: string | undefined
75
+ shaclIn: string | undefined
76
+ shaclOr: Term[] | undefined
77
+ languageIn: Term[] | undefined
78
+ datatype: NamedNode | undefined
79
+ hasValue: Term | undefined
80
+ owlImports: NamedNode[] = []
81
+
82
+ config: Config
83
+ extendedShapes: NamedNode[] | undefined
84
+
85
+ constructor(quads: Quad[], parent: ShaclNode, config: Config) {
86
+ this.parent = parent
87
+ this.config = config
88
+ this.merge(quads)
89
+ }
90
+
91
+ merge(quads: Quad[]): ShaclPropertyTemplate {
92
+ for (const quad of quads) {
93
+ mappers[quad.predicate.id]?.call(this, this, quad.object)
94
+ }
95
+ // provide best fitting label for UI
96
+ this.label = this.name?.value || findLabel(quads, this.config.languages)
97
+ if (!this.label && !this.shaclAnd) {
98
+ this.label = this.path ? removePrefixes(this.path, this.config.prefixes) : 'unknown'
99
+ }
100
+ // resolve extended shapes
101
+ if (this.node || this.shaclAnd) {
102
+ this.extendedShapes = []
103
+ if (this.node) {
104
+ this.extendedShapes.push(this.node)
105
+ }
106
+ if (this.shaclAnd) {
107
+ const list = this.config.lists[this.shaclAnd]
108
+ if (list?.length) {
109
+ for (const node of list) {
110
+ this.extendedShapes.push(node as NamedNode)
111
+ }
112
+ }
113
+ }
114
+ }
115
+ return this
116
+ }
117
+
118
+ clone(): ShaclPropertyTemplate {
119
+ const copy = Object.assign({}, this)
120
+ // arrays are not cloned but referenced, so create them manually
121
+ copy.owlImports = [ ...this.owlImports ]
122
+ if (this.languageIn) {
123
+ copy.languageIn = [ ...this.languageIn ]
124
+ }
125
+ if (this.shaclOr) {
126
+ copy.shaclOr = [ ...this.shaclOr ]
127
+ }
128
+ copy.merge = this.merge.bind(copy)
129
+ copy.clone = this.clone.bind(copy)
130
+ return copy
131
+ }
132
+ }