Commit 6887a4a2 authored by Marek Vavrusa's avatar Marek Vavrusa

modules: deprecated tinyweb, superseded by http

this module is superseded by http module, removing
parent 55520347
......@@ -55,4 +55,3 @@ kresd.amalg.c
libkres.amalg.c
/doc/kresd.8
/libkres.pc
/modules/tinyweb/tinyweb.h
......@@ -28,7 +28,6 @@ $(eval $(call find_lib,hiredis))
$(eval $(call find_lib,socket_wrapper))
$(eval $(call find_lib,libdnssec))
$(eval $(call find_lib,libsystemd))
$(eval $(call find_gopkg,geoip,github.com/abh/geoip))
# Find Go version and platform
GO_VERSION := $(shell $(GO) version 2>/dev/null)
......@@ -94,7 +93,6 @@ info:
$(info --------)
$(info [$(HAS_doxygen)] doxygen (doc))
$(info [$(HAS_go)] go (modules/go, Go buildmode=c-shared support))
$(info [$(HAS_geoip)] geoip (modules/tinyweb, github.com/abh/geoip))
$(info [$(HAS_libmemcached)] libmemcached (modules/memcached))
$(info [$(HAS_hiredis)] hiredis (modules/redis))
$(info [$(HAS_cmocka)] cmocka (tests/unit))
......
......@@ -49,7 +49,6 @@ There are also *optional* packages that enable specific functionality in Knot DN
"luasec_", "``trust anchors``", "TLS for Lua."
"libmemcached_", "``modules/memcached``", "To build memcached backend module."
"hiredis_", "``modules/redis``", "To build redis backend module."
"geoip_", "``modules/tinyweb``", "To build a proof-of-concept web interface (needs Go as well)."
"Go_ 1.5+", "``modules``", "Build modules written in Go."
"cmocka_", "``unit tests``", "Unit testing framework."
"Doxygen_", "``documentation``", "Generating API documentation."
......
......@@ -19,6 +19,5 @@ Knot DNS Resolver modules
.. include:: ../modules/kmemcached/README.rst
.. include:: ../modules/redis/README.rst
.. include:: ../modules/ketcd/README.rst
.. include:: ../modules/tinyweb/README.rst
.. include:: ../modules/dns64/README.rst
.. include:: ../modules/renumber/README.rst
......@@ -24,13 +24,6 @@ modules_TARGETS += ketcd \
daf
endif
# List of Golang modules
ifeq ($(HAS_go),yes)
ifeq ($(HAS_geoip),yes)
modules_TARGETS += tinyweb
endif
endif
# Make C module
define make_c_module
$(1)-install: $(DESTDIR)$(MODULEDIR)
......
.. _mod-tinyweb:
Web interface
-------------
This module provides an embedded web interface for resolver. It plots current performance in real-time,
including a feed of recent iterative queries. It also includes bindings_ to `MaxMind GeoIP`_, and presents a world map coloured by frequency of queries, so you can see where do your queries go.
The *stats* module is required for plotting query rate.
By default, it listens on ``localhost:8053``.
.. warning:: This is a proof of concept module for embedding Go, which has several drawbacks - it runs in separate threads, is relatively heavy-weight due to the nature of Go, and is opaque for other modules. Look at :ref:`http module <mod-http>` if you want to expose services over HTTP from other modules.
Examples
^^^^^^^^
.. code-block:: lua
-- Load web interface
modules = { 'tinyweb' }
-- Listen on specific address/port
modules = {
tinyweb = {
addr = 'localhost:8080', -- Custom address
geoip = '/usr/local/var/GeoIP' -- Different path to GeoIP DB
}
}
Dependencies
^^^^^^^^^^^^
It depends on Go 1.5+, `github.com/abh/geoip <bindings>`_ package.
.. code-block:: bash
$ <install> libgeoip
$ go get github.com/abh/geoip
.. _`MaxMind GeoIP`: https://www.maxmind.com/en/home
.. _bindings: https://github.com/abh/geoip
package main
/*
#include "lib/layer.h"
#include "lib/module.h"
#include "lib/utils.h"
#include <libknot/descriptor.h>
int consume(knot_layer_t *, knot_pkt_t *);
static inline const char *module_path(void)
{ return MODULEDIR; }
static inline const knot_layer_api_t *_layer(void)
{ static const knot_layer_api_t api = { .consume = &consume, }; return &api; }
*/
import "C"
import (
"os"
"runtime"
"unsafe"
"fmt"
"net"
"net/http"
"html"
"html/template"
"encoding/json"
"github.com/abh/geoip"
)
type QueryInfo struct {
Qname string
Qtype string
Addr string
Secure bool
Country string
}
// Global context
var resolver *C.struct_kr_context
// FIFO of last-seen metrics
var fifo_metrics [10] QueryInfo
var fifo_metrics_i = 0
// Geo frequency table
var geo_freq map[string] int
var geo_db *geoip.GeoIP
var geo_db6 *geoip.GeoIP
/*
* Callbacks for serving static content.
*/
func resource_path(filename string) string {
return C.GoString(C.module_path()) + "/tinyweb" + filename;
}
func serve_page(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles(resource_path("/tinyweb.tpl"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
host, err := os.Hostname()
t.Execute(w, struct {
Title string
}{
Title: "kresd @ " + host,
})
w.Header().Set("Content-Type", "text/html; charset=utf-8")
}
func serve_file(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, resource_path(html.EscapeString(r.URL.Path)))
}
/*
* Serving dynamic contents.
*/
func serve_json(w http.ResponseWriter, r *http.Request, v interface{}) {
js, err := json.Marshal(v)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(js)
}
func serve_geo(w http.ResponseWriter, r *http.Request) {
serve_json(w, r, geo_freq)
}
func serve_feed(w http.ResponseWriter, r *http.Request) {
// Walk back FIFO to preserve ordering
const nsamples = len(fifo_metrics)
var samples [nsamples] QueryInfo
for i := 0; i < nsamples; i++ {
samples[i] = fifo_metrics[(nsamples + (fifo_metrics_i - i - 1)) % nsamples]
}
serve_json(w, r, samples)
}
func serve_stats(w http.ResponseWriter, r *http.Request) {
mod_name := C.CString("stats")
defer C.free(unsafe.Pointer(mod_name))
prop_name := C.CString("list")
defer C.free(unsafe.Pointer(prop_name))
out := C.kr_module_call(resolver, mod_name, prop_name, nil)
defer C.free(unsafe.Pointer(out))
if out != nil {
fmt.Fprintf(w, C.GoString(out))
} else {
http.Error(w, "No stats module", http.StatusInternalServerError)
}
}
/*
* Module implementation.
*/
func process_sample(qname string, qtype int, addr net.IP, secure bool) {
var qtype_str [16] byte
C.knot_rrtype_to_string(C.uint16_t(qtype), (*C.char)(unsafe.Pointer(&qtype_str[0])), C.size_t(16))
// Sample NS country code
var cc string
switch len(addr) {
case 4: if (geo_db != nil) { cc, _ = geo_db.GetCountry(addr.String()) }
case 16: if (geo_db6 != nil) { cc, _ = geo_db6.GetCountry_v6(addr.String()) }
default: return
}
// Count occurences
if freq, exists := geo_freq[cc]; exists {
geo_freq[cc] = freq + 1
} else {
geo_freq[cc] = 1
}
fifo_metrics[fifo_metrics_i] = QueryInfo{qname, string(qtype_str[:]), addr.String(), secure, cc}
fifo_metrics_i = (fifo_metrics_i + 1) % len(fifo_metrics)
}
//export tinyweb_init
func tinyweb_init(module *C.struct_kr_module) int {
resolver = (*C.struct_kr_context)(module.data)
geo_freq = make(map[string]int)
return 0
}
//export tinyweb_config
func tinyweb_config(module *C.struct_kr_module, conf *C.char) int {
var err error
var config map[string] interface{}
addr := "localhost:8053"
if err = json.Unmarshal([]byte(C.GoString(conf)), &config); err != nil {
fmt.Printf("[tinyweb] %s\n", err)
} else {
if v, ok := config["addr"]; ok {
addr = v.(string)
}
if v, ok := config["geoip"]; ok {
geoip.SetCustomDirectory(v.(string))
}
}
geo_db, err = geoip.OpenTypeFlag(geoip.GEOIP_COUNTRY_EDITION, geoip.GEOIP_MEMORY_CACHE)
if err != nil {
fmt.Printf("[tinyweb] couldn't open GeoIP IPv4 Country Edition\n");
}
geo_db6, err = geoip.OpenTypeFlag(geoip.GEOIP_COUNTRY_EDITION_V6, geoip.GEOIP_MEMORY_CACHE)
if err != nil {
fmt.Printf("[tinyweb] couldn't open GeoIP IPv6 Country Edition\n");
}
// Start web interface
http.HandleFunc("/feed", serve_feed)
http.HandleFunc("/stats", serve_stats)
http.HandleFunc("/geo", serve_geo)
http.HandleFunc("/tinyweb.js", serve_file)
http.HandleFunc("/datamaps.world.min.js", serve_file)
http.HandleFunc("/topojson.js", serve_file)
http.HandleFunc("/jquery.js", serve_file)
http.HandleFunc("/epoch.css", serve_file)
http.HandleFunc("/favicon.ico", serve_file)
http.HandleFunc("/epoch.js", serve_file)
http.HandleFunc("/d3.js", serve_file)
http.HandleFunc("/", serve_page)
// @todo Not sure how to cancel this routine yet
fmt.Printf("[tinyweb] listening on %s\n", addr)
go http.ListenAndServe(addr, nil)
return 0
}
//export tinyweb_deinit
func tinyweb_deinit(module *C.struct_kr_module) int {
geo_db = nil
geo_db6 = nil
runtime.GC()
return 0
}
//export consume
func consume(ctx *C.knot_layer_t, pkt *C.knot_pkt_t) C.int {
req := (*C.struct_kr_request)(ctx.data)
qry := req.current_query
state := (C.int)(ctx.state)
if qry.flags & C.QUERY_CACHED != 0 {
return state
}
// Parse answer source address
sa := (*C.struct_sockaddr)(unsafe.Pointer(&qry.ns.addr[0]))
var ip net.IP
if sa.sa_family == C.AF_INET {
sa_v4 := (*C.struct_sockaddr_in)(unsafe.Pointer(sa))
ip = net.IP(C.GoBytes(unsafe.Pointer(&sa_v4.sin_addr), 4))
} else if sa.sa_family == C.AF_INET6 {
sa_v6 := (*C.struct_sockaddr_in6)(unsafe.Pointer(sa))
ip = net.IP(C.GoBytes(unsafe.Pointer(&sa_v6.sin6_addr), 16))
}
// Parse metadata
qname := C.knot_dname_to_str_alloc(C.knot_pkt_qname(pkt))
defer C.free(unsafe.Pointer(qname))
qtype := C.knot_pkt_qtype(pkt)
secure := (bool)(C.knot_pkt_has_dnssec(pkt))
// Sample metric
process_sample(C.GoString(qname), int(qtype), ip, secure)
return state
}
//export tinyweb_layer
func tinyweb_layer(module *C.struct_kr_module) *C.knot_layer_api_t {
return C._layer()
}
//export tinyweb_api
func tinyweb_api() C.uint32_t {
return C.KR_MODULE_API
}
func main() {}
tinyweb_SOURCES := modules/tinyweb/tinyweb.go
tinyweb_INSTALL := $(wildcard modules/tinyweb/tinyweb/*)
tinyweb_DEPEND := $(libkres)
tinyweb_LIBS := $(libkres_TARGET) $(libkres_LIBS)
$(call make_go_module,tinyweb)
jQuery is provided under MIT license <https://jquery.org/license/>
D3 under BSD license <https://github.com/mbostock/d3/blob/master/LICENSE>
Epoch under MIT license <https://github.com/epochjs/epoch/blob/master/LICENSE>
TopoJSON under BSD license <https://github.com/mbostock/topojson/blob/master/LICENSE>
DataMaps under MIT license <https://github.com/markmarkoh/datamaps/blob/master/LICENSE>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
// Country code conversion
var iso2_to_iso3 = {
"AF": "AFG", "AL": "ALB", "DZ": "DZA", "AS": "ASM", "AD": "AND", "AO": "AGO", "AI": "AIA", "AQ": "ATA", "AG": "ATG", "AR": "ARG", "AM": "ARM", "AW": "ABW", "AU": "AUS", "AT": "AUT", "AZ": "AZE", "BS": "BHS", "BH": "BHR", "BD": "BGD", "BB": "BRB", "BY": "BLR", "BE": "BEL", "BZ": "BLZ", "BJ": "BEN", "BM": "BMU", "BT": "BTN", "BO": "BOL", "BA": "BIH", "BW": "BWA", "BV": "BVT", "BR": "BRA", "IO": "IOT", "VG": "VGB", "BN": "BRN", "BG": "BGR", "BF": "BFA", "BI": "BDI", "KH": "KHM", "CM": "CMR", "CA": "CAN", "CV": "CPV", "KY": "CYM", "CF": "CAF", "TD": "TCD", "CL": "CHL", "CN": "CHN", "CX": "CXR", "CC": "CCK", "CO": "COL", "KM": "COM", "CD": "COD", "CG": "COG", "CK": "COK", "CR": "CRI", "CI": "CIV", "CU": "CUB", "CY": "CYP", "CZ": "CZE", "DK": "DNK", "DJ": "DJI", "DM": "DMA", "DO": "DOM", "EC": "ECU", "EG": "EGY", "SV": "SLV", "GQ": "GNQ", "ER": "ERI", "EE": "EST", "ET": "ETH", "FO": "FRO", "FK": "FLK", "FJ": "FJI", "FI": "FIN", "FR": "FRA", "GF": "GUF", "PF": "PYF", "TF": "ATF", "GA": "GAB", "GM": "GMB", "GE": "GEO", "DE": "DEU", "GH": "GHA", "GI": "GIB", "GR": "GRC", "GL": "GRL", "GD": "GRD", "GP": "GLP", "GU": "GUM", "GT": "GTM", "GN": "GIN", "GW": "GNB", "GY": "GUY", "HT": "HTI", "HM": "HMD", "VA": "VAT", "HN": "HND", "HK": "HKG", "HR": "HRV", "HU": "HUN", "IS": "ISL", "IN": "IND", "ID": "IDN", "IR": "IRN", "IQ": "IRQ", "IE": "IRL", "IL": "ISR", "IT": "ITA", "JM": "JAM", "JP": "JPN", "JO": "JOR", "KZ": "KAZ", "KE": "KEN", "KI": "KIR", "KP": "PRK", "KR": "KOR", "KW": "KWT", "KG": "KGZ", "LA": "LAO", "LV": "LVA", "LB": "LBN", "LS": "LSO", "LR": "LBR", "LY": "LBY", "LI": "LIE", "LT": "LTU", "LU": "LUX", "MO": "MAC", "MK": "MKD", "MG": "MDG", "MW": "MWI", "MY": "MYS", "MV": "MDV", "ML": "MLI", "MT": "MLT", "MH": "MHL", "MQ": "MTQ", "MR": "MRT", "MU": "MUS", "YT": "MYT", "MX": "MEX", "FM": "FSM", "MD": "MDA", "MC": "MCO", "MN": "MNG", "MS": "MSR", "MA": "MAR", "MZ": "MOZ", "MM": "MMR", "NA": "NAM", "NR": "NRU", "NP": "NPL", "AN": "ANT", "NL": "NLD", "NC": "NCL", "NZ": "NZL", "NI": "NIC", "NE": "NER", "NG": "NGA", "NU": "NIU", "NF": "NFK", "MP": "MNP", "NO": "NOR", "OM": "OMN", "PK": "PAK", "PW": "PLW", "PS": "PSE", "PA": "PAN", "PG": "PNG", "PY": "PRY", "PE": "PER", "PH": "PHL", "PN": "PCN", "PL": "POL", "PT": "PRT", "PR": "PRI", "QA": "QAT", "RE": "REU", "RO": "ROU", "RU": "RUS", "RW": "RWA", "SH": "SHN", "KN": "KNA", "LC": "LCA", "PM": "SPM", "VC": "VCT", "WS": "WSM", "SM": "SMR", "ST": "STP", "SA": "SAU", "SN": "SEN", "CS": "SCG", "SC": "SYC", "SL": "SLE", "SG": "SGP", "SK": "SVK", "SI": "SVN", "SB": "SLB", "SO": "SOM", "ZA": "ZAF", "GS": "SGS", "ES": "ESP", "LK": "LKA", "SD": "SDN", "SR": "SUR", "SJ": "SJM", "SZ": "SWZ", "SE": "SWE", "CH": "CHE", "SY": "SYR", "TW": "TWN", "TJ": "TJK", "TZ": "TZA", "TH": "THA", "TL": "TLS", "TG": "TGO", "TK": "TKL", "TO": "TON", "TT": "TTO", "TN": "TUN", "TR": "TUR", "TM": "TKM", "TC": "TCA", "TV": "TUV", "VI": "VIR", "UG": "UGA", "UA": "UKR", "AE": "ARE", "GB": "GBR", "UM": "UMI", "US": "USA", "UY": "URY", "UZ": "UZB", "VU": "VUT", "VE": "VEN", "VN": "VNM", "WF": "WLF", "EH": "ESH", "YE": "YEM", "ZM": "ZMB", "ZW": "ZWE",
};
// Unit conversion
function tounit(d) {
d = parseInt(d);
if (d < 1000) return d.toFixed(0);
else if (d < 1000000) return (d / 1000.0).toFixed(1) + 'K';
else return (d / 1000000.0).toFixed(1) + 'M';
}
// Set up UI and pollers
window.onload = function() {
var statsLabels = ['cached', '10ms', '100ms', '1000ms', 'slow'];
var statsHistory = [];
var now = Date.now();
for (i = 0; i < statsLabels.length; ++i) {
statsHistory.push({ label: 'Layer ' + statsLabels[i], values: [{time: now, y:0}] });
$('.stats-legend').append('<li class="l-' + statsLabels[i] + '">' + statsLabels[i]);
}
var statsChart = $('#stats').epoch({
type: 'time.area',
axes: ['right', 'bottom'],
ticks: { right: 2 },
margins: { right: 60 },
tickFormats: {
right: function(d) { return tounit(d) + ' pps'; },
bottom: function(d) { return new Date(d).toTimeString().split(' ')[0]; },
},
data: statsHistory
});
var statsPrev = null;
/* Map colour brackets. */
var colours = [
'rgb(198,219,239)',
'rgb(158,202,225)',
'rgb(107,174,214)',
'rgb(66,146,198)',
'rgb(33,113,181)',
'rgb(8,81,156)',
'rgb(8,48,107)',
];
var fills = { defaultFill: '#F5F5F5' };
for (var i in colours) {
fills['q' + i] = colours[i];
}
var map = new Datamap({
element: document.getElementById('map'),
fills: fills,
data: {},
height: 400,
geographyConfig: {
highlightOnHover: false,
borderColor: '#ccc',
borderWidth: 0.5,
popupTemplate: function(geo, data) {
return ['<div class="hoverinfo">',
'<strong>', geo.properties.name, '</strong>',
'<br>Queries: <strong>', data ? data.queries : '0', '</strong>',
'</div>'].join('');
}
},
done: function(datamap) {
datamap.svg.call(d3.behavior.zoom().on("zoom", redraw));
function redraw() {
datamap.svg.selectAll("g")
.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
}
});
/* Draw map legend */
var legendBarWidth = 30, legendBarHeight = 10, legendOffset = 150;
d3.select('#map svg').append('g').attr('class', 'map-legend');
d3.select('.map-legend').selectAll('.map-legend')
.data(colours).enter()
.append('rect')
.attr('y', function(d,i) { return legendOffset + legendBarHeight*i; })
.attr('width', legendBarWidth)
.attr('height', legendBarHeight)
.attr('fill', function(d){ return d; });
/* Realtime updates */
function poller(feed, interval, cb) {
var func = function() {
$.ajax({
url: feed,
type: 'get',
dataType: 'json',
success: cb
});
}
setInterval(func, interval);
func();
}
poller('stats', 1000, function(resp) {
var now = Date.now();
var next = [];
for (i = 0; i < statsLabels.length; ++i) {
next.push(resp['answer.' + statsLabels[i]]);
}
if (statsPrev) {
var delta = [];
for (i = 0; i < statsLabels.length; ++i) {
delta.push({time: now, y: next[i]-statsPrev[i]});
}
statsChart.push(delta);
}
statsPrev = next;
});
poller('feed', 2000, function(resp) {
var feed = $('#feed');
feed.children().remove();
feed.append('<tr><th>Type</th><th>Query</th><th>Nameserver</th><th>DNSSEC</th></tr>')
for (i = 0; i < resp.length; ++i) {
if (resp[i].Qname != "") {
var row = $('<tr />');
row.append('<td>' + resp[i].Qtype + '</td>');
row.append('<td>' + resp[i].Qname + '</td>');
row.append('<td>' + resp[i].Addr + '</td>');
if (resp[i].Secure) {
row.append('<td class="secure">SECURE</td>');
} else {
row.append('<td></td>');
}
feed.append(row);
}
}
});
poller('geo', 2000, function(resp) {
var max = 0.0;
var series = [];
/* Calculate dataset limits. */
for (var key in resp) {
if (resp.hasOwnProperty(key)) {
max = Math.max(max, resp[key]);
series.push(resp[key]);
}
}
/* Map frequency to palette. */
var dataset = {};
var quantile = d3.scale.quantile()
.domain(series)
.range(d3.range(colours.length).map(function(i) { return "q" + i; }));
for (var key in resp) {
if (resp.hasOwnProperty(key)) {
var iso3_key = iso2_to_iso3[key];
if (iso3_key) {
var val = resp[key];
dataset[iso3_key] = { queries: val, fillKey: quantile(val) };
}
}
}
map.updateChoropleth(dataset);
/* Update legend */
d3.select('.map-legend').selectAll('text').remove();
d3.select('.map-legend').selectAll('.map-legend')
.data(colours).enter()
.append('text')
.text(function(d, i) {
var range = quantile.invertExtent('q' + i);
if (parseInt(range[1]) > 0) {
return tounit(range[0]) + ' - ' + tounit(range[1]);
}
})
.attr('x', (legendBarWidth*1.25))
.attr('y', function(d, i){
return legendOffset + (legendBarHeight*0.9) + legendBarHeight*i;
})
});
}
<!DOCTYPE html>
<title>{{.Title}}</title>
<style>
body { font-family: 'Gill Sans', 'Gill Sans MT', Verdana, sans-serif; color: #555; }
h1, h2, h3 { line-height: 2em; color: #000; text-align: center; border-bottom: 1px solid #ccc; }
h1, h2, h3 { font-weight: 300; }
th { text-align: left; font-weight: normal; margin-bottom: 0.5em; }
#page { font-weight: 300; }
#page { width: 900px; margin: 0 auto; }
#stats { height: 300px; }
#stats .layer-cached .area, .l-cached { fill: #2CA02C; color: #2CA02C; }
#stats .layer-10ms .area , .l-10ms { fill: #165683; color: #165683; }
#stats .layer-100ms .area , .l-100ms { fill: #258FDA; color: #258FDA; }
#stats .layer-1000ms .area, .l-1000ms { fill: #51A5E1; color: #51A5E1; }
#stats .layer-slow .area , .l-slow { fill: #E1AC51; color: #E1AC51; }
#feed { width: 100%; }
#feed .secure { color: #74c476; }
.stats-legend { text-align: center; }
.stats-legend li { display: inline; list-style-type: none; padding-right: 20px; }
.map-legend { font-size: 10px; }
</style>
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="d3.js"></script>
<script type="text/javascript" src="epoch.js"></script>
<script type="text/javascript" src="topojson.js"></script>
<script type="text/javascript" src="datamaps.world.min.js"></script>
<script type="text/javascript" src="tinyweb.js"></script>
<link rel="icon" type="image/ico" href="favicon.ico">
<link rel="stylesheet" type="text/css" href="epoch.css">
<div id="page">
<h1>{{.Title}}</h1>
<div class="epoch" id="stats"></div>
<ul class="stats-legend"></ul>
<h2>Queried servers</h2>
<div id="map" style="position: relative;"></div>
<h2>Last queries</h2>
<table id="feed"></table>
</div>
!function(){function t(n,t){function r(t){var r,e=n.arcs[0>t?~t:t],o=e[0];return n.transform?(r=[0,0],e.forEach(function(n){r[0]+=n[0],r[1]+=n[1]})):r=e[e.length-1],0>t?[r,o]:[o,r]}function e(n,t){for(var r in n){var e=n[r];delete t[e.start],delete e.start,delete e.end,e.forEach(function(n){o[0>n?~n:n]=1}),f.push(e)}}var o={},i={},u={},f=[],c=-1;return t.forEach(function(r,e){var o,i=n.arcs[0>r?~r:r];i.length<3&&!i[1][0]&&!i[1][1]&&(o=t[++c],t[