Compare commits

...

7 commits

Author SHA1 Message Date
Ruben van de Ven
6eb67407d9 New light version as default. 2021-05-04 18:40:06 +02:00
Ruben van de Ven
40b2251dbd Simple demo: scroll text moves focus 2021-04-29 15:36:52 +02:00
Ruben van de Ven
07503883d3 Show tooltip and select moves into view 2021-04-29 15:36:27 +02:00
Ruben van de Ven
4dec76a325 Fancier markers and legend 2021-04-28 11:18:02 +02:00
Ruben van de Ven
7751fc05a8 Dynamic font size for node titles 2021-04-28 10:24:07 +02:00
Ruben van de Ven
2dda26ff77 Title placement 2021-04-28 09:38:06 +02:00
Ruben van de Ven
9a79cea9d7 Remove overlaps on update() 2021-04-27 16:10:23 +02:00
5 changed files with 1439 additions and 157 deletions

View file

@ -4,6 +4,7 @@ This repository contains a script to pull semantic data out of Semantic Mediawik
Built for [Security Vision](https://securityvision.io).
## Installation
```bash
@ -41,4 +42,72 @@ _Ask_ SMW with the following query:
?Software Deployed.Developped by (institutions)=Software Developer
?Datasets Used
?Datasets Used.Developed by Institution=Dataset Developer
```
----
```
({{#ask: [[Category:Deployments||Institution]]
OR [[Category:Technologies]] [[Developed by (institutions)::+]] // TODO: + should give a subquery <q></q> with all institutions in EU
OR [[Category:Technologies]] [[-Software Deployed::+]] // TODO: + should give a subquery <q></q> with all deployments in EU
|?Category
|?Geolocation
|?City
|?City.Has Coordinates=Has Coordinates
|?City.Is in Country=City Country
|?City Country.Has Coordinates=Country Coordinates
|?Clients
|?Managed by
|?Used by
|?Funded by
|?Provided by // TODO: technology/product provided by
|?Software Deployed
|?Developped by (institutions)
|format=broadtable
|limit=500
|offset=0
|link=all
|sort=
|order=asc
|headers=show
|searchlabel=... further results
|class=sortable wikitable smwtable
}}
{{#ask: [[Category:Deployments||Institution]] [[Not in graph::!Greens Report 2021]]
OR [[Category:Technologies]] [[Developed by (institutions)::+]]
OR [[Category:Technologies]] [[-Software Deployed::+]]
|?Category
|?Geolocation
|?City
|?City.Has Coordinates=City Coordinates
|?City.Is in Country=City Country
|?City Country.Has Coordinates=Country Coordinates
|?Clients
|?Managed by
|?Used by
|?Funded by
|?Provided by
|?Software Deployed
|?Software Deployed.Developped by (institutions)=Software Developer
|?Datasets Used
|?Datasets Used.Developed by Institution=Dataset Developer
|format=broadtable
|limit=50
|offset=0
|link=all
|sort=
|order=asc
|headers=show
|searchlabel=... further results
|class=sortable wikitable smwtable
}}
```
Fetch using CURL:
```bash
curl 'https://www.securityvision.io/wiki/index.php?title=Special:Ask&#search' -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'Content-Type: application/x-www-form-urlencoded' -H 'Origin: https://www.securityvision.io' -H 'Connection: keep-alive' -H 'Referer: https://www.securityvision.io/wiki/index.php/Special:Ask' -H 'Upgrade-Insecure-Requests: 1' --data-raw 'title=Special%3AAsk&_action=submit&q=%5B%5BCategory%3ADeployments%7C%7CInstitution%5D%5D++&po=%0D%0A+%7C%3FCategory%0D%0A+%7C%3FGeolocation%0D%0A+%7C%3FCity%0D%0A+%7C%3FCity.Has+Coordinates%3DCity+Coordinates%0D%0A+%7C%3FCity.Is+in+Country%3DCity+Country%0D%0A+%7C%3FCity+Country.Has+Coordinates%3DCountry+Coordinates%0D%0A+%7C%3FClients%0D%0A+%7C%3FManaged+by%0D%0A+%7C%3FUsed+by%0D%0A+%7C%3FFunded+by%0D%0A+%7C%3FProvided+by%0D%0A+%7C%3FSoftware+Deployed%0D%0A+%7C%3FSoftware+Deployed.Developped+by+%28institutions%29%3DSoftware+Developer%0D%0A+%7C%3FDatasets+Used%0D%0A+%7C%3FDatasets+Used.Developed+by+Institution%3DDataset+Developer&eq=yes&p%5Bformat%5D=json&p%5Blimit%5D=500&p%5Boffset%5D=0&p%5Blink%5D=all&p%5Bheaders%5D=show&p%5Bmainlabel%5D=&p%5Bintro%5D=&p%5Boutro%5D=&p%5Bsearchlabel%5D=JSON&p%5Bdefault%5D=&p%5Btype%5D=full&p%5Bfilename%5D=result2.json&sort_num%5B%5D=&order_num%5B%5D=asc&sort_num%5B%5D=&order_num%5B%5D=asc&eq=yes'
```

View file

@ -1,6 +1,12 @@
@font-face {
font-family: 'Lexend Mega Regular';
font-style: normal;
font-weight: normal;
src: local('Lexend Mega Regular'), url('LexendMega-Regular.woff') format('woff');
}
:root{
--color1: #f94144;
:root {
--color1: #9741f9;
--color2: #f3722c;
--color3: #f8961e;
/* --color4: #f9844a; */
@ -10,18 +16,25 @@
--color8: #4d908e;
--color9: #577590;
--color10: #277da1;
--hover-color: var(--color1);
--hover-related-color: #d1bce9;
/* --hover-color: var(darkblue); */
--selected-color: var(--color1);
--selected-color: var(--color1);
--link-color: rgba(255,255,255,0.5);
--link-hover-color: var(--hover-color);
--link-hover-related-color: var(--hover-related-color);
--link-focus-color: var(--hover-color);
--body-back: #96a7b7; /*#9cb3c9; /*#8195a7; /*#9cb3c9; #b9cada*/
--title-color: #1c1c1c;
}
body {
margin: 0;
/* overflow: hidden; */
/* background: linear-gradient(to top, #040308, #AD4A28, #DD723C, #fc7001, #dcb697, #9ba5ae, #3e5879, #020b1a); */
background:linear-gradient(to top, #414141, #99a6b8);
background: var(--body-back);
/* background: #9cb3c9; */
font-family: sans-serif;
min-height: 100vh;
}
@ -35,111 +48,214 @@ svg.dragging {
cursor: grabbing;
}
svg .links line,svg .links path{
stroke: #f3722c;
#arrowHead {
fill: var(--link-color);
}
#arrowHeadSelected {
fill: var(--link-focus-color);;
}
#arrowHeadSelectedRelated {
fill: var(--link-hover-related-color);;
}
svg .links line, svg .links path {
/* stroke: #f3722c; */
/* stroke: #9df32c; */
stroke: var(--link-color);
stroke-width: 6;
fill:none;
fill: none;
transition: stroke-width 1s;
cursor: pointer;
}
svg .links line.hover, svg .links path.hover{
stroke:red;
svg .links line.hover, svg .links path.hover {
stroke: var(--link-hover-color);
/* stroke-width: 12; */
marker-end: url(#arrowHeadSelected);
}
svg.zoomed .links line, svg.zoomed .links path{
svg .links .linkedHover path{
stroke: var(--link-hover-related-color);
stroke-width: 12;
marker-end: url(#arrowHeadSelectedRelated);
}
svg .links .linkedSelected path{
stroke: var(--link-focus-color);
stroke-width: 12;
marker-end: url(#arrowHeadSelected);
}
svg.zoomed .links line, svg.zoomed .links path {
stroke-width: 2;
}
.links text{
display:none;
font-size:5pt;
/* svg.zoomed .links line, svg.zoomed .links path.hover {
stroke-width: 4;
} */
svg .title {
font-size: 200;
}
svg .subtitle {
font-size: 100;
}
svg #countries .country {
fill: #f7f5f5;
fill: #f0eeee;
/* stroke:rgb(230, 230, 230); */
stroke:rgb(230, 230, 230);
stroke-width: 5;
}
svg #countries .country.eu_country {
/* fill:#eae5e3; */
fill:#e9e1e4;
/* fill:rgb(234, 229, 235); */
stroke:rgb(217, 209, 218);
}
svg #header #titlePath, svg #header #title2Path, svg #header #subtitlePath {
stroke: none;
fill: none;
}
svg #header text {
font-size: 180px;
font-family: "Lexend Mega Regular";
/*Comfortaa*/
opacity: .8;
fill: var(--title-color);
text-shadow: rgba(0, 0, 0, .5) 5px 5px 10px;
/* text-transform: uppercase; */
}
svg #header text:nth-of-type(2) {
/* dominant-baseline: hanging; */
/* transform: translate(10px, 25px); */
}
svg #header text#subtitle {
font-size: 70px;
fill: var(--body-back);
}
.links text {
display: none;
font-size: 5pt;
text-anchor: middle;
fill: whitesmoke;
}
.node{
.node {
cursor: pointer;
}
.node text.nodeTitle{
.node text.nodeTitle {
text-anchor: start;
dominant-baseline: hanging; /*achieves a 'text-anchor: top'*/
font-size:16pt;
dominant-baseline: hanging;
/*achieves a 'text-anchor: top'*/
/* font-size: 16pt; */
/*Set this in JS*/
transition: font-size .4s, opacity 1s;
fill: white;
fill: #5d5d5f; /*also when hovering node*/
opacity: 1;
pointer-events: none;
/*prevent mouse glitches*/
}
.node:not(:hover):not(.linkHover) text.nodeTitle.overlapping{
/* .node:not(:hover):not(.linkHover) text.nodeTitle.overlapping { */
.node text.nodeTitle.overlapping {
/* used to be shown on hover, but disabled now that we have a tooltip */
opacity: 0;
}
svg.zoomed .node text.nodeTitle{
font-size:6pt;
}
svg.zoomed.zoomed2 .node text.nodeTitle{
font-size:3pt;
svg.zoomed .node text.nodeTitle {
/* font-size: 6pt; */
}
.node circle{
fill: white;
svg.zoomed.zoomed2 .node text.nodeTitle {
font-size: 3pt;
}
.node circle, .node path {
fill: lightgray;
}
/* Whenever a connected link is hovered */
.node.linkHover circle{
stroke: var(--hover-color);
.node.linkHover circle, .node.linkHover path, .node.linkedHover path, label:hover .node path {
fill: var(--hover-related-color) !important;
stroke: var(--hover-related-color);
stroke-width: 5px;
}
.node.linkHover text.nodeTitle.overlapping{
.node.linkedSelected path {
fill: var(--hover-related-color) !important;
/* same as linkHover/linkedHover but without border */
}
.node.linkHover text.nodeTitle.overlapping {
transition: opacity 0s;
}
.node:hover circle{
.node:hover circle, .node:hover path, .node.selected path {
fill: var(--hover-color) !important;
stroke: var(--hover-color);
stroke-width: 5px;
}
.node:hover text{
/*
.node:hover text {
transition: none;
fill: var(--hover-color);
}
.node.selected circle{
fill: var(--hover-color);
} */
.node.selected circle, .node.selected path {
fill: var(--selected-color) !important;
stroke: var(--selected-color);
stroke-width: 5px;
}
.node.City circle{
display:none;
.node.City circle, .node.City path {
display: none;
}
.node.City{
fill:white;
.node.City {
fill: white;
stroke: black;
stroke-width: .5px;
font-size: 20px;
}
.node.Person circle {
.node.Person circle, .node.Person path {
fill: lightgreen
}
.node.Technology circle {
fill: lightcoral;
.node.Technology circle, .node.Technology path {
fill: plum;
}
.node.Deployment circle {
fill: lightblue;
.node.Deployments circle, .node.Deployments path {
/* fill: lightblue; */
/* fill: rgb(75, 97, 190); */
fill: rgb(8, 8, 8);
}
.node.Institution circle {
.node.Institution circle, .node.Institution path {
/* fill: lightcoral; */
fill: red;
}
.node.Dataset circle, .node.Dataset path {
fill: lightgoldenrodyellow
}
.node.Dataset circle {
fill: plum
}
.labels .label text{
fill:yellow;
.labels .label text {
fill: yellow;
opacity: 1 !important;
}
}
/* .node.Person circle {
fill: var(--color2)
@ -157,69 +273,164 @@ svg.zoomed.zoomed2 .node text.nodeTitle{
fill: var(--color7)
} */
#nodeInfo{
#nodeInfo {
position: fixed;
display:block;
right:20px;
bottom:20px;
background:white;
display: block;
right: 20px;
bottom: 20px;
background: white;
padding: 10px;
border: solid 1px #ccc;
}
#nodeInfo.hidden{
display:none;
#nodeInfo.hidden {
display: none;
}
#nodeInfo h2{
#nodeInfo h2 {
margin: 0;
padding: 0;
}
#nodeInfo iframe{
#nodeInfo iframe {
width: 50vw;
height: calc(100vh - 40px - 20px - 30px);
}
#closeInfo{
#tooltip{
position:absolute;
z-index: 100;
opacity: 1;
transition: opacity .3s;
background:white;
padding: 20px 10px;
border-radius: 5px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, .5);
}
#tooltip:not(.visible){
position:absolute;
z-index: 100;
opacity:0;
pointer-events: none;
}
#tooltip h3{
margin: 5px 0;
text-align: center;;
}
#tooltip .category{
display: block;
color: black;
text-align: center;;
}
#tooltip .category::before{
content:'· '
}
#tooltip .category::after{
content:' ·'
}
#tooltip .clickForMore{
display: block;
color: gray;
text-align: center;;
}
#closeInfo {
cursor: pointer;
position: absolute;
right: 10px;
top: 10px;
}
#closeInfo:hover{
#closeInfo:hover {
color: var(--hover-color);
}
a, a:link{
a, a:link {
text-decoration: none;
}
a:hover{
a:hover {
text-decoration: underline;
}
#filters,#menu{
header {
position: fixed;
left: 0;
top: 0;
bottom: 0;
right: 0;
background: white;
padding: 10px;
border-top-left-radius: 5px;
}
h1 {
/* color:var(--color9); */
margin: 0;
text-transform: uppercase;
display: none;
}
p.subtitle {
margin: 0;
color: var(--color10);
/* text-transform: uppercase;; */
display: none;
}
#filters h3 {
text-align: center;
;
}
#filters label {
cursor: pointer;
display: block;
padding: 10px;
}
#map .borders{
#filters label svg {
display: inline;
width: 30px;
height: 30px;
vertical-align: middle;
}
#filters span:hover {
color: var(--hover-color);
}
#filters input {
display: none;
}
#filters input+span {
display: inline-block;
padding-left: 10px;
/* background: var(--color9); */
text-decoration: line-through;
}
#filters input:checked+span {
/* color: var(--selected-color); */
text-decoration: none;
/* box-shadow: inset 0 0 5px black; */
}
#map .borders {
stroke-width: 6px;
stroke: white;
fill:none;
stroke: rgb(221, 210, 210);
fill: none;
}
#alluvial{
#alluvial {
/* position:fixed; */
top:0;
left:0;
top: 0;
left: 0;
}
#alluvial .flow_label text{
#alluvial .flow_label text {
font-size: 30;
}
}

View file

@ -1,8 +1,12 @@
const CONFIG = {
'nodeSize': 8,
'title': "Remote Biometric Identification",
'subtitle': "A survey of the European Union",
// 'nodeSize': 8,
'nodeRadius': 5,
'nodeRepositionPadding': 10,
'baseUrl': 'https://www.securityvision.io/wiki/index.php/',
'dataUrl': 'result.json',
'dataUrl': 'result2.json',
'preSimulate': false, // run simulation before starting, so we don't start with lines jumping around
'labels': {
'rotate': true,
@ -47,13 +51,66 @@ const CONFIG = {
"Company": ["Managed by", "Provided by", "Developped by (institutions)"],
"Tech": ["Technologies Used", "Software Deployed"],
"Funding": ["Funded by"],
}
},
"zoom": {
"scale_min": .2,
"scale_max": 20,
},
"cases": [
"Data-lab Burglary-Free Neighbourhood",
"Dragonfly Project",
]
};
// let width = window.innerWidth;
// let height = window.innerHeight;
function getSymbolForCategories(classes) {
if (!Array.isArray(classes)) {
classes = [classes];
}
if (classes.includes('Institution')) {
return d3.symbol()
.type(d3.symbolTriangle)
.size(CONFIG.nodeRadius * 16);
}
return d3.symbol()
.type(d3.symbolCircle)
.size(CONFIG.nodeRadius * 16);
}
// returns a symbol function
function getSymbolForNode(n) {
const classes = getCategories(n);
return getSymbolForCategories(classes);
}
// Slugify a string, by https://lucidar.me/en/web-dev/how-to-slugify-a-string-in-javascript/
function slugify(str) {
str = str.replace(/^\s+|\s+$/g, '');
// Make the string lowercase
str = str.toLowerCase();
// Remove accents, swap ñ for n, etc
var from = "ÁÄÂÀÃÅČÇĆĎÉĚËÈÊẼĔȆÍÌÎÏŇÑÓÖÒÔÕØŘŔŠŤÚŮÜÙÛÝŸŽáäâàãåčçćďéěëèêẽĕȇíìîïňñóöòôõøðřŕšťúůüùûýÿžþÞĐđßÆa·/_,:;";
var to = "AAAAAACCCDEEEEEEEEIIIINNOOOOOORRSTUUUUUYYZaaaaaacccdeeeeeeeeiiiinnooooooorrstuuuuuyyzbBDdBAa------";
for (var i = 0, l = from.length; i < l; i++) {
str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i));
}
// Remove invalid chars
str = str.replace(/[^a-z0-9 -]/g, '')
// Collapse whitespace and replace by -
.replace(/\s+/g, '-')
// Collapse dashes
.replace(/-+/g, '-');
return str;
}
@ -146,12 +203,19 @@ function getClasses(obj) {
return 'node ' + classes.join(' ');
}
function getLinkId(link) {
return "link_" + link.nr;
// return "link-" + link.source.id + '-' + link.target.id + '-' + slugify(link.name);
}
class NodeMap {
constructor(parent) {
this.root = d3.select(parent);
this.resizeEvent = window.addEventListener('resize', this.resize.bind(this));
this.tooltipEl = document.getElementById('tooltip');
this.selectedNode = null;
}
resize() {
@ -199,7 +263,47 @@ class NodeMap {
render() {
this.svg = this.root.append('svg')
this.svg.append('defs').html('<marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHead" fill="#f3722c"><path d="M0,-3L8,0L0,3"></path></marker><marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHeadSelected"><path d="M0,-3L8,0L0,3" fill="white"></path></marker>');
this.svg.append('defs').html(`<marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHead" ><path d="M0,-3L8,0L0,3"></path></marker><marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHeadSelected"><path d="M0,-3L8,0L0,3"></path></marker><marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHeadSelectedRelated"><path d="M0,-3L8,0L0,3"></path></marker>
<!--Sketching:-->
<defs>
<filter id="tint">
<feColorMatrix values="1.1 0 0 0 0 0 1.1 0 0 0 0 0 0.9 0 0 0 0 0 1 0" />
</filter>
<filter id="splotch">
<feTurbulence type="fractalNoise" baseFrequency=".01" numOctaves="4" />
<feColorMatrix values="0 0 0 0 0, 0 0 0 0 0, 0 0 0 0 0, 0 0 0 -0.9 1.2" result="texture" />
<feComposite in="SourceGraphic" in2="texture" operator="in" />
<feGaussianBlur stdDeviation="3.5" />
</filter>
<filter id="pencil">
<feTurbulence baseFrequency="0.03" numOctaves="6" type="fractalNoise" />
<feDisplacementMap scale="4" in="SourceGraphic" xChannelSelector="R" yChannelSelector="G" />
<feGaussianBlur stdDeviation="0.5" />
</filter>
</defs>`);
this.svg.on('click', (e) => { console.log(e); this.deselectNode()})
// const noise = 0.001;
// this.svg.append('defs').append('filter').attr('id', 'splotch').html( `${
// noise
// ? `<feTurbulence
// type="fractalNoise"
// baseFrequency="${noise}"
// numOctaves="4"
// ></feTurbulence>
// <feColorMatrix
// values="0 0 0 0 0, 0 0 0 0 0, 0 0 0 0 0, 0 0 0 -0.9 0.2"
// result="texture"
// ></feColorMatrix>
// <feComposite
// in="SourceGraphic"
// in2="texture"
// operator="in"
// ></feComposite>`
// : ``
// }`)
this.resize();
this.projection = d3.geoHill()
@ -234,45 +338,104 @@ class NodeMap {
.data(this.countries)
.enter()
.append("path")
.attr("class", "countries")
.attr("d", this.proj)
.attr("fill", (n) => {
.attr("class", (n) => {
if (CONFIG.countries.indexOf(n.properties.name) !== -1) {
return '';
return 'country eu_country';
}
return "rgba(200,200,200,.7)";
});
return "country";
})
.attr("d", this.proj)
// .attr("filter", 'url(#splotch)')
// .attr("fill", );
this.g_borders
.append("path")
.attr("class", "borders")
.attr("d", this.proj(this.borders))
const zoom = d3.zoom().scaleExtent([0.2, 10]).on("start", () => {
this.svg.node().classList.add("dragging");
}).on("end", () => {
this.svg.node().classList.remove("dragging");
}).on("zoom", ({ transform }) => {
this.container.attr("transform", transform);
const oldZoom = this.svg.classed('zoomed');
const newZoom = transform.k > 2.0;
if (oldZoom != newZoom) {
this.svg.classed('zoomed', newZoom);
let zoomTimeout = null;
this.zoom = d3.zoom()
.scaleExtent([CONFIG.zoom.scale_min, CONFIG.zoom.scale_max])
.on("start", () => {
this.svg.node().classList.add("dragging");
}).on("end", () => {
this.svg.node().classList.remove("dragging");
}).on("zoom", (evt) => {
this.container.attr("transform", evt.transform);
const oldZoom = this.svg.classed('zoomed');
const newZoom = evt.transform.k > 2.0;
if (zoomTimeout) {
clearTimeout(zoomTimeout)
}
zoomTimeout = setTimeout(() => {
this.g_nodes.attr('style', `font-size:${22000 / this.height / evt.transform.k}pt`)
setTimeout(() => {
this.calculateLabels();
}, 500);
}, 250);
if (oldZoom != newZoom) {
this.svg.classed('zoomed', newZoom);
setTimeout(() => {
this.calculateLabels();
}, 500);
}
});
this.svg
.call(zoom)
.call(zoom.transform, d3.zoomIdentity.scale(.5, .5));
}
});
this.title = this.container.append('g').attr('id', 'header');
const titleFeature = {
"type": "LineString",
"coordinates": []
};
const title2Feature = {
"type": "LineString",
"coordinates": []
};
const subtitleFeature = {
"type": "LineString",
"coordinates": []
};
for (let index = 26; index < 70; index++) {
// projection apparently tries to find the shortest path between two points
// which is NOT following a lat/lon line on the globe
titleFeature.coordinates.push([index, 52]);
title2Feature.coordinates.push([index, 50.5]);
subtitleFeature.coordinates.push([index, 49]);
}
this.title.append("path")
.attr("id", "titlePath")
.attr("d", this.proj(titleFeature))
;
this.title.append("path")
.attr("id", "title2Path")
.attr("d", this.proj(title2Feature))
;
this.title.append("path")
.attr("id", "subtitlePath")
.attr("d", this.proj(subtitleFeature))
;
this.title.append("text")
.html('<textPath xlink:href="#titlePath">Remote Biometric</textPath>')
this.title.append("text")
.html('<textPath xlink:href="#title2Path">Identification</textPath>')
this.title.append("text")
.attr("id", "subtitle")
.html('<textPath xlink:href="#subtitlePath">' + CONFIG.subtitle + '</textPath>')
// this.title.append('text')
// .attr('class', 'title')
// .attr('x', 1000)
// .attr('y', 1000)
// .text(CONFIG.title);
// this.title.append('text')
// .attr('class', 'subtitle')
// .attr('x', 1000)
// .attr('y', 1200)
// .text(CONFIG.subtitle);
this.link = this.container.append("g")
.attr('class', 'links')
.selectAll(".link");
this.node = this.container.append("g")
.attr('class', 'nodes')
this.g_nodes = this.container.append("g")
.attr('class', 'nodes');
this.node = this.g_nodes
.selectAll(".node");
@ -283,10 +446,16 @@ class NodeMap {
if (d.printouts[prop].length) {
// console.log("fix node", d);
var p = this.projection([d.printouts[prop][0].lon, d.printouts[prop][0].lat]);
// initial positions:
d.x = p[0];
d.y = p[1];
// d.targetX = p[0];
// d.targetY = p[1];
//These are used when we need to move overlapping points:
d.originalX = p[0];
d.originalY = p[1];
// target pos for the force layout
d.fx = p[0];
d.fy = p[1];
// d.targetLat = d.printouts[prop][0].lat;
@ -296,6 +465,8 @@ class NodeMap {
}
})
this.store.configureTree();
// this.nodeMap = Object.fromEntries(this.graph.nodes.map(d => [d['id'], d]));
@ -322,28 +493,216 @@ class NodeMap {
.force("collision", d3.forceCollide(this.nodeSize))
// TODO look at simpler labels https://github.com/d3fc/d3fc/tree/master/packages/d3fc-label-layout
// TODO look at rects https://github.com/emeeks/d3-bboxCollide
.force("posX", d3.forceX(n => n.targetX || 0).strength(n => n.targetX ? 1 : 0)) // TODO: should not be or 0
.force("posY", d3.forceY(n => n.targetY || 0).strength(n => n.targetY ? 1 : 0))
// .force("posX", d3.forceX(n => n.targetX || 0).strength(n => n.targetX ? 1 : 0)) // TODO: should not be or 0
// .force("posY", d3.forceY(n => n.targetY || 0).strength(n => n.targetY ? 1 : 0))
;
this.svg
.call(this.zoom)
.call(this.zoom.transform, d3.zoomIdentity.scale(.5, .5));
this.update();
setTimeout(() => this.calculateLabels(), 1000);
}
resetZoom() {
this.deselectNode();
this.svg
.transition()
.duration(2000) // milliseconds
.call(this.zoom.transform, d3.zoomIdentity.scale(.5, .5));
}
getSizeForNode(node) {
return this.nodeSize;
}
getNodeRadius(node) {
return CONFIG.nodeRadius;
}
resolveOverlaps() {
// reset:
this.graph.nodes.forEach((n) => {
if (n.hasOwnProperty('originalX')) {
n.x = n.originalX;
n.y = n.originalY;
n.fx = n.originalX;
n.fy = n.originalY;
}
})
this.store.configureTree();
let moved = 0;
// resolve overlapping points by repositioning
this.graph.nodes.forEach((n) => {
// only for fixed points:
if (!n.hasOwnProperty('originalX')) {
return;
}
const startX = n.originalX;
const startY = n.originalY;
let alpha = 0; // angle
let step = 1;
const d_alpha = Math.PI / 4;
const r = this.getNodeRadius(n) + CONFIG.nodeRepositionPadding / 2;
let foundNodes;
let i = 0;
// find a new pos until it's not overlapping anymore...
while ((foundNodes = this.store.findVisibleInCircle(n.x, n.y, r)).length > 1) {
this.store.quadtree.remove(n); // remove uses the current x,y so we need to do this before reconfiguring these
n.x = startX + Math.cos(alpha) * r * 2 * step;
n.y = startY + Math.sin(alpha) * r * 2 * step;
this.store.quadtree.add(n);
alpha += d_alpha;
// on to the next round:
if (alpha > Math.PI * 2) {
step++;
alpha -= Math.PI * 2;
alpha += d_alpha - 2; // little offset
}
i++;
// if(n.fulltext == 'Control Room (Venice)') {
// console.log(startX, startY, n.x, n.y, r, foundNodes);
// // this.store.configureTree();
// // this.store.findVisibleInCircle(n.x, n.y, r).forEach((found) => console.log(found.x, found.y, found));
// }
}
n.fx = n.x;
n.fy = n.y;
if (i > 0) {
// we moved something, update tree
console.debug('resolved for', n.fulltext, i);
moved++;
}
});
console.log(`moved ${moved} nodes`);
}
showTooltip(el, node, links) {
// TODO: make links optional (otherwise collect links here)
this.tooltipEl.innerHTML = `
<span class='category'>${getCategories(node)[0]}</span>
<h3>${node.fulltext}</h3>
`;
if (links.length) {
const rels = links.length === 1 ? 'relationship' : 'relationships';
this.tooltipEl.innerHTML += `
<span class='clickForMore'>Click to examine ${links.length} ${rels}</span>
`;
}
const rect = el.getBoundingClientRect()
const rectTT = this.tooltipEl.getBoundingClientRect();
this.tooltipEl.style.top = (rect.top - rectTT.height) + 'px';
this.tooltipEl.style.left = (rect.left + rect.width / 2 - rectTT.width / 2) + 'px';
// console.log(el, node, rect.top);
this.tooltipEl.classList.add('visible');
}
hideTooltip() {
this.tooltipEl.classList.remove('visible');
}
selectNode(node) {
this.deselectNode(); // remove potential old selection
this.selectedNode = node;
let links = [];
let connectedNodes = [];
for (let link of this.graph.links) {
if (link.source == node || link.target == node) {
links.push(link);
const otherNode = node == link.target ? link.source : link.target;
connectedNodes.push(otherNode);
}
}
let allNodes = [...connectedNodes, node];
this.zoomFit(allNodes);
document.getElementById(node.id).classList.add('selected');
connectedNodes.forEach(n => document.getElementById(n.id).classList.add('linkedSelected'));
links.forEach(l => document.getElementById(getLinkId(l)).classList.add('linkedSelected'));
// TODO: show details;
// alert('not yet implemented');
}
deselectNode() {
this.selectedNode = null;
let nodeEls = document.getElementsByClassName('selected');
while (nodeEls.length) {
nodeEls[0].classList.remove('selected');
}
let els = document.getElementsByClassName('linkedSelected');
while (els.length) {
els[0].classList.remove('linkedSelected');
}
}
update() {
console.log(this.graph)
// console.log(this.graph)
this.resolveOverlaps();
// see also: https://www.createwithdata.com/enter-exit-with-d3-join/
this.node = this.node.data(this.graph.nodes, d => d.id)
.join((enter) => {
let group = enter.append("g").attr("class", getClasses);
let group = enter.append("g")
.attr("class", getClasses)
.attr("id", (n) => getIdForTitle(n.fulltext));
// group.call(drag(simulation));
group.on("click", (evt, n) => selectNode(evt, n, node));
group.append('circle').attr("r", 5 /*this.nodeSize*/);
group.on("click", (evt, n) => {
evt.stopPropagation(); this.selectNode(n);
});
group.on("mouseover", (evt, n) => {
// d3.select(this).classed('hover', true);
const links = document.getElementsByClassName('link');
const linkedLinks = [];
for (let link of links) {
const l = d3.select(link).datum();
if (n == l.target || n == l.source) {
link.classList.add('linkedHover');
// make sure it's the last element, so it's drawn on top
// link.parentNode.appendChild(link); .. causes gliches
// find related related node:
const otherNode = n == l.target ? l.source : l.target;
const otherNodeEl = document.getElementById(otherNode.id);
otherNodeEl.classList.add('linkedHover');
linkedLinks.push(l);
}
}
this.showTooltip(evt.target, n, linkedLinks);
});
group.on("mouseout", (evt, n) => {
this.hideTooltip();
const links = document.getElementsByClassName('linkedHover');
while (links.length) {
links[0].classList.remove('linkedHover');
}
});
// group.append('circle').attr("r", 5 /*this.nodeSize*/);
group.append('path')
.attr('d', (n) => {
return getSymbolForNode(n)(n);
})
var nodeTitle = group.append('text').attr("class", "nodeTitle").attr("y", "3").attr('x', 5);
nodeTitle
.each(function (node, i, nodes) {
@ -432,10 +791,12 @@ class NodeMap {
.data(this.graph.links)
.join(
enter => {
let group = enter.append("g").attr("class", "link");
let group = enter.append("g")
.attr("class", (l) => "link " + slugify(l.name))
.attr("id", getLinkId);
group.append("path")
.attr("marker-end", "url(#arrowHead)")
.attr('id', (d, i) => 'linkid_' + i)
.attr('id', (d, i) => 'linkpath_' + i)
.on("mouseover", function (ev, link) {
d3.select(this).classed('hover', true);
const nodes = document.getElementsByClassName('node');
@ -449,14 +810,18 @@ class NodeMap {
}).on("mouseout", function (ev, link) {
d3.select(this).classed('hover', false);
const nodes = document.getElementsByClassName('linkHover');
for (let n of nodes) {
n.classList.remove('linkHover');
while (nodes.length) {
nodes[0].classList.remove('linkHover');
}
// l.classed('hover',false);
// l.target.classed('hover',false);
// l.source.classed('hover',false);
// console.log(l,'l');
});
}).on("click", (ev, link) => {
ev.stopPropagation();
this.selectNode(link.source);
})
;
group.filter((l) => l.name != "City").append("text").attr("class", "labelText").text(function (l) {
return l.name;
});
@ -590,6 +955,60 @@ class NodeMap {
redraw() {
this.update()
}
// viewBox + preserveAspectRatio can lead to a visible area that is larger than
// the viewBox. Try to get this
getVisibleBox() {
const svgEl = this.svg.node()
const vbRatio = svgEl.viewBox.baseVal.width / svgEl.viewBox.baseVal.height;
const wRatio = this.width / this.height;
if (wRatio > vbRatio) {
// wider
return {
width: (wRatio / vbRatio) * svgEl.viewBox.baseVal.width,
height: svgEl.viewBox.baseVal.height
}
} else {
// taller
return {
width: svgEl.viewBox.baseVal.width,
height: (vbRatio / wRatio) * svgEl.viewBox.baseVal.height
}
}
}
zoomFit(nodes, paddingPercent = 0.8, transitionDuration = 2000) {
// var bounds = root.node().getBBox();
const x0 = Math.min(...nodes.map(n => n.x - CONFIG.nodeRadius));
const x1 = Math.max(...nodes.map(n => n.x + CONFIG.nodeRadius));
const y0 = Math.min(...nodes.map(n => n.y - CONFIG.nodeRadius));
const y1 = Math.max(...nodes.map(n => n.y + CONFIG.nodeRadius));
const width = x1 - x0;
const height = y1 - y0;
const visibleBox = this.getVisibleBox();
const fullWidth = visibleBox.width, //2000, //this.width, TODO: use viewbox now, but consider the overflowing of the vbox
fullHeight = visibleBox.height; //2000; //this.height;
const viewBox = this.svg.node().viewBox.baseVal;
const midX = x0 + width / 2,
midY = y0 + height / 2;
if (width == 0 || height == 0) return; // nothing to fit
let scale = paddingPercent / Math.max(width / fullWidth, height / fullHeight);
scale = Math.min(CONFIG.zoom.scale_max, scale);
const translate = [fullWidth / 2 - midX, fullHeight / 2 - midY];
const transform = d3.zoomIdentity.translate(
-midX * scale + viewBox.width / 2,
-midY * scale + viewBox.height / 2)
.scale(scale);
// console.log("zoomFit", x0, x1, y0, y1, translate, scale, transform);
this.svg
.transition()
.duration(transitionDuration || 0) // milliseconds
.call(this.zoom.transform, transform);
// .call(this.zoom.translateTo, midX, midY)
// .call(this.zoom.scaleTo, scale);
}
}
class AlluvialMap {
@ -625,6 +1044,7 @@ class AlluvialMap {
render() {
this.svg = this.root.append('svg')
this.resize();
this.sankey = d3.sankey()
@ -633,7 +1053,7 @@ class AlluvialMap {
.nodeWidth(40) // height
.nodePadding(10)
.extent([[1, 5], [2000 - 1, 2000 - 5]]);
console.log(this.graph);
// console.log(this.graph);
let s = this.sankey({
nodes: this.graph.nodes.map(d => Object.assign({}, d)),
links: this.graph.links.map(d => Object.assign({}, d))
@ -643,7 +1063,7 @@ class AlluvialMap {
this.links = s.links;
const scale = d3.scaleOrdinal(d3.schemeCategory10);
const color = (c) => c == "Unknown" ? "#333": scale(c);
const color = (c) => c == "Unknown" ? "#333" : scale(c);
this.svg.append("g")
@ -669,12 +1089,12 @@ class AlluvialMap {
.join("g")
.style("mix-blend-mode", "multiply");
const edgeColor = 'path'; // either: path, none, input, output
if (edgeColor === "path") {
const gradient = link.append("linearGradient")
.attr("id", (d,i) => {
.attr("id", (d, i) => {
const id = `link-${i}`; // thanks https://talk.observablehq.com/t/how-do-i-work-with-the-d3-sankey-example/1696/3
d.uid = `url(#${id})`;
return id;
@ -682,12 +1102,12 @@ class AlluvialMap {
.attr("gradientUnits", "userSpaceOnUse")
.attr("y1", d => d.source.x1)
.attr("y2", d => d.target.x0)
.attr("x1",0)
.attr("x1", 0)
.attr("x2", 0);
// .attr("y1", "0%")
// .attr("y2", "100%")
// .attr("x1", "0%")
// .attr("x2", "0%");
// .attr("y1", "0%")
// .attr("y2", "100%")
// .attr("x1", "0%")
// .attr("x2", "0%");
gradient.append("stop")
.attr("offset", "0%")
@ -733,7 +1153,13 @@ class AlluvialMap {
}
titleIdMap = {};
function getIdForTitle(title) {
if (!titleIdMap.hasOwnProperty(title)) {
titleIdMap[title] = slugify(title) + `-${Object.keys(titleIdMap).length}`
}
return titleIdMap[title];
}
JsonToGraph = function (data) {
let nodes = [];
@ -741,10 +1167,13 @@ JsonToGraph = function (data) {
let smwBugFixLocationMaps = {};
console.log(data)
let i = 0;
let linkI = 0;
for (const node_id in data.results) {
if (Object.hasOwnProperty.call(data.results, node_id)) {
i++;
let node = data.results[node_id];
node.id = node.fulltext; //node_id;
node.id = getIdForTitle(node.fulltext); //node_id;
nodes.push(node);
// console.log(node_id, node);
@ -764,9 +1193,9 @@ JsonToGraph = function (data) {
}
for (const target_node of node.printouts[prop]) {
links.push({
"source": node_id,
"target": target_node.fulltext,
"name": prop
"source": node.id,
"target": getIdForTitle(target_node.fulltext),
"name": prop,
})
}
}
@ -795,14 +1224,18 @@ JsonToGraph = function (data) {
console.debug(`Fixed location for ${fixes} nodes`);
console.log(links.length);
nodeMap = Object.fromEntries(nodes.map(d => [d['id'], d]));
links = links.filter(l => nodeMap[l.source] && nodeMap[l.target]).map(l => {
l.source = nodeMap[l.source];
l.target = nodeMap[l.target];
l.nr = linkI++;
return l;
});
}).filter((link, index, self) =>
// remove incidental duplicates
index === self.findIndex((l) => (
l.source.id === link.source.id && l.target.id === link.target.id && l.name === link.name
))
);
@ -999,6 +1432,7 @@ class Store {
constructor(graph, parent) {
this.nodes = graph.nodes;
this.links = graph.links;
// graph is a filtered version of this.nodes & this.links
this.graph = {
'nodes': [],
'links': []
@ -1012,10 +1446,52 @@ class Store {
'categories': [],
}
this.filter();
}
configureTree() {
// set up the tree, we do this only after all points are configured.
this.quadtree = d3.quadtree(
this.nodes,
(n) => n.x,
(n) => n.y
);
}
// from: https://observablehq.com/@d3/quadtree-findincircle
findInCircle(x, y, radius, filter) {
if (typeof this.quadtree === 'undefined') {
this.configureTree();
}
const result = [],
radius2 = radius * radius,
accept = filter
? d => filter(d) && result.push(d)
: d => result.push(d);
this.quadtree.visit((node, x1, y1, x2, y2) => {
if (node.length) {
return x1 >= x + radius || y1 >= y + radius || x2 < x - radius || y2 < y - radius;
}
const dx = +this.quadtree._x.call(null, node.data) - x,
dy = +this.quadtree._y.call(null, node.data) - y;
if (dx * dx + dy * dy < radius2) {
do { accept(node.data); } while (node = node.next);
}
});
return result;
}
findVisibleInCircle(x, y, radius) {
return this.findInCircle(x, y, radius, (n) => !n.filtered);
}
registerMap(map) {
this._maps.push(map);
return this;
@ -1067,9 +1543,16 @@ class Store {
render() {
CONFIG.filters.forEach(f => {
let labelEl = document.createElement('label')
let inputEl = document.createElement('input')
let textEl = document.createElement('span');
let svg = d3.select(labelEl).append('svg')
.attr("viewBox", [-12, -12, 24, 24]);
svg.append('g')
.attr("class", "node " + f)
.append('path')
.attr('d', getSymbolForCategories(f)());
inputEl.type = "checkbox";
textEl.innerText = f;
labelEl.appendChild(inputEl);
@ -1103,7 +1586,7 @@ class Store {
var mapGraph = new NodeMap('#map')
var alluvialGraph = new AlluvialMap('#alluvial')
// var alluvialGraph = new AlluvialMap('#alluvial')
// REQUEST ATLAS & GRAPH
const req_data = new Request(CONFIG.dataUrl, { method: 'GET' });
@ -1118,12 +1601,11 @@ Promise.all([fetch(req_data), fetch(req_world)])
mapGraph.setWorld(world);
mapGraph.setStore(store);
// console.log();
alluvialGraph.setData(data.results);
// alluvialGraph.setData(data.results);
store.render()
mapGraph.render()
alluvialGraph.render()
// alluvialGraph.render()
}).catch(error => {
console.error(error);

View file

@ -3,27 +3,41 @@
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="graph.css">
</head>
<body>
<div id='map'></div>
<div id='alluvial'></div>
<div id='tooltip'></div>
<div id='map'></div>
<!-- <div id='alluvial'></div> -->
<div id="filters">
</div>
<header>
<h1>Remote Biometric Identification</h1>
<p class='subtitle'>A survey of the European Union</p>
<script src="https://d3js.org/d3.v6.js"></script>
<script src="https://d3js.org/d3-geo-projection.v3.min.js"></script>
<script src="https://d3js.org/topojson.v3.min.js"></script>
<script src="//unpkg.com/d3-geo-zoom"></script>
<aside id="filters">
<h3>Legend</h3>
</aside>
<script src="https://unpkg.com/d3-sankey@0"></script>
<!-- <script src="//unpkg.com/d3fc@14.0.1"></script> -->
<script src="graph.js"></script>
</header>
<script src="https://d3js.org/d3.v6.js"></script>
<script src="https://d3js.org/d3-geo-projection.v3.min.js"></script>
<script src="https://d3js.org/topojson.v3.min.js"></script>
<script src="//unpkg.com/d3-geo-zoom"></script>
<script src="https://unpkg.com/d3-sankey@0"></script>
<!-- <script src="//unpkg.com/d3fc@14.0.1"></script> -->
<script src="graph.js"></script>
<script>
if(window.location.hash == '#light') {
document.body.classList.add('light');
}
</script>
</body>
</html>

506
www/story.html Normal file
View file

@ -0,0 +1,506 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Biometric Mass Surveillance</title>
<style>
body {
margin: 0;
font-family: sans-serif;
}
iframe {
position: fixed;
top: 0;
left: 0;
width: 50vw;
height: 100vh;
}
content {
margin-left: 55vw;
max-width: 800px;
margin-right: 5vw;
display: block;
}
</style>
</head>
<body>
<iframe id='frame' src="index.html"></iframe>
<content>
<div class="section">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi risus augue, pellentesque quis iaculis
vitae, condimentum a leo. Nunc scelerisque, odio a tristique molestie, massa mi sollicitudin odio, ac
fermentum orci nisi a ipsum. Curabitur pellentesque eleifend risus, eget porta tellus malesuada ut. Nunc
dignissim, ipsum pellentesque accumsan faucibus, mi leo mattis ante, a rhoncus massa leo vel augue.
Mauris eros nunc, vehicula et faucibus at, convallis non odio. Ut gravida, lorem non sodales maximus,
purus justo blandit felis, at sollicitudin dolor erat eget est. Ut euismod auctor lorem, iaculis
pulvinar odio tempus et. Proin ullamcorper cursus posuere. Donec venenatis diam nulla, sed egestas ipsum
vestibulum ultrices. Proin lobortis elementum semper. Morbi sollicitudin aliquam tortor, ac maximus urna
finibus vel. Vivamus ultricies, nibh non hendrerit rutrum, sapien neque eleifend leo, nec lobortis
libero orci ut sapien.
</p>
<p>
Nulla porttitor dolor laoreet elit ornare, eu pellentesque nulla tempus. Duis et suscipit massa. Etiam
velit lorem, ornare at odio vel, malesuada imperdiet massa. Pellentesque mauris justo, sollicitudin quis
varius vel, dignissim ut turpis. Maecenas vel odio mauris. Aliquam maximus maximus erat vel fermentum.
Aenean vel venenatis diam. Interdum et malesuada fames ac ante ipsum primis in faucibus. Curabitur risus
risus, facilisis sed velit a, feugiat eleifend quam. Donec rutrum molestie sem.
</p>
<p>
Ut metus elit, porta et turpis in, sodales imperdiet ligula. Nam vulputate ultricies urna non facilisis.
Vivamus sit amet pulvinar risus. Nullam id odio tempus, malesuada metus et, lobortis nibh. Suspendisse
viverra nulla magna, id tincidunt turpis pretium rhoncus. Pellentesque vel aliquet dui. Mauris eleifend
efficitur libero eu porttitor. Aenean congue interdum dui eget aliquam. Maecenas volutpat porta quam,
vitae congue justo cursus in. Fusce ac congue lorem. Nulla pretium odio ac quam aliquam convallis.
</p>
<p>
Fusce a urna ac lorem eleifend eleifend. Etiam mollis quam nisl, et scelerisque felis lacinia quis.
Mauris ullamcorper mi et feugiat auctor. Sed placerat tincidunt tincidunt. Vestibulum dictum vestibulum
orci dignissim ornare. Nullam sed congue sapien. Donec rhoncus nibh quis malesuada egestas. Cras non est
quam. Curabitur vulputate, metus non faucibus rutrum, ex lorem viverra ligula, sodales vehicula sapien
diam quis justo. Quisque tempor maximus mauris sit amet eleifend. Cras auctor feugiat ullamcorper.
Mauris ultricies elit vel euismod volutpat. Praesent in elit dignissim, dignissim urna varius,
consectetur lacus. Sed ligula tellus, consectetur a ligula a, tempus hendrerit urna. Maecenas volutpat
sapien risus, ac egestas ex porta et.
</p>
<p>
Nullam velit quam, efficitur vitae sem nec, sodales porttitor orci. Sed egestas lacus sit amet ex
dictum, sit amet tincidunt ex hendrerit. Morbi pharetra, ipsum sed gravida fermentum, felis quam rhoncus
lacus, vitae auctor enim ligula pulvinar tellus. Cras sapien augue, ultricies pulvinar arcu et,
convallis porttitor augue. Vestibulum accumsan lorem eros, id volutpat orci tincidunt at. Vivamus porta
magna id finibus tristique. Quisque sodales augue in lacus tristique ornare. Curabitur cursus hendrerit
neque rutrum blandit. Maecenas posuere ex eget eleifend fringilla.
</p>
<p>
Integer est dui, elementum in nisl nec, blandit auctor libero. Cras in nulla nunc. Curabitur fringilla
dolor sagittis felis fringilla, non euismod felis tempus. Aenean scelerisque euismod nisl vitae
tincidunt. Nam nec purus eleifend, fringilla diam quis, faucibus ex. Maecenas nunc felis, rhoncus sed
sollicitudin non, luctus sed dolor. Phasellus elit neque, rutrum a est venenatis, convallis imperdiet
dui. Vestibulum at ultrices eros, at vulputate sem. Ut vestibulum libero sed sapien sagittis, id laoreet
velit eleifend. Phasellus eleifend nec odio non bibendum. Suspendisse nec lectus nec ex blandit rutrum
nec et felis. Cras libero justo, sagittis sed lacinia quis, pharetra quis nibh. Maecenas vitae lacus ac
augue condimentum suscipit.
</p>
<p>
Pellentesque eu tempor orci. Aenean pulvinar, justo ac maximus molestie, nibh nunc rutrum ex, varius
finibus mi nulla lobortis tellus. Nam dictum leo maximus ultrices maximus. Aliquam bibendum libero sem,
mattis pulvinar erat vulputate quis. Duis venenatis nisi ac sem iaculis, nec feugiat orci elementum.
Curabitur accumsan lacinia justo id suscipit. In lacinia ornare justo quis egestas. Nullam nec lacus a
ligula pulvinar tempus eget id nunc. Morbi tincidunt placerat placerat.
</p>
</div>
<div class="section" data-title="Dragonfly Project">
<h2>Project Dragonfly</h2>
<p>
Morbi vitae nulla erat. Etiam eros ipsum, tristique in maximus sed, rutrum a mi. Integer molestie
imperdiet mauris sit amet lacinia. Aenean dignissim posuere lacus, ut vulputate tellus venenatis
posuere. Nullam suscipit tempor massa at viverra. Sed volutpat justo ut lacinia luctus. Sed egestas
hendrerit tellus, non bibendum quam gravida non. Aenean suscipit ligula tristique venenatis molestie.
Nunc aliquet orci mi.
</p>
<p>
Nunc vel dolor lorem. Sed venenatis lacinia sapien ut aliquam. Nulla vitae tincidunt sem. Aliquam eu
nisl pharetra, ornare justo nec, egestas nisi. Nam et metus vitae enim tempus vestibulum. Fusce ipsum
quam, maximus et mattis nec, consectetur vitae urna. Vivamus rhoncus augue sed justo bibendum, in
imperdiet metus vestibulum. Nam egestas eleifend enim ultricies lobortis. Pellentesque sed nibh eros.
Nulla mattis facilisis felis, eu lacinia neque condimentum ut. Suspendisse fermentum ultrices faucibus.
Aliquam ac ultrices lectus, in pharetra lectus. Curabitur in molestie felis. Proin ac ante eget sem
fringilla hendrerit sit amet a leo. Sed at nunc vitae dolor vestibulum luctus quis ac nunc. Suspendisse
nunc justo, gravida id pharetra et, convallis nec velit.
</p>
<p>
Nunc dui leo, varius ac venenatis nec, vehicula et dui. Pellentesque quis enim dolor. Vestibulum
tincidunt egestas porta. Suspendisse ac nisl nisl. Pellentesque auctor convallis euismod. In pharetra
iaculis accumsan. Maecenas scelerisque molestie dui eu gravida. Aliquam hendrerit ipsum vitae laoreet
laoreet. Phasellus nec mattis leo, at rhoncus sem. Nam maximus, dolor eget consequat mattis, tortor leo
blandit mi, in lacinia est ligula et erat. Fusce at nisi eleifend, dapibus leo eget, porttitor ipsum.
Duis tristique velit vitae dui fermentum, quis gravida lorem tempor. Phasellus sed risus diam. Aenean
fringilla justo ac felis lacinia, quis placerat tellus pretium.
</p>
<p>
Vestibulum ante nisl, porta vitae ultrices in, fermentum eget tellus. Quisque ornare, leo aliquet tempor
aliquet, ligula diam vehicula nisi, nec faucibus mauris ex at dui. Phasellus vel ligula suscipit lacus
eleifend egestas. Suspendisse ut lacus suscipit, gravida dui at, volutpat dolor. Nunc suscipit erat ac
risus tempus tempor. Nulla quis mi blandit, hendrerit mauris eget, commodo nisl. Aliquam ultricies purus
metus, quis gravida ante condimentum quis. Mauris porta vestibulum ornare. Vivamus elementum pharetra
nisl, a vehicula nibh. Etiam ac turpis tortor.
</p>
<p>
Nunc non elit ac diam interdum pretium ac sed velit. Etiam non metus elementum, consectetur ante vel,
iaculis lorem. Pellentesque convallis finibus ipsum ut fermentum. Morbi at eros vel felis pharetra
dapibus faucibus et nisl. Nunc sit amet felis nec leo malesuada aliquam quis in risus. Sed quis leo
felis. Curabitur in porta urna. Nulla dignissim, tortor et sodales auctor, ante odio porttitor nunc, ut
tincidunt ligula dolor non sapien. Suspendisse vitae iaculis urna, id commodo sem.
</p>
<p>
Nulla facilisi. Sed at augue sit amet erat posuere consectetur. Aliquam pretium lectus ipsum, nec ornare
enim tincidunt sit amet. Praesent sit amet iaculis odio. Etiam nibh risus, pulvinar vel arcu quis,
elementum faucibus mauris. Curabitur posuere leo a consectetur hendrerit. Proin finibus lobortis turpis,
ac feugiat est elementum at. Suspendisse vitae neque id nisi ultricies viverra. Curabitur ut libero sed
arcu accumsan accumsan a sed ex. Sed suscipit non sapien non eleifend. Vivamus condimentum scelerisque
est, sit amet sollicitudin nisi lacinia nec. Nunc in lacus consectetur neque ultrices laoreet ut et est.
Quisque vulputate odio neque, eget laoreet felis molestie sit amet.
</p>
</div>
<div class='section' data-title="Data-lab Burglary-free Neighbourhood">
<h2>Burglary-free Neighbourhood</h2>
<p>
Aenean fringilla nisl sed ante congue, in commodo tellus ullamcorper. Nullam sollicitudin, lacus id
interdum facilisis, erat augue feugiat justo, ac finibus nunc tellus ac odio. Cras a tempor nulla.
Nullam consequat convallis eros, sed interdum orci. Etiam justo lectus, ornare nec risus ut,
pellentesque mattis ante. Integer tristique sapien quis lacus cursus luctus. Pellentesque risus lacus,
auctor eget lacus eu, dapibus pretium ligula. Sed eget tortor ut lacus elementum facilisis. Pellentesque
velit ante, sagittis vel odio nec, rhoncus sagittis sem. Duis sodales lectus eget suscipit scelerisque.
</p>
<p>
Donec suscipit, nulla at congue scelerisque, eros odio consectetur quam, a ornare ex leo a sapien.
Curabitur gravida massa at posuere aliquet. Maecenas consequat, dui quis posuere hendrerit, nunc ipsum
interdum erat, sit amet ultricies justo nibh vel nulla. Integer venenatis semper ipsum quis congue.
Nulla facilisi. Proin dapibus diam nec erat viverra imperdiet. Proin quis rhoncus sem. Maecenas
ultricies elementum mauris, sit amet rutrum libero laoreet cursus. Maecenas quam velit, consectetur sit
amet sapien vitae, tempor egestas nisi. Suspendisse pulvinar ultricies erat sit amet pretium. In luctus,
diam vitae ornare feugiat, risus eros tincidunt orci, at convallis lacus lacus quis felis.
</p>
<p>
Praesent et facilisis elit, id ultricies magna. Donec interdum sed dolor vitae blandit. Vestibulum
hendrerit eros et magna finibus, nec pharetra est aliquam. Suspendisse tempor sagittis maximus. Ut
porttitor tellus ipsum, non malesuada lectus vulputate a. Fusce vitae bibendum justo, non aliquam elit.
Mauris fringilla purus sit amet aliquet laoreet. Nulla tempus ac metus luctus porta. Donec quis tellus
luctus, dignissim nisl vitae, iaculis eros. Duis nisl nulla, rhoncus eu iaculis non, faucibus vitae
risus. Pellentesque consectetur faucibus metus. Nulla facilisi.
</p>
<p>
Mauris maximus ullamcorper mattis. Morbi consectetur mollis sapien, sed ultricies ipsum interdum vitae.
Proin feugiat vitae elit eget pharetra. Sed in maximus nisl, non ultrices massa. In nec ipsum placerat,
tempor neque vitae, consequat neque. Nam in tempor libero. Sed feugiat massa pellentesque malesuada
volutpat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec
vel odio vel orci auctor aliquam convallis id lacus. Sed euismod ultricies ullamcorper. Morbi lobortis
luctus interdum. Quisque ac nisl lobortis, elementum tortor et, euismod erat.
</p>
<p>
Etiam at dolor mauris. Morbi in mauris scelerisque, suscipit lacus id, condimentum est. Vestibulum ante
ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Sed rutrum lobortis ullamcorper.
Vivamus ac mi non libero rutrum tempus. Donec ante ipsum, gravida eu magna auctor, efficitur molestie
augue. Aenean a justo eros. Vivamus aliquet pretium eros, tempus feugiat odio molestie vitae. Aenean
mattis gravida erat, eget venenatis dolor faucibus nec. Nullam in risus feugiat, aliquam nisi vel,
tincidunt tortor. Donec quis imperdiet felis. Curabitur suscipit auctor purus quis faucibus. Cras nec
diam rutrum, pharetra erat ut, pellentesque augue. Morbi mattis massa ut ante congue blandit. Orci
varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
</p>
<p>
Integer tincidunt odio non mauris egestas viverra. Nulla blandit vulputate enim vel vestibulum. Donec
suscipit dignissim nisi a scelerisque. Class aptent taciti sociosqu ad litora torquent per conubia
nostra, per inceptos himenaeos. Donec egestas mauris posuere diam tempor, vel consectetur enim rhoncus.
Phasellus accumsan feugiat dui, sit amet viverra neque facilisis in. Pellentesque fermentum ipsum
porttitor, luctus felis eu, ornare quam. Integer ut pharetra nisi. Phasellus sed facilisis quam, nec
porta metus. Sed consequat ac nisl eget faucibus. Proin blandit nisi id pharetra vestibulum. Vestibulum
posuere nibh vitae elit pharetra rhoncus at a ligula. Duis at tincidunt dolor. Quisque a diam varius,
facilisis libero a, volutpat dolor. Nulla vel accumsan diam. Praesent eu neque nibh.
</p>
</div>
<p>
Suspendisse ac libero mi. In hac habitasse platea dictumst. Ut bibendum a dui eu congue. Sed et lacus
congue, faucibus nulla cursus, sagittis odio. In ac risus vehicula, interdum mauris quis, porttitor
massa. Vivamus non iaculis tortor. Mauris ac pharetra urna. Integer ac mattis enim. Integer egestas
velit purus. In hac habitasse platea dictumst. Ut interdum felis ut libero pellentesque, ac vulputate
felis faucibus. Sed facilisis fringilla leo, et condimentum quam fermentum ac. Maecenas blandit
sollicitudin libero in pretium.
</p>
<p>
Donec sodales convallis sapien eget rutrum. Nullam fermentum nisi nec auctor convallis. Phasellus
euismod ante erat, ut vulputate sem rhoncus et. Quisque ac eros sit amet metus aliquet blandit. Integer
sagittis ligula vitae orci gravida rhoncus. Vestibulum eu ante id ipsum ultricies tempor. Proin viverra,
felis vel suscipit laoreet, lorem nisl condimentum ipsum, id convallis ipsum ante ac sem. Maecenas vitae
ultricies augue. Morbi lectus dolor, tempus ac scelerisque sit amet, condimentum eget tortor. Fusce id
rutrum ligula.
</p>
<p>
In suscipit purus at leo convallis sodales. Sed a placerat arcu. Fusce id mi quis nisl ullamcorper
imperdiet. Donec eu leo sit amet lacus euismod accumsan. Fusce facilisis congue leo eu commodo. Sed vel
libero sem. Nulla nec volutpat eros. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. In lacinia massa sed justo tincidunt, sed aliquam erat tristique. Nam
porta pharetra nisi eu aliquet.
</p>
<p>
In in neque vel ligula dictum suscipit. Cras cursus a felis nec mattis. Praesent rhoncus orci nec nulla
rutrum, sit amet iaculis massa varius. Mauris sit amet finibus ipsum. Nam sit amet augue sed ipsum
laoreet venenatis. Praesent feugiat faucibus congue. Nunc vulputate ante at neque suscipit auctor. In
nec sagittis lacus. Suspendisse ornare ut eros a lobortis. Donec et pharetra urna, sit amet dictum enim.
Morbi fringilla ligula tristique nibh tempus, quis pharetra sem imperdiet. Suspendisse auctor fringilla
purus, ultricies feugiat augue consectetur et. Integer neque mauris, elementum imperdiet finibus vel,
suscipit ac metus. In hac habitasse platea dictumst. Mauris eu nibh porttitor, tempor enim vel,
tristique libero.
</p>
<p>
Suspendisse commodo in justo a hendrerit. Nunc nec laoreet ante. Nam ac tincidunt libero, sed rhoncus
libero. Nunc et arcu a ex consequat malesuada eget vel nunc. Mauris felis lectus, sodales sed lacus id,
congue blandit diam. Curabitur eu quam accumsan augue accumsan lacinia. Nulla id justo elementum,
placerat lectus et, varius purus. Nam congue, erat at congue posuere, diam felis consequat mi, a tempor
erat odio id dolor. Nunc pulvinar libero sit amet neque posuere, sit amet placerat metus facilisis. In
justo libero, facilisis condimentum molestie eu, tristique in sapien. Interdum et malesuada fames ac
ante ipsum primis in faucibus. In mattis, nisl eu mollis ultricies, justo lacus commodo nisl, ut
pulvinar augue odio nec leo. Pellentesque ornare tellus nec massa pellentesque, id euismod odio mattis.
Aliquam at massa a nisi maximus varius. Donec interdum odio id nibh commodo, in ultricies lacus lacinia.
Ut et ultricies libero.
</p>
<p>
Nunc pretium vehicula leo, ut vehicula dolor ultricies quis. Nunc ultricies elit lobortis lorem tempus
placerat. Vestibulum vitae velit ut libero pretium dignissim sed vitae sapien. Ut ornare nunc nec tortor
sodales, nec volutpat risus ornare. Morbi ac iaculis arcu. Fusce viverra feugiat porta. Nam aliquam
aliquet urna quis consequat. Sed suscipit arcu ut eros imperdiet posuere non at nunc. Quisque ac
convallis lacus, non efficitur nulla. Interdum et malesuada fames ac ante ipsum primis in faucibus.
Fusce suscipit tincidunt pharetra. Sed eget egestas enim.
</p>
<p>
Quisque tellus nulla, facilisis eget ante ac, pharetra pretium mauris. Nam arcu urna, condimentum sed
urna sed, mattis dictum nisi. Maecenas eget ante facilisis, dignissim nisl non, venenatis odio. Integer
turpis enim, sagittis eu commodo nec, mollis at ex. In non dui dictum, porttitor mauris in, rutrum
dolor. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Proin vel
nisi eros. Aliquam vel turpis est. Class aptent taciti sociosqu ad litora torquent per conubia nostra,
per inceptos himenaeos. In libero risus, feugiat non tristique quis, egestas vel tortor. Praesent cursus
lorem nec euismod placerat. Fusce tincidunt non risus at luctus. Nullam hendrerit mi diam, ut rhoncus
neque accumsan vel. Sed cursus dolor id velit ullamcorper rhoncus. Duis volutpat sodales aliquam.
</p>
<p>
Curabitur dignissim posuere ex sit amet porta. Sed viverra arcu et sapien tristique, ullamcorper
eleifend elit posuere. Aenean nec augue dui. Proin semper efficitur vehicula. Praesent vestibulum tortor
fringilla dolor congue maximus. Morbi sagittis mollis blandit. Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Cras pulvinar laoreet tincidunt. Mauris hendrerit mollis neque nec vehicula. Mauris
auctor pharetra nisl, at ornare ante accumsan feugiat. Maecenas felis risus, posuere vel nunc et,
fringilla viverra dolor. Cras consectetur rutrum ipsum vel tempus. Etiam mattis ante in lacus mollis,
non porta quam pulvinar. Sed rutrum tempor placerat. Sed id dui eleifend, interdum tellus ut, sagittis
odio. Ut iaculis at enim a dignissim.
</p>
<p>
Proin tempor mi sed mattis mollis. Quisque nulla odio, rutrum vitae ligula eget, interdum faucibus
tellus. Aliquam et augue vel justo molestie vestibulum eu nec risus. Maecenas aliquet, risus eget
feugiat consequat, diam magna bibendum diam, sed pharetra tellus purus ut est. Aenean dictum pulvinar
vestibulum. Sed non neque eros. Etiam vitae aliquam nisl, quis congue velit. Ut gravida libero vel
vestibulum tincidunt. Sed vel nibh ac velit aliquam porttitor eget et nulla.
</p>
<p>
Suspendisse ut odio molestie odio auctor sollicitudin. Vestibulum viverra ornare nibh, vel lobortis odio
dapibus ac. Cras dapibus porta libero, nec congue nisi sagittis sit amet. Class aptent taciti sociosqu
ad litora torquent per conubia nostra, per inceptos himenaeos. Donec ullamcorper leo vitae urna
sollicitudin sodales. Suspendisse quis nulla vitae quam ornare lobortis sodales sit amet lectus.
Interdum et malesuada fames ac ante ipsum primis in faucibus. Ut eleifend gravida laoreet. Cras id purus
vehicula, egestas velit sit amet, pretium magna. In in pulvinar quam.
</p>
<p>
Aliquam fermentum a tellus a sodales. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. Etiam facilisis molestie ipsum ac viverra. Vestibulum lacus nunc,
egestas at lorem vel, pulvinar consequat lacus. Sed feugiat nisl vel risus vehicula, sed imperdiet elit
ornare. Maecenas non magna vitae erat feugiat scelerisque. Nam vulputate lacus porta porttitor
sollicitudin.
</p>
<p>
Quisque auctor tellus lorem, et ornare tellus efficitur id. Cras dictum tortor quis lorem facilisis, in
molestie erat porta. Sed vel porttitor libero. Etiam pulvinar augue a semper finibus. Mauris tellus
metus, lobortis non viverra eu, commodo quis nunc. Maecenas vitae pellentesque lectus. Nunc ut felis
tellus. Phasellus a mi in neque placerat egestas eget vitae tellus. Vivamus ut metus tellus.
Pellentesque mollis tempus tempus.
</p>
<p>
Ut at pretium lacus, non sollicitudin velit. Vivamus sagittis fermentum purus, nec suscipit lectus
imperdiet eu. Cras velit risus, porttitor vitae lectus id, condimentum pulvinar sem. Ut lacinia metus eu
rhoncus finibus. Vivamus accumsan, ex suscipit porttitor dictum, tellus lectus luctus lorem, sed lacinia
lectus sem et ligula. Duis vitae turpis nunc. Curabitur non sapien felis. Maecenas a egestas nunc, eget
luctus quam. Duis vestibulum metus eu turpis vulputate, et lacinia ante maximus. Vivamus molestie leo
eget sem dictum, nec posuere sapien euismod.
</p>
<p>
Cras et risus venenatis nisi dapibus feugiat non at dui. Pellentesque faucibus nulla aliquam nunc
congue, ut fringilla est viverra. Mauris nulla elit, ornare non augue a, cursus elementum diam. Proin
pulvinar justo nulla, et semper tortor dapibus ac. Maecenas ac libero ut eros hendrerit auctor at nec
augue. Aliquam dictum, ligula fermentum lobortis egestas, risus neque pulvinar felis, id lobortis tellus
nunc eget massa. Fusce vel mollis arcu. Suspendisse dapibus, tortor in blandit pulvinar, nulla dolor
euismod justo, vel ullamcorper dui ante ac dolor. Duis congue urna et purus posuere semper. Phasellus ut
feugiat justo, in commodo turpis. Donec venenatis et urna ut vestibulum.
</p>
<p>
Nam tristique odio a ultricies suscipit. Cras sollicitudin vulputate euismod. Sed elementum congue
tortor, ut pretium lacus dapibus accumsan. Sed eget aliquam tortor. Donec blandit tempor tellus, sit
amet pharetra mauris iaculis vel. Quisque pharetra nisi eget orci egestas dignissim. Mauris sed laoreet
ligula, vel placerat ante. Curabitur vel libero ornare, suscipit ante sed, feugiat orci. Sed malesuada
mattis augue. Sed auctor mattis diam, bibendum pretium arcu tempus quis.
</p>
<p>
Praesent id finibus purus, sed congue turpis. Maecenas fringilla, nisi non elementum congue, diam nunc
vestibulum urna, non elementum quam risus ut tellus. Donec imperdiet, turpis vel elementum sollicitudin,
ipsum odio faucibus lectus, quis facilisis urna nibh non ipsum. Sed sed massa sed orci cursus viverra eu
quis velit. Donec volutpat pretium erat vitae sodales. Suspendisse posuere nisi at orci rhoncus,
fermentum sodales lacus scelerisque. Suspendisse consectetur enim eget faucibus eleifend. Nullam magna
eros, elementum id aliquam non, egestas eu nunc. Sed tincidunt scelerisque nibh, id venenatis dolor
ultricies vitae. Morbi sapien nisi, tempor ut odio non, varius varius ipsum.
</p>
<p>
Pellentesque sit amet maximus nibh, et placerat orci. Duis accumsan libero a dolor dignissim viverra.
Sed purus nibh, egestas eu nisi at, commodo elementum sem. Nunc id dui a ex condimentum venenatis.
Maecenas id mattis nunc. Fusce accumsan nibh nec lacus congue consequat. Donec cursus laoreet leo in
egestas. Proin sit amet dui tristique, rhoncus tortor eget, aliquet tellus. Aenean vitae pellentesque
ex, in congue odio. Etiam lectus velit, fermentum ultricies consequat eget, pellentesque quis tellus.
</p>
<p>
Aenean id libero et turpis fringilla elementum. Orci varius natoque penatibus et magnis dis parturient
montes, nascetur ridiculus mus. Sed vel ex euismod, dignissim nunc ac, consequat ipsum. Donec et tortor
nisi. Nulla tempor turpis nec elit aliquam, id rutrum nulla blandit. Maecenas ullamcorper turpis
placerat lectus imperdiet tincidunt. Vestibulum tempor posuere risus tincidunt porttitor. Mauris sit
amet hendrerit diam, non porta sem. Proin finibus quam eu metus vulputate, quis vehicula lectus
lobortis. Aenean libero felis, ullamcorper sit amet sagittis et, scelerisque et leo. Sed vehicula sem
justo, eget pulvinar leo laoreet nec. In hac habitasse platea dictumst.
</p>
<p>
Donec ac felis sem. Morbi lorem felis, mollis ac elit ut, luctus congue augue. Donec leo purus, accumsan
non pulvinar vel, hendrerit vitae diam. Etiam bibendum ut quam vehicula cursus. Mauris tristique
volutpat ligula. Ut quis augue sollicitudin, tincidunt nisl ac, egestas turpis. Morbi gravida in nisi
non interdum. Nullam pharetra accumsan tellus in laoreet. Mauris auctor auctor odio. Nunc cursus at
metus a fermentum. Interdum et malesuada fames ac ante ipsum primis in faucibus.
</p>
<p>
Mauris hendrerit vel mi non mollis. Morbi nibh felis, convallis non lacinia sed, accumsan in nulla.
Fusce non erat in massa venenatis euismod. Fusce justo felis, varius nec dolor vel, lobortis fringilla
enim. Pellentesque ac fringilla mi. Aliquam ultrices interdum pellentesque. Vivamus purus mi, porta ut
dui malesuada, interdum lobortis leo. Sed in sem in dolor hendrerit posuere ut eget tortor. Duis id
dolor vitae risus accumsan tincidunt.
</p>
<p>
Sed consectetur ornare risus porta rutrum. Sed vel leo at neque rutrum lobortis eu at lorem. Curabitur
ultrices congue lacinia. Proin non bibendum augue. Maecenas et volutpat leo, in rutrum neque. In
efficitur, purus quis pharetra pretium, nibh metus pharetra metus, vitae euismod mi metus a nibh.
Praesent condimentum velit felis, sollicitudin pretium elit lacinia nec. Integer non arcu dignissim,
suscipit nunc nec, rutrum tellus. Mauris lobortis, lorem lacinia sagittis tincidunt, mi lacus viverra
orci, id ultricies ipsum dui at quam.
</p>
<p>
Maecenas ornare sagittis vestibulum. Phasellus egestas a tellus sit amet ullamcorper. Sed eu vestibulum
sapien. Ut et ipsum augue. Sed tincidunt nibh magna, non accumsan dolor aliquet eu. Cras vitae pharetra
elit. Donec pretium dui nec pellentesque vehicula. Cras eu sem elementum mi posuere fringilla. Duis
porta, purus a sodales rhoncus, eros eros iaculis turpis, nec ornare magna nisl a ex. Pellentesque
dictum eu mi non pretium. Donec pharetra elementum dui. Curabitur et nunc ut nunc euismod aliquet eu
quis libero. Quisque sollicitudin metus maximus elit lacinia bibendum a a velit. Sed commodo est at nisl
varius, et egestas libero mattis. Vivamus sit amet rutrum turpis, nec vulputate purus. Morbi in feugiat
massa.
</p>
<p>
Pellentesque vel justo congue, scelerisque ex at, vehicula diam. Donec elementum tincidunt nulla, et
pharetra ex varius et. Suspendisse iaculis ligula a dolor bibendum, quis eleifend velit tristique. Fusce
vitae leo vitae dolor porta lobortis. Pellentesque consectetur sem quis dolor rhoncus volutpat.
Pellentesque ullamcorper volutpat condimentum. Cras venenatis elit a vehicula feugiat. Maecenas interdum
enim justo, eu tincidunt nibh pharetra at. Cras porta dui ut accumsan aliquam. Nam ut ultricies dolor.
Ut in libero vitae nibh accumsan maximus. Duis hendrerit ligula ligula, vehicula ullamcorper purus
cursus vel. Mauris efficitur vulputate elit id fermentum. Nam turpis lorem, fermentum et ullamcorper ut,
ultrices aliquam felis. Morbi et faucibus justo.
</p>
<p>
Morbi a nisi nibh. Aenean ut ex ut justo cursus venenatis. Aliquam erat volutpat. Phasellus posuere
libero sed enim pretium maximus. Curabitur risus nunc, feugiat nec libero sed, fermentum semper turpis.
Pellentesque vestibulum scelerisque ipsum quis porttitor. Proin dapibus tortor sed odio dignissim, a
maximus turpis fringilla.
</p>
<p>
Aenean blandit nulla lorem, sodales rutrum leo dignissim ac. Aliquam ornare nulla mi, eget mollis enim
rhoncus eu. Donec quis erat sem. Sed in orci quis lectus interdum efficitur. Fusce non fringilla orci.
Pellentesque elementum ut leo at aliquet. Mauris fringilla, nisi in imperdiet consectetur, lorem tellus
iaculis eros, quis ullamcorper justo est sit amet felis. In felis justo, mattis nec elit vel, convallis
accumsan libero. Nulla facilisi. Suspendisse faucibus, magna a hendrerit tempor, elit elit posuere nibh,
non maximus mi enim ac justo. Mauris ut tempor leo. Etiam finibus imperdiet pellentesque. Vestibulum
massa arcu, iaculis vitae leo eu, fringilla semper metus. Etiam nunc dolor, consectetur eu justo non,
feugiat viverra est. Aliquam tristique molestie purus, quis ultrices est ullamcorper eu. Etiam cursus
elit et rhoncus viverra.
</p>
<p>
Donec et porttitor lacus. Suspendisse venenatis sapien ut ultrices ornare. Quisque dignissim, tortor ut
fringilla dignissim, est orci lobortis odio, ac egestas sem odio dapibus lorem. Vestibulum in magna
fermentum, laoreet metus sit amet, molestie lacus. Ut quam felis, dignissim non gravida non, vulputate
quis sapien. Sed sit amet tellus sem. Fusce eget ligula et enim feugiat vulputate. Duis nec orci id
lectus accumsan fermentum vel ac dolor. Aliquam vel accumsan augue, at consequat nibh. Ut vulputate
sapien augue, ullamcorper accumsan magna luctus id. In congue et dui sed tempus. Sed et venenatis dui,
et viverra sem. Nam elementum, libero sed lobortis pellentesque, nulla dolor tincidunt odio, in
ullamcorper orci magna eu nulla.
</p>
<p>
Etiam vitae pharetra erat. Pellentesque ullamcorper a urna vel gravida. Maecenas dapibus erat sed metus
ultrices, venenatis pulvinar felis bibendum. Morbi sollicitudin pretium rhoncus. Proin tempor pretium
feugiat. Ut sed ornare eros. Etiam sed ullamcorper justo. Integer tristique ornare lobortis. Aenean et
vestibulum sapien. Aliquam non interdum diam. Pellentesque egestas sagittis nibh, eu feugiat leo
vestibulum eget. Proin bibendum ligula sapien, ut dapibus turpis sodales sed. Sed vitae nunc feugiat,
porta diam non, fringilla arcu. Suspendisse eleifend arcu vitae mi auctor suscipit. Lorem ipsum dolor
sit amet, consectetur adipiscing elit. Integer ut odio vel ipsum luctus maximus.
</p>
<p>
Nunc rhoncus dui nibh, ac pulvinar risus vulputate in. Quisque gravida convallis ultricies. In vitae
felis dolor. Aenean congue vitae libero ac mattis. Quisque tincidunt arcu id mi bibendum, non viverra mi
vulputate. Sed mollis commodo elementum. Vivamus iaculis urna at ante tempor, eu aliquet elit bibendum.
Praesent feugiat lacinia dolor, sit amet semper enim auctor nec.
</p>
<p>
Vivamus tincidunt turpis id nulla maximus commodo. Nullam a ligula ut risus congue tempus in et ipsum.
Fusce dapibus a elit vitae sagittis. Nam metus quam, pretium a tortor eu, molestie hendrerit lacus. Cras
volutpat lacinia sem, non volutpat tellus pretium sagittis. Morbi a viverra diam, vel feugiat nisi. Orci
varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras varius nibh ut
tempor ullamcorper. Cras convallis, massa sit amet mollis laoreet, elit erat scelerisque arcu, ac
bibendum augue dolor vel nunc. Aliquam sit amet orci nec dui venenatis imperdiet.
</p>
<p>
Nulla facilisi. Aliquam vehicula velit in aliquam auctor. Cras ut nibh diam. Donec vitae vulputate
lacus. Suspendisse potenti. Morbi quis leo quis sapien sagittis finibus in id purus. Sed congue
fringilla purus, vel finibus leo aliquet eu. Cras volutpat sapien imperdiet erat consectetur interdum.
Nam eu pretium diam, quis mattis lacus.
</p>
<p>
Donec sed ligula nunc. Donec fermentum tempor diam, malesuada cursus ante iaculis at. Aliquam sodales
lectus in lorem molestie commodo. Ut aliquet faucibus commodo. Curabitur ut nisi sit amet orci molestie
ultrices at ac diam. Morbi nec tellus nec enim lobortis faucibus quis non tellus. Nulla varius ligula in
dolor fringilla vulputate. Phasellus sed finibus justo, nec faucibus ipsum. Cras at tempor mi. Donec
ipsum ligula, ultrices in suscipit non, cursus vel est. In vitae neque condimentum, euismod nunc at,
pulvinar est. Donec rhoncus luctus ornare.
</p>
</content>
<script>
const frameEl = document.getElementById('frame');
frameEl.addEventListener('load', function () {
const caseEls = document.getElementsByClassName('section');
for (let caseEl of caseEls) {
console.log(caseEl.dataset.title);
const toSelect = typeof caseEl.dataset.title == 'undefined' ? null : frameEl.contentWindow.getIdForTitle(caseEl.dataset.title);
// navItemEl.hash url-encodes
// let targetEl = document.getElementById(navItemEl.attributes.href.value.substr(1));
// let wrapperEl = targetEl.parentNode;
let intersectionObserver = new IntersectionObserver(function (entries) {
console.log(entries);
// If intersectionRatio is 0, the target is out of view
// and we do not need to do anything.
if (entries[0].intersectionRatio <= 0) {
// navItemEl.classList.remove('active');
} else {
if(toSelect === null) {
frameEl.contentWindow.mapGraph.deselectNode();
frameEl.contentWindow.mapGraph.resetZoom();
} else {
const node = frameEl.contentWindow.mapGraph.graph.nodes.filter(n => n.id == toSelect)[0]
frameEl.contentWindow.mapGraph.selectNode(node);
}
// navItemEl.classList.add('active');
}
});
// start observing
intersectionObserver.observe(caseEl);
}
})
// frame.contentWindow;
</script>
</body>
</html>