Last active
March 8, 2023 16:07
-
-
Save arbakker/f715db856ab134f2f292e3ccba339a8b to your computer and use it in GitHub Desktop.
Tampermonkey Script for Syntax Highlight OWS Capabilities Documents #js #tampermonkey #userscript #ogc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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/[email protected]/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("&", "&") | |
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("<", "<") | |
xmlString = xmlString.replaceAll(">", ">") | |
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