1const hpccWasm = window['@hpcc-js/wasm']; 2const dagConfig = { 3 lang: document.documentElement.lang || window?.taxonyms?.lang || 'de', 4 siteUrl: window?.taxonyms?.siteUrl || 'https://www.infoflora.ch', 5 basePurl: window?.taxonyms?.basePurl || 'https://purl.infoflora.ch', 6 obsUrl: window?.taxonyms?.obsUrl || 'https://obs.infoflora.ch', 7 taxonymPath: window?.taxonyms?.taxonymPath || '/rest/public/v5/taxonomy/taxa/' 8}; 9const captionMode = { 10 withCaption: 'with-caption', 11 withoutCaption: 'without-caption', 12 captionOnly: 'caption-only' 13} 14 15async function createDagForTaxon(parentElt, taxonId, params) { 16 const { 17 upperRank = 'genus', 18 highlightedReferences = [], 19 scale = 1, 20 w = '100%', 21 h = '100%', 22 padding = 0.25, 23 zoomable = true, 24 centerable = true, 25 interactive = true, 26 highlightNodeColor = 'yellow', 27 highlightEdgeColor = 'red' 28 } = params || {}; 29 const opts = { 30 method: 'GET', 31 headers: {} 32 }; 33 response = await fetch(getDotUrl(taxonId, captionMode.withoutCaption, upperRank, highlightedReferences), opts); 34 data = await response.text(); 35 36 const dot = convertPurlToUrl(data); 37 const ww = Number.isInteger(w) ? w : (parseInt(w) / 100) * parentElt.clientWidth; 38 const wh = Number.isInteger(h) ? h : (parseInt(h) / 100) * parentElt.clientHeight; 39 40 return await buildDag(dot, { 41 scale, w, ww, h, wh, padding, zoomable, centerable, interactive, highlightNodeColor, highlightEdgeColor 42 }); 43} 44 45async function createDagCaptionForTaxon(parentElt, taxonId, params) { 46 const { 47 upperRank = 'genus', 48 highlightedReferences = [], 49 scale = 1, 50 w = '100%', 51 h = '100%', 52 zoomable = false 53 } = params || {}; 54 const opts = { 55 method: 'GET', 56 headers: {} 57 }; 58 response = await fetch(getDotUrl(taxonId, captionMode.captionOnly, upperRank, highlightedReferences), opts); 59 data = await response.text(); 60 61 const ww = Number.isInteger(w) ? w : (parseInt(w) / 100) * parentElt.clientWidth; 62 const wh = Number.isInteger(h) ? h : (parseInt(h) / 100) * parentElt.clientHeight; 63 const padding = 0; 64 const centerable = false; 65 const interactive = false; 66 67 return await buildDag(data, { 68 scale, w, ww, h, wh, padding, zoomable, centerable, interactive 69 }); 70} 71 72function getDotUrl(taxonId, caption, upperRank, highlightedReferences) { 73 const references = highlightedReferences.join(','); 74 75 return dagConfig.obsUrl 76 + dagConfig.taxonymPath + taxonId 77 + '/relations.dot?highlighted-references='+ references 78 + '&upper-rank=' + upperRank 79 + '&caption=' + caption; 80} 81 82function convertPurlToUrl(str) { 83 let path; 84 const purl = dagConfig.basePurl+'/taxonomy/taxonyms/'; 85 86 switch (dagConfig.lang) { 87 case 'fr': 88 path = '/fr/flore/'; 89 break; 90 default: 91 path = '/' + dagConfig.lang + '/flora/'; 92 } 93 94 return str.replaceAll(purl, dagConfig.siteUrl + path); 95} 96 97buildDag = async (dotStr, params) => { 98 { 99 const { 100 scale, w, ww, h, wh, padding, zoomable, centerable, interactive, highlightNodeColor, highlightEdgeColor 101 } = params || {}; 102 const svgStr = await dot2Svg(dotStr); 103 const viz = buildSvg({ svgStr, scale, ww, wh, padding }); 104 105 const wrap = d3.create('svg'); 106 wrap.attr('class', 'dag-wrapper'); 107 wrap.attr('width', viz.wrapper.width); 108 wrap.attr('height', viz.wrapper.height); 109 wrap.attr('style', 'background-color: white;'); 110 wrap.html(viz.html); 111 112 const zoomed = e => d3.select(`#${viz.id}`).attr('transform', e.transform); 113 const zoom = d3 114 .zoom() 115 .scaleExtent([scale * 0.05, 5]) 116 .on('zoom', zoomed); 117 118 const hover = () => { 119 const nodes = wrap.selectAll('.node'); 120 const edges = wrap.selectAll('.edge'); 121 const shapes = 'ellipse,polygon,rect'; 122 const lines = 'path'; 123 let color = {}; 124 const clicked = {}; 125 126 const select = that => { 127 const s = d3.select(that); 128 const id = s.attr('id'); 129 return { s, id }; 130 }; 131 132 nodes 133 .on('mouseover', function(e) { 134 const { s, id } = select(this); 135 if (!clicked[id]) color[id] = s.selectAll(shapes).attr('fill'); 136 s.selectAll(shapes).attr('fill', highlightNodeColor); 137 }) 138 .on('mouseout', function(e) { 139 const { s, id } = select(this); 140 if (!clicked[id]) s.selectAll(shapes).attr('fill', color[id]); 141 }); 142 143 edges 144 .on("mouseover", function(e) { 145 const { s, id } = select(this); 146 if (!clicked[id]) color[id] = s.selectAll(lines).attr('stroke'); 147 s.selectAll(lines) 148 .attr('stroke', highlightEdgeColor) 149 .attr("stroke-width", 2); 150 }) 151 .on("mouseout", function(e) { 152 const { s, id } = select(this); 153 if (!clicked[id]) 154 s.selectAll(lines) 155 .attr("stroke-width", 1) 156 .attr('stroke', color[id]); 157 }); 158 159 /* 160 nodes.on("click", function(e) { 161 const { s, id } = select(this); 162 if (clicked[id]) { 163 s.selectAll(shapes).attr('fill', color[id]); 164 clicked[id] = false;
165 } else { 166 s.selectAll(shapes).attr('fill', highlightNodeColor); 167 clicked[id] = true; 168 } 169 }); 170 171 edges.on("click", function(e) { 172 const { s, id } = select(this); 173 if (clicked[id]) { 174 s.selectAll(lines) 175 .attr("stroke-width", 1) 176 .attr('stroke', color[id]); 177 clicked[id] = false; 178 } else { 179 s.selectAll(lines) 180 .attr("stroke-width", 2) 181 .attr('stroke', highlightEdgeColor); 182 clicked[id] = true; 183 } 184 }); 185 */ 186 }; 187 188 const centerOnSelected = () => { 189 const wrapW = parseFloat(wrap.attr('width')); 190 const wrapH = parseFloat(wrap.attr('height')); 191 const targetScale = findScaleToCenter(scale, wrapW, wrapH, viz.graph.width, viz.graph.height); 192 wrap.call(zoom.scaleTo, targetScale); 193 194 const [x, y] = findSelectedTaxonym(wrap); 195 wrap.call(zoom.translateTo, x, y); 196 }; 197 198 const fitExtent = () => { 199 const wrapW = parseFloat(wrap.attr('width')); 200 const wrapH = parseFloat(wrap.attr('height')); 201 202 const x = viz.graph.width / 2; 203 const y = -1 * viz.graph.height / 2; 204 const targetScale = findScaleToFit(wrapW, wrapH, viz.graph.width, viz.graph.height); 205 206 wrap.call(zoom.translateTo, x, y); 207 wrap.call(zoom.scaleTo, targetScale); 208 }; 209 210 const zoomIn = () => { 211 wrap.transition().call(zoom.scaleBy, 2); 212 }; 213 214 const zoomOut = () => { 215 wrap.transition().call(zoom.scaleBy, 0.5); 216 }; 217 218 const viewTaxonym = () => { 219 defaultZoom = centerOnSelected; 220 defaultZoom(); 221 }; 222 223 const viewAll = () => { 224 defaultZoom = fitExtent; 225 defaultZoom(); 226 } 227 228 const toggleFullscreen = () => { 229 if (isInFullscreen()) { 230 document.exitFullscreen(); 231 } else { 232 div.requestFullscreen(); 233 } 234 }; 235 236 const isInFullscreen = () => document.fullscreenElement && document.fullscreenElement === div; 237 238 const listenToFullscreen = () => { 239 div.onfullscreenchange = () => { 240 if (document.fullscreenElement) { 241 div.classList.add('fullscreen'); 242 } else { 243 div.classList.remove('fullscreen'); 244 } 245 } 246 }; 247 248 const listenToResize = () => { 249 const relW = (Number.isInteger(w)) ? false : parseInt(w) / 100; 250 const relH = (Number.isInteger(h)) ? false : parseInt(h) / 100; 251 252 d3.select(window).on(`resize.${viz.id}`, () => { 253 let targetWidth, targetHeight; 254 const { width, height } = div.getBoundingClientRect(); 255 const fullScreen = isInFullscreen(); 256 if (fullScreen) { 257 targetWidth = width; 258 targetHeight = height; 259 } else { 260 targetWidth = relW ? width * relW : Math.min(ww, width); 261 targetHeight = relH ? height * relH : Math.min(wh, height); 262 } 263 264 wrap.attr('width', targetWidth); 265 if (fullScreen || wh !== 0) { 266 wrap.attr('height', targetHeight); 267 } 268 269 defaultZoom(); 270 }); 271 }; 272 273 const buildDom = (zoomable, centerable) => { 274 const dag = d3.create('div').attr('class', 'dag'); 275 276 if (zoomable) { 277 const dagZoom = dag.append('div').attr('class', 'dag-zoom'); 278 dagZoom.append('button') 279 .attr('class', 'btn zoom-in') 280 .html('+') 281 .on('click', zoomIn); 282 dagZoom.append('button') 283 .attr('class', 'btn zoom-out') 284 .html('-') 285 .on('click', zoomOut); 286 287 dag.append('button') 288 .attr('class', 'btn full-screen') 289 .html('⤢') 290 .on('click', toggleFullscreen); 291 } 292 293 if (centerable) { 294 const dagResize = dag.append('div').attr('class', 'dag-resize'); 295 dagResize.append('button') 296 .attr('class', 'btn zoom-reset') 297 .html(lg['flora.fl.view_taxonym']) 298 .on('click', viewTaxonym); 299 dagResize.append('button') 300 .attr('class', 'btn zoom-extent') 301 .html(lg['flora.fl.view_all']) 302 .on('click', viewAll); 303 } 304 305 dag.node().appendChild(wrap.node()); 306 307 return dag.node(); 308 }; 309 310 const div = buildDom(zoomable, centerable); 311 zoomable && wrap.call(zoom); 312 let defaultZoom = centerOnSelected; 313 defaultZoom(); 314 listenToResize(); 315 listenToFullscreen(); 316 interactive && hover(); 317 318 return div; 319 } 320} 321 322dot2Svg = async s => { 323 const graphviz = await hpccWasm.Graphviz.load(); 324 325 return graphviz.layout(s, 'svg', 'dot'); 326} 327 328buildSvg = ({ svgStr, scale, ww, wh, padding }) => { 329 const document = new DOMParser().parseFromString(svgStr, 'image/svg+xml'); 330 const elt = document.documentElement; 331 const svg = d3.select(elt).remove(); 332 333 const gw = parseInt(elt.getAttribute('width')); 334 const gh = parseInt(elt.getAttribute('height')); 335 336 const buffer = 1 + padding; 337 wh = wh === 0 ? buffer * gh * scale : wh; 338 339 const g = svg.select('#graph0'); 340 const transformSrc = g.attr('transform'); 341 342 const s = findScaleToCenter(scale, ww, wh, gw, gh); 343 const [x, y] = findSelectedTaxonym(svg); 344 const t = { 345 x: ww / 2 - x * s, 346 y: wh / 2 - y * s 347 }; 348 349 g.attr('transform', `translate(${t.x},${t.y}) scale(${s})`); 350 351 const id = `graph-${uuid()}`; 352 g.attr('id', id); 353 354 return { 355 graph: { width: gw, height: gh }, 356 wrapper: { width: ww, height: wh }, 357 transform: { src: transformSrc, zoom: { scale: s, translate: t } }, 358 id, 359 html: svg.html() 360 }; 361} 362 363function findScaleToFit(wrapWidth, wrapHeight, graphWidth, graphHeight) { 364 return Math.min(wrapWidth / graphWidth, wrapHeight / graphHeight); 365} 366 367function findScaleToCenter(initialScale, wrapWidth, wrapHeight, graphWidth, graphHeight) { 368 const scaleToFit = findScaleToFit(wrapWidth, wrapHeight, graphWidth, graphHeight); 369 370 return Math.max(initialScale, scaleToFit); 371} 372 373function findSelectedTaxonym(svg) { 374 let polygon = svg.select('g polygon[fill="#e4e4e4"]'); 375 if (polygon.empty()) { 376 polygon = svg.select('g polygon'); 377 } 378 const points = bbox(polygon); 379 const x = (points.xMin + points.xMax) / 2; 380 const y = (points.yMin + points.yMax) / 2; 381 382 return [x, y]; 383} 384 385bbox = polygon => { 386 let x, y, xList = [], yList = []; 387 388 for (point of polygon.attr('points').split(' ')) { 389 [x, y] = point.split(','); 390 xList.push(x); 391 yList.push(y); 392 } 393 394 return { 395 xMin: Math.min(...xList), 396 xMax: Math.max(...xList), 397 yMin: Math.min(...yList), 398 yMax: Math.max(...yList) 399 } 400}; 401 402uuid = () => String(Math.floor(Math.random() * 1e9));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.