Skip to content

Instantly share code, notes, and snippets.

@arbakker
Last active March 8, 2023 16:07
Show Gist options
  • Save arbakker/f715db856ab134f2f292e3ccba339a8b to your computer and use it in GitHub Desktop.
Save arbakker/f715db856ab134f2f292e3ccba339a8b to your computer and use it in GitHub Desktop.
Tampermonkey Script for Syntax Highlight OWS Capabilities Documents #js #tampermonkey #userscript #ogc
// ==UserScript==
// @name Syntax Highlight for PDOK OWS Capabilities Documents
// @description Adds dark theme syntax highlighting to OWS capabilities documents and adds some convenience hyperlinks
// @version 1.1.1
// @updateUrl https://gist.github.com/arbakker/f715db856ab134f2f292e3ccba339a8b/raw/script.user.js
// @supportURL https://gist.github.com/arbakker/f715db856ab134f2f292e3ccba339a8b
// @include /^https?://(service.pdok.nl|geodata.nationaalgeoregister.nl).*/(wms|wfs|wmts|wcs)(/v[0-9]+_[0-9]+)?\?.*(request=GetCapabilities)
// @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/highlight.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/languages/xml.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/languages/css.min.js
// @require https://cdn.jsdelivr.net/npm/xml-beautify@1.2.3/dist/XmlBeautify.js
// @run-at document-start
// ==/UserScript==
function getHTMLHeader (title = "") {
return `
<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
<style>
body {
background-color: #0d1117;
font-family: "Sans Mono", "Consolas", "Courier", monospace;
}
body {
color: #c9d1d9;
}
body a {
color: #79c0ff
}
pre {
white-space: pre-wrap;
}
/*collapsible div */
/* Style the button that is used to open and close the collapsible content */
.collapsible {
background-color: #202a38;
color: #c9d1d9;
cursor: pointer;
padding: 18px;
width: 100%;
border: none;
text-align: left;
outline: none;
font-size: 15px;
font-family: "Sans Mono", "Consolas", "Courier", monospace;
border-bottom: 2px dashed;
}
/* Add a background color to the button if it is clicked on (add the .active class with JS), and when you move the mouse over it (hover) */
.active,
.collapsible:hover {
background-color: #1c2430;
}
/* Style the collapsible content. Note: hidden by default */
.content {
padding: 0 18px;
display: none;
overflow: hidden;
background-color: #0d1117;
}
/* highlight.js css - could not get it to work loading as an external resource */
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
</style>
</head>
`
}
function getHTML (title, serviceType, capUrl, xmlString, svcMdUrl, dsUrls) {
const docTitle = `${title} - ${serviceType.toUpperCase()} Capabilities`
const resourceNames = {
"wms": "Layers",
"wfs": "FeatureTypes",
"wcs": "Coverages",
"wmts": "Layers",
}
let resourceName = resourceNames[serviceType]
let reviewerAnchor = ""
if (["wms", "wfs", "wmts"].includes(serviceType)) {
reviewerAnchor = `<li><a href="https://docs.kadaster.nl/pdok/pdok-reviewer/#/${serviceType}/${encodeURIComponent(capUrl)}" target="_blank" title="Open service in PDOK Reviewer">${docTitle}</a></li>`
} else {
reviewerAnchor = docTitle
}
let svcMdAnchor = ""
if (svcMdUrl !== "") {
let mdId = getMdIdUrl(svcMdUrl)
let mdUrlHtml = `https://nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/${mdId}`
svcMdAnchor = `<li>Service Metadata (NGR)<ul><li><a href="${svcMdUrl}" target="_blank">XML</a></li><li><a href="${mdUrlHtml}" target="_blank">HTML</a></li></ul></li></li>`
}
let dsAnchors = ""
for (let key of Object.keys(dsUrls)) {
let mdId = getMdIdUrl(key)
let mdUrlHtml = `https://nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/${mdId}`
dsAnchors += `<li>Dataset Metadata (NGR) - <em>${dsUrls[key].join(", ")}</em><ul><li><a href="${key}" target="_blank">XML</a></li><li><a href="${mdUrlHtml}" target="_blank">HTML</a></li></ul></li>`
}
let layers = getLayers(serviceType)
let layersLi = ""
for (let layer of layers) {
if (serviceType == "wms") {
let getMapUrl = getGetMapUrl(layer, capUrl)
layersLi += `<li><a href="${getMapUrl}" title="GetMap URL" target="_blank">${layer.title} (${layer.name})</a></li>`
} else if (serviceType == "wfs") {
let getFtUrl = getGetFeatureUrl(layer, capUrl)
layersLi += `<li><a href="${getFtUrl}" title="GetFeature URL" target="_blank">${layer.title} (${layer.name})</a></li>`
} else {
layersLi += `<li>${layer.title} (${layer.name})</li>`
}
}
layersLi = `<ul>${layersLi}</ul>`
const htmlHeader = getHTMLHeader(docTitle)
return `
${htmlHeader}
<body>
<h1>${reviewerAnchor}</h1>
<p>
<div>
<button type="button" class="collapsible" title="Show metadata links">Metadata</button>
<div class="content">
<ul>
${svcMdAnchor}
${dsAnchors}
</ul>
</div>
<button type="button" class="collapsible" title="Show GetFeature links">${resourceName}</button>
<div class="content">
${layersLi}
</div>
</div>
</p>
<pre class="line-numbers">
<code class="language-xml">
${xmlString}
</code>
</pre>
</body>
`
}
function getBasicHTML (xmlString) {
const htmlHeader = getHTMLHeader()
return `
${htmlHeader}
<body>
<pre class="line-numbers">
<code class="language-xml">
${xmlString}
</code>
</pre>
</body>
`
}
function getGetMapUrl (layer, capUrl) {
const WIDTH = 800
const bboxString = `${layer.bbox[0]},${layer.bbox[1]},${layer.bbox[2]},${layer.bbox[3]}`
const ratio = (layer.bbox[3] - layer.bbox[1]) / (layer.bbox[2] - layer.bbox[0])
const height = parseInt(WIDTH * ratio)
let baseUrl = capUrl.split('?')[0]
const constQueryParam = "SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&FORMAT=image%2Fpng&TRANSPARENT=true&STYLES="
const varQueryParam = `LAYERS=${layer.name}&CRS=${layer.crs}&WIDTH=${WIDTH}&HEIGHT=${height}&BBOX=${bboxString}`
return `${baseUrl}?${constQueryParam}&${varQueryParam}`
}
function getGetFeatureUrl (ft, capUrl) {
let baseUrl = capUrl.split('?')[0]
return `${baseUrl}?request=GetFeature&service=WFS&version=2.0.0&startIndex=0&count=1&typename=${ft.name}`
}
function getMdIdUrl (url) {
const urlObj = new URL(url)
const searchParams = new URLSearchParams(urlObj.search.toLowerCase());
if (searchParams.has("uuid")) {
return searchParams.get("uuid")
} else if (searchParams.has("id")) {
return searchParams.get("id")
}
return ""
}
function getTitle (serviceType) {
let result = ""
if (serviceType == "wms") {
const titleEL = document.querySelector("Service Title")
result = titleEL.innerHTML
} else if (["wmts", "wfs", "wcs"].includes(serviceType)) {
const titleEL = document.querySelector("ServiceIdentification Title")
result = titleEL.innerHTML
}
if (result === "") {
result = "[service title missing]"
}
return result
}
function getServiceMdUrl () {
const mdUrlEl = document.querySelector("ExtendedCapabilities URL")
if (!mdUrlEl) return ""
let result = mdUrlEl.innerHTML
result = result.replaceAll("&amp;", "&")
return result
}
function getDatasetMdUrl (serviceType) {
const result = {}
if (serviceType == "wms") {
const layers = document.querySelectorAll("Layer Layer")
Array.from(layers).forEach(layer => {
const orEl = layer.querySelector("MetadataURL OnlineResource")
if (orEl) {
const nameEl = layer.querySelector("Title")
const name = nameEl.innerHTML
const mdUrl = orEl.getAttribute("xlink:href")
if (Object.keys(result).includes(mdUrl)) {
result[mdUrl].push(name)
} else {
result[mdUrl] = [name]
}
}
})
} else if (serviceType == "wfs") {
const layers = document.querySelectorAll("FeatureTypeList FeatureType")
Array.from(layers).forEach(layer => {
const nameEl = layer.querySelector("Title")
const name = nameEl.innerHTML
const mdUrlEl = layer.querySelector("MetadataURL")
const mdUrl = mdUrlEl.getAttribute("xlink:href")
if (Object.keys(result).includes(mdUrl)) {
result[mdUrl].push(name)
} else {
result[mdUrl] = [name]
}
})
}
return result
}
function getLayers (serviceType) {
let result = []
if (serviceType == "wms") {
const layers = document.querySelectorAll("Layer Layer")
Array.from(layers).forEach(layer => {
const title = layer.querySelector("Title").innerHTML
const name = layer.querySelector("Name").innerHTML
const crs = layer.querySelector("CRS").innerHTML
const bbox = layer.querySelector(`BoundingBox[CRS='${crs}']`)
result.push({ "name": name, "title": title, "crs": crs, "bbox": [bbox.getAttribute("minx"), bbox.getAttribute("miny"), bbox.getAttribute("maxx"), bbox.getAttribute("maxy")] })
})
} else if (serviceType == "wfs") {
const fts = document.querySelectorAll("FeatureTypeList FeatureType")
Array.from(fts).forEach(ft => {
const title = ft.querySelector("Title").innerHTML
const name = ft.querySelector("Name").innerHTML
result.push({ "name": name, "title": title })
})
}
return result
}
function addCollapsibleEventHandlers () {
var coll = document.getElementsByClassName("collapsible");
var i;
for (i = 0; i < coll.length; i++) {
coll[i].addEventListener("click", function () {
this.classList.toggle("active");
var content = this.nextElementSibling;
if (content.style.display === "block") {
content.style.display = "none";
} else {
content.style.display = "block";
}
});
}
}
(function () {
const searchParams = new URLSearchParams(window.location.search.toLowerCase());
let xmlString = new XMLSerializer().serializeToString(document.documentElement);
xmlString = new XmlBeautify().beautify(xmlString,
{
indent: " ",
useSelfClosingElement: true
});
xmlString = xmlString.replace(/(^[ \t]*\n)/gm, "")
xmlString = xmlString.replaceAll("<", "&lt;")
xmlString = xmlString.replaceAll(">", "&gt;")
let newDoc
if (!searchParams.has("service") || document.querySelector("ExceptionReport")) {
// todo: show error message in case of missing service param
newDoc = new DOMParser().parseFromString(getBasicHTML(xmlString), "text/html");
} else {
const serviceType = searchParams.get("service")
const xml = document.getRootNode()
const title = getTitle(serviceType)
const svcMdUrl = getServiceMdUrl()
const dsUrls = getDatasetMdUrl(serviceType)
newDoc = new DOMParser().parseFromString(getHTML(title, serviceType, window.location.href, xmlString, svcMdUrl, dsUrls), "text/html");
}
document.replaceChild(newDoc.documentElement, document.documentElement)
hljs.highlightAll()
addCollapsibleEventHandlers()
})(); // anonymous function that executes directly
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment