From 4beb97fc6fc2d9b38fa06ee874a21160884e83eb Mon Sep 17 00:00:00 2001
From: Russ Handorf <rhandorf@handorf.org>
Date: Tue, 28 Feb 2023 12:57:50 -0500
Subject: [PATCH] dot graph map

---
 .../dbview/__pycache__/views.cpython-38.pyc   | Bin 15820 -> 16410 bytes
 logviewer/dbview/views.py                     |  16 ++
 logviewer/dotmap/__init__.py                  |   0
 .../__pycache__/__init__.cpython-38.pyc       | Bin 0 -> 152 bytes
 .../dotmap/__pycache__/urls.cpython-38.pyc    | Bin 0 -> 283 bytes
 .../dotmap/__pycache__/views.cpython-38.pyc   | Bin 0 -> 614 bytes
 logviewer/dotmap/admin.py                     |   3 +
 logviewer/dotmap/apps.py                      |   6 +
 logviewer/dotmap/migrations/__init__.py       |   0
 logviewer/dotmap/models.py                    |   3 +
 logviewer/dotmap/tests.py                     |   3 +
 logviewer/dotmap/urls.py                      |   8 +
 logviewer/dotmap/views.py                     |  15 +
 .../logviewer/__pycache__/urls.cpython-38.pyc | Bin 2953 -> 3037 bytes
 logviewer/logviewer/urls.py                   |   2 +
 logviewer/static/dot_map_panel.html           | 270 ++++++++++++++++++
 logviewer/static/dynamic.js                   |   2 +
 logviewer/static/js/kismet.ui.dotmap.js       |  30 ++
 18 files changed, 358 insertions(+)
 create mode 100644 logviewer/dotmap/__init__.py
 create mode 100644 logviewer/dotmap/__pycache__/__init__.cpython-38.pyc
 create mode 100644 logviewer/dotmap/__pycache__/urls.cpython-38.pyc
 create mode 100644 logviewer/dotmap/__pycache__/views.cpython-38.pyc
 create mode 100644 logviewer/dotmap/admin.py
 create mode 100644 logviewer/dotmap/apps.py
 create mode 100644 logviewer/dotmap/migrations/__init__.py
 create mode 100644 logviewer/dotmap/models.py
 create mode 100644 logviewer/dotmap/tests.py
 create mode 100644 logviewer/dotmap/urls.py
 create mode 100644 logviewer/dotmap/views.py
 create mode 100644 logviewer/static/dot_map_panel.html
 create mode 100644 logviewer/static/js/kismet.ui.dotmap.js

diff --git a/logviewer/dbview/__pycache__/views.cpython-38.pyc b/logviewer/dbview/__pycache__/views.cpython-38.pyc
index 7e183cbbd6fdb4a67527f58af4cc6d6c5c0f508c..714a6c4bc1c0ea3f18fbc52d7c9c981b53f9ddd3 100644
GIT binary patch
delta 1974
zcmbVNX>1!s6rQn7(v;Ya6DO`Ygyotfj*s;f(l}cQXjLI00a8n$w4pK`D@h%kdc7dw
z%xDWy5&S@Mwg2ExgDQR?q=H;fe^r8?2+AGqBeV_oU7(~;c=LASx(Y%SUhVGt-Zk^y
zd~f~A?Bn~X=XSSyErH)j_57X#v(I_j9o~%qrEU}4)f2sqh~rhy)n4sMwqFdw9o4Ds
zU?=A292M*N`psngAv5F)7=!G!0O9@YXpjU5d%Z<|BS6F;ST+R-doxHNA}=D`@{icQ
z^8FV2McLaBEAcxmITpYct$Qm&bx&mpVhI~K;0VgRQA@rBK$N}92O;o12#mt}eFz%T
zU3`c|+0bK-t%Q@kgndwQ)VsBZ*@q&=J`!>Eu}Hu>$vzPZ`&6W`ywA*T->|U=8~z;b
zlR>h}A<`nFdw7POg3PDq9Ej3v$wp~uDLLz1+T9$qwv^$C5&_+9JR=f3&0~nbL&TsV
zGXEFN{8^EM{JCcSoB=S~Tp%k=nD)5T+ER`uOD<U8S)LO~p5<{w;348Xr916KzO)vZ
zqp;aLG$nNhfnF|9BCp!n3unW=0?(C_jZ3ZB2lZJsOUbI)r`_06GkRdnHVJT{|EOJ-
zZmg=^rya9vmy%VxPkXwhcJyf0-n!Vo^q}mwI-q;m*K?GT9i;5I(|$sKj0K)*5-kNg
zO0ZMwtg}x5im@dd#Q-Inwj`i<(+0=Dz6lUGiWs*4Ta=S1e}{4c<?m6Bqr5{Nk?t}e
zw3WC&0C9V?M_>kr*$qMgGGBhEYhKRNOF+--#E9O0Xbm5kr(&27vmbHN4vPp9`X|^x
z1e1pY1ZQ^x->{2{Q5gfaQRWJQ9%A!cVcT&th86HL>_&lvzhJ_g7~^Bi#ZJrdV`p$n
z9xH^80lj{O*~{}W{0P7h3#Hyuk@nv(i_+|;0=gcwcUO+ryDNtQuMD-csR5D`3JkAu
zQ4fBH1$(uZR}Jr3p<<nXm}mTHW0Cil+UkDosKtLiz!p^^pDpaZRC4>ZZ8o{3WRcsi
zZD}DldH}ht{6OmM_7`K-L<2Kc547B-HPBMBNbT32Y9Tdxh`71jo9$T+%0iZ*oPKr=
z<#XoseOW_=gLLzO`IQk@q3_4I+U%(cYyUs~s_ajsgT>#!P`3Dc9%bb30Q(!k1p=#Z
zcIwL6F|w5m!ncMr`U1~f8SGQ`O+KJ(zIB^&Z(-kVR?rK{z15j=%`gwf^X5(QPV>QR
z$gJc7iE9UH9kZeE6bt7=;hH-f4~3@-_Y|iQdJz`yN0^2N&E&3_x=5K3@tPar^@280
z)G^2-gQ_$4X?u!@`@$iq(6wTDLZ1XxDmfB@uucdo&#+<@R6A?#ak=tTxnx1-ct|u}
z%p9SOM|15?y0LL^<OzCJhxFe)Jyq53>X5#cc$q#~C{LKN(S?qX6aG`%I;qR-%HM2W
olW%M6%)dbUJLwwQM%Pjo4bd*Tjz(xFTz-7IKx<u%fciZB2dO?|hX4Qo

delta 1495
zcmbVMTWb?R6rS0JwkXY|sjk5nmC)vXY11}aiy-wM6sr||(a{GZ#jFd0&bWo(qY%iG
z`qGMs4<bnL4~Y06YOAI;t+n27wYFNN>YHcIZek!J#VoV?ojGUDneWUuxiCuaop$bS
zX<0|$_hWAM(6N&@oK1?W4@@~X0OQ0`ThXuFcW(9QSJ(p)U=t$9CPfIoVfIi&*dq}&
zsG+dOg%wY?S@ID!1>=d2>{mog#0@8pvuTyEa!x@j#%63QCOw&kyh}gBVb(@*9?B4y
zJI>=G#A7^w7DR{uZ)Y<9Y&CyEBq4vYnm=iRnWz>>NEfD^m$NoX@^Hoh1txh?gn5Do
z(SisO<WZx+F7nhWlB2NMK9z{*9+G!vS1FOtsh<!v01<OAp24V4HrLA84g4(55?P$x
z`oS8`7=fH?3;lz;q;A%6ck5-FyF?auw|=*VJ4RHw*B1Lr4@TOo4j3*rpQDrvlM!XF
z{f7RSt31jG>o}|Upk=V55S!j)oqY(b0GqL`09fIwFAP?&>Vsoo&s73P5y1ApKpaN=
z5^)IeE5t#>!}5p>hY3rAEObs8B%B;2>~%)Tx9V5fsOV%nMFi&7kGaRmE^^=&$g@kN
zjHa<>o*Jj3lXtQ=KIyqE{3z(RumL|N-=>lr%~8?KyZ2MkBiDehhdF#O4{LjQgbm?l
zOl!eA*i8fyzQ=?qk>+XUU}f1A_5rsPvzG8Q$m^p@crQ=;2w=ZlnQzTX{{m)-RQnl$
zIWPJ>+4;ODdmHRZucfbFB`Fax8~|&3@CgdG=?it2cTs4_^K+rJA!(L)d!{k()!{2I
z8Sh}Fetg%cd#&R3>V_?Di7at@^&>Un#t0BMRDt{;>LL3dqwYq7GU_k1+@^J*C9*{A
z)$i1Z8Y4uoFuSqKyM%O>fKEGGM!a0uqIQ`Y9Apdc7_VG$4f<Z@TD7Md)c!yHYU~Tj
z!P4(n#Fl<n5Tkzm>>C<?1gyc?X_d2MWRL{E7k{bOHWwFD*C|yC6TNEjd{1)&?JHi=
aF4Drj?NgK#6a6lw(Lv!+iidaHq`v{KF5+bX

diff --git a/logviewer/dbview/views.py b/logviewer/dbview/views.py
index d91b507..161ef6a 100644
--- a/logviewer/dbview/views.py
+++ b/logviewer/dbview/views.py
@@ -309,6 +309,22 @@ def index(request):
         return HttpResponse(dev_string, content_type='text/json')
     elif request.path == "/eventbus/events.ws":
         return HttpResponse("[]", content_type='text/json')
+    elif request.path == "/phy/DOT/map_data.json":
+        node_list = ""
+        link_list = ""
+        dev_list = list(load_db("select cast(device as text) from devices where phyname = 'IEEE802.11' and cast(device as text) like '%dot11.device.client_map%'"))
+        for device in dev_list:
+            (dev,) = device
+            dev_json = json.loads(dev)
+            newdev = {}
+            node_list = node_list + "{ \"id\": \""+dev_json["kismet.device.base.macaddr"]+"\", \"label\": \""+dev_json["kismet.device.base.macaddr"]+"\", \"level\": 1},"
+            for device in dev_json['dot11.device']['dot11.device.client_map']:
+                node_list = node_list + "{ \"id\": \""+device+"\", \"label\": \""+device+"\", \"level\": 2},"
+                link_list = link_list + "{ \"target\": \""+device+"\", \"source\": \""+dev_json["kismet.device.base.macaddr"]+"\" , \"strength\": 0.7 },"
+        node_list = node_list[:-1]
+        link_list = link_list[:-1]
+        thang="{ \"nodes\": [" +node_list+"], \"links\": [" +link_list+"] }"
+        return HttpResponse(thang, content_type='text/json')
     elif request.path == "/phy/RUSS/map_data.json":
         min_long = 361.0
         max_long = 0.0
diff --git a/logviewer/dotmap/__init__.py b/logviewer/dotmap/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/logviewer/dotmap/__pycache__/__init__.cpython-38.pyc b/logviewer/dotmap/__pycache__/__init__.cpython-38.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..605e22a63ce7080a63df656864f667576ce36c4b
GIT binary patch
literal 152
zcmWIL<>g`kg1>JLCWGk5AOaaM0yz#qT+9L_QW%06G#UL?G8BP?5yUTB{fzwFRQ;li
z#JrUJqBQ;N%;MbC65X8q^s>y<^3)=I1S>J6I4L)=KtDb{GcU6wK3=b&@)n0pZhlH>
NPO2Tq*v~-B001}PCHMdU

literal 0
HcmV?d00001

diff --git a/logviewer/dotmap/__pycache__/urls.cpython-38.pyc b/logviewer/dotmap/__pycache__/urls.cpython-38.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..fbce56cb81fc16d126188fd5c297b38a2724adb8
GIT binary patch
literal 283
zcmYjLJ5Izf5Vhkttk};jC{hsDE3MF;fCkZ|Gt7`=@h7sg3mk%ja15^DmTC(wK(maY
zfG5qHd796g+vW0{5iUO8?>*&@EdE21*iyuX2pDj~EN?kyP=GLn6|LY5Ig}`NB}k}V
zMaNqSXE&^#9chQvJe8xK`Xm1p@-X02Of1R40Y2xp(-yDmD$QZ1UFVy9FfkM~9<qHX
zGgU-sP93^6VQ}${h5Y&teXUjREvnFK2R=Ng$01srE{*T98wT}PX^3}Lk1Crq<4c-t
StcSh9tNdTgHj82*R;O>`%tyHZ

literal 0
HcmV?d00001

diff --git a/logviewer/dotmap/__pycache__/views.cpython-38.pyc b/logviewer/dotmap/__pycache__/views.cpython-38.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..87ecb61221b098722ef9e1eefe436ca525628d49
GIT binary patch
literal 614
zcmYjO!EV$r5VajAyWOTzwXMXN8#%Dyii8jn95|J#TP_i372;`=t`n#BY-v}!C%E<>
z%8~!zD}3d|FCZZ?4hTBZJiTYn*w3D~dwUZ`@eqDh-#BC6gSa1p#0yIIgg`OHHEVgn
zdEi2eHY%c47IM&$j*D1H6@O+$qM|v=k{{H?GVT)#odTN)pT0)yPr&u2bKrOXsLK^N
zJgUgRzmUX2zNB=g1RHii*Dr;AM2z2wFYFb2KYb^*d@C^8@QwH+IJ-PVsRUKT515LS
zycOFhC>2xZF(#WxC2Djl=JWtEI6m_7_|5!uJ#tuLQ{`UR59C+2M#1_ZR|Xet*)RIC
z13jy;)$1LEYs>?sy0kJKLWc{y=^-2Wqy?;vvN0L<af8r0ANRKD&`Z-p=Vf1FZ6n&w
zq#s!rUhbnM-1zva)GP2i+LUT>)@b<3?34<BKFw>>LT>A_Q^ua<%f`_)kF`0!YTzT-
z{68bVWH%4%Zr0P84SoA{aN|+}CMSH#Q!(WeZVyS5<$ZNgcIRg1YGbilp|fG;e)sQk
mP2(E;+As|-j2jvUXZPSBY>uDNx2-fh4})|GLCW{}So{XyDxgXL

literal 0
HcmV?d00001

diff --git a/logviewer/dotmap/admin.py b/logviewer/dotmap/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/logviewer/dotmap/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/logviewer/dotmap/apps.py b/logviewer/dotmap/apps.py
new file mode 100644
index 0000000..673bc46
--- /dev/null
+++ b/logviewer/dotmap/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class DotmapConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'dotmap'
diff --git a/logviewer/dotmap/migrations/__init__.py b/logviewer/dotmap/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/logviewer/dotmap/models.py b/logviewer/dotmap/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/logviewer/dotmap/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/logviewer/dotmap/tests.py b/logviewer/dotmap/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/logviewer/dotmap/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/logviewer/dotmap/urls.py b/logviewer/dotmap/urls.py
new file mode 100644
index 0000000..4c9189f
--- /dev/null
+++ b/logviewer/dotmap/urls.py
@@ -0,0 +1,8 @@
+from django.urls import path
+
+from . import views
+
+urlpatterns = [
+    path('', views.index, name='index'),
+]
+
diff --git a/logviewer/dotmap/views.py b/logviewer/dotmap/views.py
new file mode 100644
index 0000000..33c4107
--- /dev/null
+++ b/logviewer/dotmap/views.py
@@ -0,0 +1,15 @@
+from django.shortcuts import render
+from django.http import HttpResponse, HttpRequest
+import os
+
+def index(request):
+    #print("===")
+    #print(request)
+    #print("====")
+    if request.method == 'POST':
+        for key, value in request.POST.items():
+            print(key,value)
+
+    load_file = open('static/'+request.path, mode='rb')
+    if request.path == "/dot_map_panel.html":
+        return HttpResponse(load_file, content_type='text/html')
diff --git a/logviewer/logviewer/__pycache__/urls.cpython-38.pyc b/logviewer/logviewer/__pycache__/urls.cpython-38.pyc
index 693196dc72df57ba329e2c1f556435cba101f6d5..6300a7dd2ca94289fe4eaa3ed1a6a8daa77763fc 100644
GIT binary patch
delta 218
zcmeAazbmdC%FD~e00g-?f0H};7#JRdILN>N$Z-JT;((3XQjCmplY<zqCv!G4HZ!HN
zrWmHOq`=rlNNi&u+XSk{6iM6+N!%QXZ2@Fkg4Cz$MscNB1v6+`Z_Z%KWD!rvFNx1h
zEQl{i%uCJD%P7gssp1BTZ9dL+fQdu2Afr;>#Xn?o1?PH3#-zy_+zFEdxLFu40|1>B
BH!J`E

delta 157
zcmcaB-YKpf%FD~e00i5n+)5VXV_<j;;vfS(AjbiSi!C;4OEL0tHZwLerLv~TrLv@O
zPIhCwZf^(`F#?G|*~UONOxy&CZ3<+Yfz+gHM{%W?2Qz3|Y;Iu6WZBHczKv<~b<PEh
NjB%4IxDyz!0syrYBqIO-

diff --git a/logviewer/logviewer/urls.py b/logviewer/logviewer/urls.py
index aee7816..0317a34 100644
--- a/logviewer/logviewer/urls.py
+++ b/logviewer/logviewer/urls.py
@@ -37,6 +37,7 @@ urlpatterns = [
     path('css/images/<str:loadfile>', include('kiscontent.urls')),
     path('adsb_map_panel.html', include('adsbmap.urls')),
     path('russ_map_panel.html', include('russmap.urls')),
+    path('dot_map_panel.html', include('dotmap.urls')),
     path('system/status.json', include('dbview.urls')),
     path('alerts/wrapped/last-time/0/alerts.json', include('dbview.urls')),
     path('messagebus/last-time/0/messages.json', include('dbview.urls')),
@@ -56,6 +57,7 @@ urlpatterns = [
     path('devices/views/phy-IEEE802.11/devices.json', include('dbview.urls')),
     path('phy/ADSB/map_data.json', include('dbview.urls')),
     path('phy/RUSS/map_data.json', include('dbview.urls')),
+    path('phy/DOT/map_data.json', include('dbview.urls')),
     path('devices/by-key/<str:devicename>/device.json', include('devices.urls')),
     path('devices/multikey/as-object/devices.json', include('dbview.urls')),
     path('datasource/by-uuid/<str:devicename>/source.json', include('devices.urls')),
diff --git a/logviewer/static/dot_map_panel.html b/logviewer/static/dot_map_panel.html
new file mode 100644
index 0000000..bd8eb9f
--- /dev/null
+++ b/logviewer/static/dot_map_panel.html
@@ -0,0 +1,270 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>dot map</title>
+
+    <script src="js/jquery-3.1.0.min.js"></script>
+    <script src="js/chart.umd.js"></script>
+    <script src="js/js.storage.min.js"></script>
+    <script src="js/kismet.ui.theme.js"></script>
+
+    <link rel="stylesheet" type="text/css" href="css/font-awesome.min.css">
+    <link rel="stylesheet" href="css/leaflet.css" />
+    <link rel="stylesheet" type="text/css" href="css/jquery.jspanel.min.css" />
+    <link rel="stylesheet" type="text/css" href="css/Control.Loading.css" />
+
+    <script src="js/leaflet.js"></script>
+    <script src="js/Leaflet.MultiOptionsPolyline.min.js"></script>
+    <script src="js/Control.Loading.js"></script>
+    <script src="js/chroma.min.js"></script>
+
+    <script src="js/js.storage.min.js"></script>
+    <script src="js/kismet.utils.js"></script>
+    <script src="js/kismet.units.js"></script>
+
+    <script src="js/datatables.min.js"></script>
+    <script src="js/dataTables.scrollResize.js"></script>
+
+    <script src="https://d3js.org/d3.v4.min.js"></script>
+
+    <style>
+
+    </style>
+</head>
+<body>
+    <div id="warning" class="warning">
+        <p><b>Warning!</b>
+        <p>To display the Russ map, your browser will connect to the Leaflet and Open Street Map servers to fetch the map tiles.  This requires you have a functional Internet connection, and will reveal something about your location (the bounding region where planes have been seen.)
+        <p><input id="dontwarn" type="checkbox">Don't warn me again</input>
+        <p><button id="continue">Continue</button>
+    </div>
+    <div id="map"><svg style="width: 100%; height: 600px;"></svg></div>
+
+    <script>
+
+function getNeighbors(node) {
+  return links.reduce(function (neighbors, link) {
+      if (link.target.id === node.id) {
+        neighbors.push(link.source.id)
+      } else if (link.source.id === node.id) {
+        neighbors.push(link.target.id)
+      }
+      return neighbors
+    },
+    [node.id]
+  )
+}
+
+function isNeighborLink(node, link) {
+  return link.target.id === node.id || link.source.id === node.id
+}
+
+
+function getNodeColor(node, neighbors) {
+  if (Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1) {
+    return node.level === 1 ? 'blue' : 'green'
+  }
+
+  return node.level === 1 ? 'red' : 'gray'
+}
+
+function getLinkColor(node, link) {
+  return isNeighborLink(node, link) ? 'green' : '#E5E5E5'
+}
+
+function getTextColor(node, neighbors) {
+  return Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1 ? 'green' : 'white'
+}
+
+function selectNode(selectedNode) {
+  var neighbors = getNeighbors(selectedNode)
+
+  // we modify the styles to highlight selected nodes
+  nodeElements.attr('fill', function (node) { return getNodeColor(node, neighbors) })
+  textElements.attr('fill', function (node) { return getTextColor(node, neighbors) })
+  linkElements.attr('stroke', function (link) { return getLinkColor(selectedNode, link) })
+}
+
+        var window_visible = true;
+/*
+        // Visibility detection from https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
+        // Set the name of the hidden property and the change event for visibility
+        var hidden, visibilityChange; 
+        if (typeof document.hidden !== "undefined") { // Opera 12.10 and Firefox 18 and later support 
+            hidden = "hidden";
+            visibilityChange = "visibilitychange";
+        } else if (typeof document.msHidden !== "undefined") {
+            hidden = "msHidden";
+            visibilityChange = "msvisibilitychange";
+        } else if (typeof document.webkitHidden !== "undefined") {
+            hidden = "webkitHidden";
+            visibilityChange = "webkitvisibilitychange";
+        }
+
+        function handleVisibilityChange() {
+            if (document[hidden]) {
+                window_visible = false;
+            } else {
+                window_visible = true;
+            }
+        }
+
+        // Warn if the browser doesn't support addEventListener or the Page Visibility API
+        if (typeof document.addEventListener === "undefined" || hidden === undefined) {
+            ; // Do nothing
+        } else {
+            // Handle page visibility change   
+            document.addEventListener(visibilityChange, handleVisibilityChange, false);
+        }
+*/
+        var urlparam = new URL(window.location.href);
+        var param_url = urlparam.searchParams.get('parent_url') + "/";
+        var param_prefix = urlparam.searchParams.get('local_uri_prefix', "");
+        var KISMET_PROXY_PREFIX = urlparam.searchParams.get('KISMET_PROXY_PREFIX', "");
+
+        if (param_prefix == 0)
+            param_prefix=""
+
+        var local_uri_prefix = param_url + param_prefix;
+        if (typeof(KISMET_URI_PREFIX) !== 'undefined')
+            local_uri_prefix = KISMET_URI_PREFIX;
+
+        var map_configured = false;
+
+        var markers = {};
+
+        var tid = -1;
+
+        var map = null;
+
+        function map_cb(d) {
+            data = kismet.sanitizeObject(d);
+	    
+            map_configured = true;
+	    var nodes = d['nodes']
+            var links = d['links']
+    
+            var width = window.innerWidth
+            var height = window.innerHeight
+
+            var svg = d3.select('svg')
+		.append("g")
+            svg.attr('width', width).attr('height', height)
+              .call(d3.zoom().on("zoom", function () {
+                    svg.attr("transform", d3.event.transform)
+               }))
+              //.append("g")
+
+            // simulation setup with all forces
+            var linkForce = d3
+              .forceLink()
+              .id(function (link) { return link.id })
+              .strength(function (link) { return link.strength })
+
+            var simulation = d3
+              .forceSimulation()
+              //.force('link', linkForce)
+              .force('charge', d3.forceManyBody().strength(-10))
+              .force('center', d3.forceCenter(width / 2, height / 2))
+              .force('link', linkForce)
+
+            var dragDrop = d3.drag().on('start', function (node) {
+              node.fx = node.x
+              node.fy = node.y
+            }).on('drag', function (node) {
+              simulation.alphaTarget(0.7).restart()
+              node.fx = d3.event.x
+              node.fy = d3.event.y
+            }).on('end', function (node) {
+              if (!d3.event.active) {
+                simulation.alphaTarget(0)
+              }
+              node.fx = null
+              node.fy = null
+            })
+
+            var linkElements = svg.append("g")
+              .attr("class", "links")
+              .selectAll("line")
+              .data(links)
+              .enter().append("line")
+                .attr("stroke-width", 1)
+                      .attr("stroke", "rgba(255, 255, 255, 5.0)")
+
+            var nodeElements = svg.append("g")
+              .attr("class", "nodes")
+              .selectAll("circle")
+              .data(nodes)
+              .enter().append("circle")
+                .attr("r", 10)
+                .attr("fill", getNodeColor)
+                .call(dragDrop)
+                .on('click', selectNode)
+
+            var textElements = svg.append("g")
+              .attr("class", "texts")
+              .selectAll("text")
+              .data(nodes)
+              .enter().append("text")
+                .text(function (node) { return  node.label })
+                      .attr("font-size", 15)
+                      .attr("dx", 15)
+                .attr("dy", 4)
+
+            simulation.nodes(nodes).on('tick', () => {
+              nodeElements
+                .attr('cx', function (node) { return node.x })
+                .attr('cy', function (node) { return node.y })
+              textElements
+                .attr('x', function (node) { return node.x })
+                .attr('y', function (node) { return node.y })
+              linkElements
+                .attr('x1', function (link) { return link.source.x })
+                .attr('y1', function (link) { return link.source.y })
+                .attr('x2', function (link) { return link.target.x })
+                .attr('y2', function (link) { return link.target.y })
+            })
+
+            simulation.force("link").links(links)
+        }
+
+        var load_maps = kismet.getStorage('kismet.russ.maps_ok', false);
+
+        function poll_map() {
+            if (window_visible && !$('#map').is(':hidden') && load_maps) {
+                $.get(local_uri_prefix + KISMET_PROXY_PREFIX + "phy/DOT/map_data.json")
+                    .done(function(d) {
+			console.log("sending to function");
+                        map_cb(d);
+                    });
+                    //.always(function(d) {
+                    //    tid = setTimeout(function() { poll_map(); }, 2000);
+                    //});
+            } else {
+                tid = setTimeout(function() { poll_map(); }, 2000);
+            }
+        }
+
+        // Set a global timeout
+        $.ajaxSetup({
+            timeout:5000,
+            xhrFields: {
+                withCredentials: true
+            }
+        });
+
+        if (load_maps)
+            $('#warning').hide();
+
+        $('#continue').on('click', function() {
+            if ($('#dontwarn').is(":checked"))
+                kismet.putStorage('kismet.russ.maps_ok', true);
+            $('#warning').hide();
+            load_maps = true;
+        });
+
+        poll_map();
+
+    </script>
+</body>
+</html>
diff --git a/logviewer/static/dynamic.js b/logviewer/static/dynamic.js
index 8c386ea..1edfd01 100644
--- a/logviewer/static/dynamic.js
+++ b/logviewer/static/dynamic.js
@@ -11,6 +11,7 @@ var kismet_ui_rtl433;
 var kismet_ui_uav;
 var kismet_ui_zwave;
 var kismet_ui_russmap;
+var kismet_ui_dotmap;
 async function load_dynamics() {
 kismet_ui_adsb = await import(`${local_uri_prefix}js/kismet.ui.adsb.js`);
 kismet_ui_bluetooth = await import(`${local_uri_prefix}js/kismet.ui.bluetooth.js`);
@@ -22,4 +23,5 @@ kismet_ui_rtl433 = await import(`${local_uri_prefix}js/kismet.ui.rtl433.js`);
 kismet_ui_uav = await import(`${local_uri_prefix}js/kismet.ui.uav.js`);
 kismet_ui_zwave = await import(`${local_uri_prefix}js/kismet.ui.zwave.js`);
 kismet_ui_russmap = await import(`${local_uri_prefix}js/kismet.ui.russmap.js`);
+kismet_ui_dotmap = await import(`${local_uri_prefix}js/kismet.ui.dotmap.js`);
 }
diff --git a/logviewer/static/js/kismet.ui.dotmap.js b/logviewer/static/js/kismet.ui.dotmap.js
new file mode 100644
index 0000000..82ec5f8
--- /dev/null
+++ b/logviewer/static/js/kismet.ui.dotmap.js
@@ -0,0 +1,30 @@
+
+"use strict";
+
+var local_uri_prefix = ""; 
+if (typeof(KISMET_URI_PREFIX) !== 'undefined')
+    local_uri_prefix = KISMET_URI_PREFIX;
+
+kismet_ui_tabpane.AddTab({
+    id: 'dotmap',
+    tabTitle: 'Relationship Map',
+    expandable: false,
+    createCallback: function(div) {
+        var url = new URL(parent.document.URL);
+        url.searchParams.append('parent_url', url.origin)
+        url.searchParams.append('local_uri_prefix', local_uri_prefix);
+        url.searchParams.append('KISMET_PROXY_PREFIX', KISMET_PROXY_PREFIX);
+        url.pathname = `${local_uri_prefix}${KISMET_PROXY_PREFIX}dot_map_panel.html`;
+
+        div.append(
+            $('<iframe>', {
+                width: '100%',
+                height: '100%',
+                src: url.href,
+            })
+        );
+    },
+    priority: -100,
+
+}, 'center');
+
-- 
2.34.1