@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.
- package/README.md +2 -2
- package/dist/form-bootstrap.js +1 -1
- package/dist/form-default.js +1 -1
- package/dist/form-material.js +1 -7
- package/dist/form-material.js.LICENSE.txt +0 -30
- package/dist/util.d.ts +2 -1
- package/package.json +3 -2
- package/src/config.ts +113 -0
- package/src/constants.ts +25 -0
- package/src/constraints.ts +106 -0
- package/src/exports.ts +6 -0
- package/src/form-bootstrap.ts +12 -0
- package/src/form-default.ts +12 -0
- package/src/form-material.ts +12 -0
- package/src/form.ts +290 -0
- package/src/globals.d.ts +2 -0
- package/src/group.ts +35 -0
- package/src/loader.ts +172 -0
- package/src/node.ts +167 -0
- package/src/plugin.ts +60 -0
- package/src/plugins/file-upload.ts +26 -0
- package/src/plugins/fixed-list.ts +19 -0
- package/src/plugins/leaflet.ts +196 -0
- package/src/plugins/map-util.ts +41 -0
- package/src/plugins/mapbox.ts +157 -0
- package/src/property-template.ts +132 -0
- package/src/property.ts +188 -0
- package/src/serialize.ts +76 -0
- package/src/shacl-engine.d.ts +2 -0
- package/src/styles.css +59 -0
- package/src/theme.ts +132 -0
- package/src/themes/bootstrap.css +6 -0
- package/src/themes/bootstrap.ts +44 -0
- package/src/themes/default.css +4 -0
- package/src/themes/default.ts +240 -0
- package/src/themes/material.css +14 -0
- package/src/themes/material.ts +240 -0
- package/src/util.ts +134 -0
|
@@ -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">ⓘ Draw a polygon or marker, then close dialog</div>
|
|
27
|
+
<button class="closeButton" type="button" onclick="this.parentElement.close()">✕</button>
|
|
28
|
+
</dialog>`
|
|
29
|
+
|
|
30
|
+
const defaultCenter = { lng: 8.657238961696038, lat: 49.87627570549512 }
|
|
31
|
+
const attribution = '© <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 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">ⓘ Draw a polygon or point, then close dialog</div>
|
|
24
|
+
<button class="closeButton" type="button" onclick="this.parentElement.close()">✕</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 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
|
+
}
|