@ulb-darmstadt/shacl-form 1.10.4 → 2.0.0-rc2
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/dist/bundle.d.ts +3 -0
- package/dist/bundle.js +415 -0
- package/dist/constants.d.ts +16 -16
- package/dist/constraints.d.ts +2 -2
- package/dist/form.d.ts +4 -3
- package/dist/index.js +62 -0
- package/dist/plugins/assets/plugin-VN3CfgGe.js +1 -0
- package/dist/plugins/file-upload.js +1 -0
- package/dist/plugins/leaflet.d.ts +8 -2
- package/dist/plugins/leaflet.js +5 -708
- package/dist/{themes/default.d.ts → theme.default.d.ts} +2 -2
- package/package.json +20 -26
- package/dist/form-bootstrap.d.ts +0 -5
- package/dist/form-bootstrap.js +0 -413
- package/dist/form-default.d.ts +0 -5
- package/dist/form-default.js +0 -402
- package/dist/form-material.d.ts +0 -5
- package/dist/form-material.js +0 -722
- package/dist/plugins/fixed-list.d.ts +0 -9
- package/dist/plugins/map-util.d.ts +0 -5
- package/dist/plugins/mapbox.d.ts +0 -19
- package/dist/plugins/mapbox.js +0 -2904
- package/dist/themes/bootstrap.d.ts +0 -10
- package/dist/themes/material.d.ts +0 -15
- package/src/config.ts +0 -110
- package/src/constants.ts +0 -30
- package/src/constraints.ts +0 -149
- package/src/exports.ts +0 -7
- package/src/form-bootstrap.ts +0 -12
- package/src/form-default.ts +0 -12
- package/src/form-material.ts +0 -12
- package/src/form.ts +0 -319
- package/src/globals.d.ts +0 -2
- package/src/group.ts +0 -34
- package/src/loader.ts +0 -187
- package/src/node.ts +0 -192
- package/src/plugin.ts +0 -60
- package/src/plugins/file-upload.ts +0 -26
- package/src/plugins/fixed-list.ts +0 -19
- package/src/plugins/leaflet.ts +0 -196
- package/src/plugins/map-util.ts +0 -41
- package/src/plugins/mapbox.ts +0 -157
- package/src/property-template.ts +0 -151
- package/src/property.ts +0 -309
- package/src/serialize.ts +0 -96
- package/src/shacl-engine.d.ts +0 -2
- package/src/styles.css +0 -49
- package/src/theme.ts +0 -132
- package/src/themes/bootstrap.css +0 -6
- package/src/themes/bootstrap.ts +0 -44
- package/src/themes/default.css +0 -4
- package/src/themes/default.ts +0 -258
- package/src/themes/material.css +0 -14
- package/src/themes/material.ts +0 -253
- package/src/util.ts +0 -275
package/src/node.ts
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { BlankNode, DataFactory, NamedNode, Store } from 'n3'
|
|
2
|
-
import { Term } from '@rdfjs/types'
|
|
3
|
-
import { PREFIX_SHACL, RDF_PREDICATE_TYPE, OWL_PREDICATE_IMPORTS, SHACL_PREDICATE_PROPERTY, SHACL_PREDICATE_NODE } from './constants'
|
|
4
|
-
import { ShaclProperty } from './property'
|
|
5
|
-
import { createShaclGroup } from './group'
|
|
6
|
-
import { v4 as uuidv4 } from 'uuid'
|
|
7
|
-
import { createShaclOrConstraint, resolveShaclOrConstraintOnNode } from './constraints'
|
|
8
|
-
import { Config } from './config'
|
|
9
|
-
|
|
10
|
-
export class ShaclNode extends HTMLElement {
|
|
11
|
-
parent: ShaclNode | undefined
|
|
12
|
-
shaclSubject: NamedNode
|
|
13
|
-
nodeId: NamedNode | BlankNode
|
|
14
|
-
targetClass: NamedNode | undefined
|
|
15
|
-
owlImports: NamedNode[] = []
|
|
16
|
-
config: Config
|
|
17
|
-
linked: boolean
|
|
18
|
-
|
|
19
|
-
constructor(shaclSubject: NamedNode, config: Config, valueSubject: NamedNode | BlankNode | undefined, parent?: ShaclNode, nodeKind?: NamedNode, label?: string, linked?: boolean) {
|
|
20
|
-
super()
|
|
21
|
-
|
|
22
|
-
this.parent = parent
|
|
23
|
-
this.config = config
|
|
24
|
-
this.shaclSubject = shaclSubject
|
|
25
|
-
this.linked = linked || false
|
|
26
|
-
let nodeId: NamedNode | BlankNode | undefined = valueSubject
|
|
27
|
-
if (!nodeId) {
|
|
28
|
-
// if no value subject given, create new node id with a type depending on own nodeKind or given parent property nodeKind
|
|
29
|
-
if (!nodeKind) {
|
|
30
|
-
const spec = config.store.getObjects(shaclSubject, `${PREFIX_SHACL}nodeKind`, null)
|
|
31
|
-
if (spec.length) {
|
|
32
|
-
nodeKind = spec[0] as NamedNode
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
// if nodeKind is not set, but a value namespace is configured or if nodeKind is sh:IRI, then create a NamedNode
|
|
36
|
-
if ((nodeKind === undefined && config.attributes.valuesNamespace) || nodeKind?.value === `${PREFIX_SHACL}IRI`) {
|
|
37
|
-
// no requirements on node type, so create a NamedNode and use configured value namespace
|
|
38
|
-
nodeId = DataFactory.namedNode(config.attributes.valuesNamespace + uuidv4())
|
|
39
|
-
} else {
|
|
40
|
-
// otherwise create a BlankNode
|
|
41
|
-
nodeId = DataFactory.blankNode(uuidv4())
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
this.nodeId = nodeId
|
|
45
|
-
|
|
46
|
-
// check if the form already contains the node/value pair to prevent recursion
|
|
47
|
-
const id = JSON.stringify([shaclSubject, valueSubject])
|
|
48
|
-
if (valueSubject && config.renderedNodes.has(id)) {
|
|
49
|
-
// node/value pair is already rendered in the form, so just display a reference
|
|
50
|
-
label = label || "Link"
|
|
51
|
-
const labelElem = document.createElement('label')
|
|
52
|
-
labelElem.innerText = label
|
|
53
|
-
labelElem.classList.add('linked')
|
|
54
|
-
this.appendChild(labelElem)
|
|
55
|
-
|
|
56
|
-
const anchor = document.createElement('a')
|
|
57
|
-
let refId = (valueSubject.termType === 'BlankNode') ? '_:' + valueSubject.value : valueSubject.value
|
|
58
|
-
anchor.innerText = refId
|
|
59
|
-
anchor.classList.add('ref-link')
|
|
60
|
-
anchor.onclick = () => {
|
|
61
|
-
// if anchor is clicked, scroll referenced shacl node into view
|
|
62
|
-
this.config.form.querySelector(`shacl-node[data-node-id='${refId}']`)?.scrollIntoView()
|
|
63
|
-
}
|
|
64
|
-
this.appendChild(anchor)
|
|
65
|
-
this.style.flexDirection = 'row'
|
|
66
|
-
} else {
|
|
67
|
-
if (valueSubject) {
|
|
68
|
-
config.renderedNodes.add(id)
|
|
69
|
-
}
|
|
70
|
-
this.dataset.nodeId = this.nodeId.id
|
|
71
|
-
if (this.config.attributes.showNodeIds !== null) {
|
|
72
|
-
const div = document.createElement('div')
|
|
73
|
-
div.innerText = `id: ${this.nodeId.id}`
|
|
74
|
-
div.classList.add('node-id-display')
|
|
75
|
-
this.appendChild(div)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// first initialize owl:imports, this is needed before adding properties to properly resolve class instances etc.
|
|
79
|
-
for (const owlImport of config.store.getQuads(shaclSubject, OWL_PREDICATE_IMPORTS, null, null)) {
|
|
80
|
-
this.owlImports.push(owlImport.object as NamedNode)
|
|
81
|
-
}
|
|
82
|
-
// now parse other node quads
|
|
83
|
-
for (const quad of config.store.getQuads(shaclSubject, null, null, null)) {
|
|
84
|
-
switch (quad.predicate.id) {
|
|
85
|
-
case SHACL_PREDICATE_PROPERTY.id:
|
|
86
|
-
this.addPropertyInstance(quad.object, config, valueSubject)
|
|
87
|
-
break;
|
|
88
|
-
case `${PREFIX_SHACL}and`:
|
|
89
|
-
// inheritance via sh:and
|
|
90
|
-
const list = config.lists[quad.object.value]
|
|
91
|
-
if (list?.length) {
|
|
92
|
-
for (const shape of list) {
|
|
93
|
-
this.prepend(new ShaclNode(shape as NamedNode, config, valueSubject, this))
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
console.error('list not found:', quad.object.value, 'existing lists:', config.lists)
|
|
98
|
-
}
|
|
99
|
-
break;
|
|
100
|
-
case SHACL_PREDICATE_NODE.id:
|
|
101
|
-
// inheritance via sh:node
|
|
102
|
-
this.prepend(new ShaclNode(quad.object as NamedNode, config, valueSubject, this))
|
|
103
|
-
break;
|
|
104
|
-
case `${PREFIX_SHACL}targetClass`:
|
|
105
|
-
this.targetClass = quad.object as NamedNode
|
|
106
|
-
break;
|
|
107
|
-
case `${PREFIX_SHACL}or`:
|
|
108
|
-
this.tryResolve(quad.object, valueSubject, config)
|
|
109
|
-
break;
|
|
110
|
-
case `${PREFIX_SHACL}xone`:
|
|
111
|
-
this.tryResolve(quad.object, valueSubject, config)
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (label) {
|
|
117
|
-
const header = document.createElement('h1')
|
|
118
|
-
header.innerText = label
|
|
119
|
-
this.prepend(header)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
toRDF(graph: Store, subject?: NamedNode | BlankNode): (NamedNode | BlankNode) {
|
|
125
|
-
if (!subject) {
|
|
126
|
-
subject = this.nodeId
|
|
127
|
-
}
|
|
128
|
-
// output triples only if node is not a link
|
|
129
|
-
if (!this.linked) {
|
|
130
|
-
for (const shape of this.querySelectorAll(':scope > shacl-node, :scope > .shacl-group > shacl-node, :scope > shacl-property, :scope > .shacl-group > shacl-property')) {
|
|
131
|
-
(shape as ShaclNode | ShaclProperty).toRDF(graph, subject)
|
|
132
|
-
}
|
|
133
|
-
if (this.targetClass) {
|
|
134
|
-
graph.addQuad(subject, RDF_PREDICATE_TYPE, this.targetClass, this.config.valuesGraphId)
|
|
135
|
-
}
|
|
136
|
-
// if this is the root shacl node, check if we should add one of the rdf:type or dcterms:conformsTo predicates
|
|
137
|
-
if (this.config.attributes.generateNodeShapeReference && !this.parent) {
|
|
138
|
-
graph.addQuad(subject, DataFactory.namedNode(this.config.attributes.generateNodeShapeReference), this.shaclSubject, this.config.valuesGraphId)
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return subject
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
addPropertyInstance(shaclSubject: Term, config: Config, valueSubject: NamedNode | BlankNode | undefined) {
|
|
145
|
-
let parentElement: HTMLElement = this
|
|
146
|
-
// check if property belongs to a group
|
|
147
|
-
const groupRef = config.store.getQuads(shaclSubject as Term, `${PREFIX_SHACL}group`, null, null)
|
|
148
|
-
if (groupRef.length > 0) {
|
|
149
|
-
const groupSubject = groupRef[0].object.value
|
|
150
|
-
if (config.groups.indexOf(groupSubject) > -1) {
|
|
151
|
-
// check if group element already exists, otherwise create it
|
|
152
|
-
let group = this.querySelector(`:scope > .shacl-group[data-subject='${groupSubject}']`) as HTMLElement
|
|
153
|
-
if (!group) {
|
|
154
|
-
group = createShaclGroup(groupSubject, config)
|
|
155
|
-
this.appendChild(group)
|
|
156
|
-
}
|
|
157
|
-
parentElement = group
|
|
158
|
-
} else {
|
|
159
|
-
console.warn('ignoring unknown group reference', groupRef[0], 'existing groups:', config.groups)
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
const property = new ShaclProperty(shaclSubject as NamedNode | BlankNode, this, config, valueSubject)
|
|
163
|
-
// do not add empty properties (i.e. properties with no instances). This can be the case e.g. in viewer mode when there is no data for the respective property.
|
|
164
|
-
if (property.childElementCount > 0) {
|
|
165
|
-
parentElement.appendChild(property)
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
tryResolve(subject: Term, valueSubject: NamedNode | BlankNode | undefined, config: Config) {
|
|
170
|
-
const list = config.lists[subject.value]
|
|
171
|
-
if (list?.length) {
|
|
172
|
-
let resolved = false
|
|
173
|
-
if (valueSubject) {
|
|
174
|
-
const resolvedPropertySubjects = resolveShaclOrConstraintOnNode(list, valueSubject, config)
|
|
175
|
-
if (resolvedPropertySubjects.length) {
|
|
176
|
-
for (const propertySubject of resolvedPropertySubjects) {
|
|
177
|
-
this.addPropertyInstance(propertySubject, config, valueSubject)
|
|
178
|
-
}
|
|
179
|
-
resolved = true
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (!resolved) {
|
|
183
|
-
this.appendChild(createShaclOrConstraint(list, this, config))
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
console.error('list for sh:or/sh:xone not found:', subject, 'existing lists:', config.lists)
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
window.customElements.define('shacl-node', ShaclNode)
|
package/src/plugin.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { ShaclPropertyTemplate } from './property-template'
|
|
2
|
-
import { Term } from '@rdfjs/types'
|
|
3
|
-
|
|
4
|
-
// store plugins in module scope so that they apply to all shacl-form elements
|
|
5
|
-
const plugins: Record<string, Plugin> = {}
|
|
6
|
-
|
|
7
|
-
export function registerPlugin(plugin: Plugin) {
|
|
8
|
-
if (plugin.predicate === undefined && plugin.datatype === undefined) {
|
|
9
|
-
console.warn('not registering plugin because it does neither define "predicate" nor "datatype"', plugin)
|
|
10
|
-
} else {
|
|
11
|
-
plugins[`${plugin.predicate}^${plugin.datatype}`] = plugin
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function listPlugins(): Plugin[] {
|
|
16
|
-
return Object.entries(plugins).map((value: [_: string, plugin: Plugin]) => { return value[1] })
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function findPlugin(predicate: string | undefined, datatype: string | undefined): Plugin | undefined {
|
|
20
|
-
// first try to find plugin with matching predicate and datatype
|
|
21
|
-
let plugin = plugins[`${predicate}^${datatype}`]
|
|
22
|
-
if (plugin) {
|
|
23
|
-
return plugin
|
|
24
|
-
}
|
|
25
|
-
// now prefer predicate over datatype
|
|
26
|
-
plugin = plugins[`${predicate}^${undefined}`]
|
|
27
|
-
if (plugin) {
|
|
28
|
-
return plugin
|
|
29
|
-
}
|
|
30
|
-
// last, try to find plugin with matching datatype
|
|
31
|
-
return plugins[`${undefined}^${datatype}`]
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export type PluginOptions = {
|
|
35
|
-
predicate?: string
|
|
36
|
-
datatype?: string
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export abstract class Plugin {
|
|
40
|
-
predicate: string | undefined
|
|
41
|
-
datatype: string | undefined
|
|
42
|
-
stylesheet: CSSStyleSheet | undefined
|
|
43
|
-
|
|
44
|
-
constructor(options: PluginOptions, css?: string) {
|
|
45
|
-
this.predicate = options.predicate
|
|
46
|
-
this.datatype = options.datatype
|
|
47
|
-
if (css) {
|
|
48
|
-
this.stylesheet = new CSSStyleSheet()
|
|
49
|
-
this.stylesheet.replaceSync(css)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
abstract createEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement
|
|
54
|
-
|
|
55
|
-
createViewer(template: ShaclPropertyTemplate, value: Term): HTMLElement {
|
|
56
|
-
return template.config.theme.createViewer(template.label, value, template)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export type ClassInstanceProvider = (clazz: string) => Promise<string>
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { Plugin, PluginOptions } from '../plugin'
|
|
2
|
-
import { ShaclPropertyTemplate } from '../property-template'
|
|
3
|
-
|
|
4
|
-
export class FileUploadPlugin extends Plugin {
|
|
5
|
-
onChange: (event: Event) => void
|
|
6
|
-
fileType: string | undefined
|
|
7
|
-
|
|
8
|
-
constructor(options: PluginOptions, onChange: (event: Event) => void, fileType?: string) {
|
|
9
|
-
super(options)
|
|
10
|
-
this.onChange = onChange
|
|
11
|
-
this.fileType = fileType
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
createEditor(template: ShaclPropertyTemplate): HTMLElement {
|
|
15
|
-
const required = template.minCount !== undefined && template.minCount > 0
|
|
16
|
-
const editor = template.config.theme.createFileEditor(template.label, null, required, template)
|
|
17
|
-
editor.addEventListener('change', event => {
|
|
18
|
-
event.stopPropagation()
|
|
19
|
-
this.onChange(event)
|
|
20
|
-
})
|
|
21
|
-
if (this.fileType) {
|
|
22
|
-
editor.querySelector('input[type="file"]')?.setAttribute('accept', this.fileType)
|
|
23
|
-
}
|
|
24
|
-
return editor
|
|
25
|
-
}
|
|
26
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Plugin, PluginOptions } from '../plugin'
|
|
2
|
-
import { Term } from '@rdfjs/types'
|
|
3
|
-
|
|
4
|
-
import { ShaclPropertyTemplate } from '../property-template'
|
|
5
|
-
import { InputListEntry } from '../theme'
|
|
6
|
-
|
|
7
|
-
export class FixedListPlugin extends Plugin {
|
|
8
|
-
entries: InputListEntry[]
|
|
9
|
-
|
|
10
|
-
constructor(options: PluginOptions, entries: InputListEntry[]) {
|
|
11
|
-
super(options)
|
|
12
|
-
this.entries = entries
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
createEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement {
|
|
16
|
-
const required = template.minCount !== undefined && template.minCount > 0
|
|
17
|
-
return template.config.theme.createListEditor(template.label, value || null, required, this.entries, template)
|
|
18
|
-
}
|
|
19
|
-
}
|
package/src/plugins/leaflet.ts
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
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, true)
|
|
140
|
-
instance.appendChild(button)
|
|
141
|
-
return instance
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
createViewer(_: 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
|
-
}
|
package/src/plugins/map-util.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
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
|
-
}
|