Compare commits

...

4 Commits

Author SHA1 Message Date
zzz
850bbb6c84 change size to 1600, regenerate mercator data
handle rotation offset
change base map to one with country borders, courtesy drzed via cartosvg
2024-06-23 14:28:26 -04:00
zzz
f3f6ce83b2 use polyline for tunnels 2024-06-22 17:17:16 -04:00
zzz
4ff293c9ee Simpler base map
courtesy drzed via cartosvg.com
2024-06-21 13:00:13 -04:00
zzz
308f60caa6 Draft: WIP: Console: Add world map with locations of routers and tunnels
license info for map image and mercator data to follow
2024-06-20 12:05:01 -04:00
7 changed files with 718 additions and 1 deletions

View File

@ -0,0 +1,406 @@
package net.i2p.router.web.helpers;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.data.router.RouterInfo;
import net.i2p.router.RouterContext;
import net.i2p.router.TunnelInfo;
import net.i2p.router.TunnelManagerFacade;
import net.i2p.router.web.ContextHelper;
import net.i2p.router.tunnel.pool.TunnelPool;
import net.i2p.util.Log;
import net.i2p.util.ObjectCounterUnsafe;
/**
* Generate a transparent image to overlay the world map in a Web Mercator format.
*
* Also contains commented-out code to generate the mercator.txt file.
*
* @since 0.9.xx
*/
public class MapMaker {
private final RouterContext _context;
private final Log _log;
private static final Map<String, Mercator> _mercator = new HashMap<String, Mercator>(256);
static {
readMercatorFile();
}
private static final String LATLONG_DEFAULT = "latlong.csv";
private static final String MERCATOR_DEFAULT = "mercator.txt";
private static final String BASEMAP_DEFAULT = "mapbase72.png";
private static final int WIDTH = 1600;
private static final int HEIGHT = 1600;
private static final int MAP_HEIGHT = 828;
// offsets from mercator to image.
// left side at 171.9 degrees (rotated 36 pixels)
// tweak to make it line up, eyeball Taiwan
private static final int IMG_X_OFF = -34;
// We crop the top from 85 degrees down to about 75 degrees (283 pixels)
// We crop the bottom from 85 degrees down to about 57 degrees (489 pixels)
private static final int IMG_Y_OFF = -283;
// center text on the spot
private static final int TEXT_Y_OFF = 5;
private static final Color TEXT_COLOR = new Color(255, 0, 0);
private static final String FONT_NAME = "Dialog";
private static final int FONT_STYLE = Font.BOLD;
private static final int FONT_SIZE = 12;
private static final Color CIRCLE_BORDER_COLOR = new Color(192, 0, 0, 192);
private static final Color CIRCLE_COLOR = new Color(160, 0, 0, 128);
private static final double CIRCLE_SIZE_FACTOR = 4.0;
private static final int MIN_CIRCLE_SIZE = 5;
private static final Color SQUARE_BORDER_COLOR = new Color(0, 0, 0);
private static final Color SQUARE_COLOR = new Color(255, 50, 255, 160);
private static final Color EXPL_COLOR = new Color(255, 100, 0);
private static final Color CLIENT_COLOR = new Color(255, 160, 160);
private static final Color TRANSPARENT = new Color(0, 0, 0, 0);
/**
*
*/
public MapMaker() {
this(ContextHelper.getContext(null));
}
/**
*
*/
public MapMaker(RouterContext ctx) {
_context = ctx;
_log = ctx.logManager().getLog(MapMaker.class);
}
/*
private static class LatLong {
public final float lat, lon;
public LatLong(float lat, float lon) {
this.lat = lat; this.lon = lon;
}
}
*/
private static class Mercator {
public final int x, y;
public Mercator(int x, int y) {
this.x = x; this.y = y;
}
@Override
public int hashCode() {
return x + y;
}
@Override
public boolean equals(Object o) {
Mercator m = (Mercator) o;
return x == m.x && y == m.y;
}
}
/*
private static class DummyImageObserver implements ImageObserver {
public boolean imageUpdate(Image imgs, int infoflags, int x, int y, int width, int height) { return false; }
}
*/
/**
* @param mode ignored for now, could be different for tunnels or routers or specific subsets
*/
public boolean render(int mode, OutputStream out) throws IOException {
if (_mercator.isEmpty()) {
_log.warn("mercator file not found");
return false;
}
/*
// Putting the map and the overlay in the same image makes it large.
// Now we load the map separately and overlay the circles and lines with CSS.
// map source https://github.com/mfeldheim/hermap
InputStream is = MapMaker.class.getResourceAsStream("/net/i2p/router/web/resources/" + BASEMAP_DEFAULT);
if (is == null) {
_log.warn("base map not found");
return false;
}
*/
ObjectCounterUnsafe<String> countries = new ObjectCounterUnsafe<String>();
for (RouterInfo ri : _context.netDb().getRouters()) {
Hash key = ri.getIdentity().getHash();
String country = _context.commSystem().getCountry(key);
if (country != null)
countries.increment(country);
}
Set<String> counts = countries.objects();
//BufferedImage bi = ImageIO.read(is);
//is.close();
BufferedImage bi = new BufferedImage(WIDTH, MAP_HEIGHT, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = bi.createGraphics();
//g.drawImage(base, 0, 0, new DummyImageObserver());
Font large = new Font(FONT_NAME, FONT_STYLE, FONT_SIZE);
g.setFont(large);
g.setBackground(TRANSPARENT);
g.setPaint(TEXT_COLOR);
g.setStroke(new BasicStroke(1));
for (String c : countries.objects()) {
Mercator m = _mercator.get(c);
if (m == null)
continue;
int count = countries.count(c);
int sz = Math.max(MIN_CIRCLE_SIZE, (int) (CIRCLE_SIZE_FACTOR * Math.sqrt(count)));
drawCircle(g, rotate(m.x), m.y + IMG_Y_OFF, sz);
c = c.toUpperCase(Locale.US);
double width = getStringWidth(c, large, g);
int xoff = (int) (width / 2);
g.drawString(c.toUpperCase(Locale.US), rotate(m.x) - xoff, m.y + IMG_Y_OFF + TEXT_Y_OFF);
}
String us = _context.commSystem().getOurCountry();
if (us != null) {
Mercator mus = _mercator.get(us);
if (mus != null) {
drawSquare(g, rotate(mus.x), mus.y + IMG_Y_OFF, 24);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setStroke(new BasicStroke(2));
TunnelManagerFacade tm = _context.tunnelManager();
renderPool(g, mus, tm.getInboundExploratoryPool(), EXPL_COLOR);
renderPool(g, mus, tm.getOutboundExploratoryPool(), EXPL_COLOR);
Map<Hash, TunnelPool> pools = tm.getInboundClientPools();
// TODO skip aliases
for (TunnelPool tp : pools.values()) {
renderPool(g, mus, tp, CLIENT_COLOR);
}
pools = tm.getOutboundClientPools();
for (TunnelPool tp : pools.values()) {
renderPool(g, mus, tp, CLIENT_COLOR);
}
}
}
ImageOutputStream ios = new MemoryCacheImageOutputStream(out);
ImageIO.write(bi, "png", ios);
return true;
}
/**
* Draw circle centered on x,y with a radius given
*/
private void drawCircle(Graphics2D g, int x, int y, int radius) {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Color c = g.getColor();
g.setColor(CIRCLE_BORDER_COLOR);
g.drawArc(x - radius, y - radius, radius * 2, radius * 2, 0, 360);
g.setColor(CIRCLE_COLOR);
g.fillArc(x - radius, y - radius, radius * 2, radius * 2, 0, 360);
g.setColor(c);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
}
/**
* Draw square centered on x,y with a width/height given
*/
private void drawSquare(Graphics2D g, int x, int y, int sz) {
Color c = g.getColor();
g.setColor(SQUARE_BORDER_COLOR);
g.drawRect(x - (sz/2), y - (sz/2), sz, sz);
g.setColor(SQUARE_COLOR);
g.fillRect(x - (sz/2), y - (sz/2), sz, sz);
g.setColor(c);
}
private void renderPool(Graphics2D g, Mercator mus, TunnelPool tp, Color color) {
Color c = g.getColor();
g.setColor(color);
List<TunnelInfo> tunnels = tp.listTunnels();
List<Mercator> hops = new ArrayList<Mercator>(8);
int[] x = new int[8];
int[] y = new int[8];
for (TunnelInfo info : tunnels) {
int length = info.getLength();
if (length < 2)
continue;
boolean isInbound = info.isInbound();
// gateway first
for (int j = 0; j < length; j++) {
Mercator m;
if (isInbound && j == length - 1) {
m = mus;
} else if (!isInbound && j == 0) {
m = mus;
} else {
Hash peer = info.getPeer(j);
String country = _context.commSystem().getCountry(peer);
if (country == null)
continue;
Mercator mc = _mercator.get(country);
if (mc == null)
continue;
m = mc;
}
if (hops.isEmpty() || !m.equals(hops.get(hops.size() - 1))) {
hops.add(m);
}
}
int sz = hops.size();
if (sz > 1) {
for (int i = 0; i < sz; i++) {
Mercator m = hops.get(i);
x[i] = rotate(m.x);
y[i] = m.y + IMG_Y_OFF;
}
g.drawPolyline(x, y, sz);
}
hops.clear();
}
g.setColor(c);
}
private static double getStringWidth(String text, Font font, Graphics2D g) {
return font.getStringBounds(text, 0, text.length(), g.getFontRenderContext()).getBounds().getWidth();
}
private static int rotate(int x) {
x += IMG_X_OFF;
if (x < 0)
x += WIDTH;
return x;
}
/**
* Read in and parse the mercator country file.
* The file need not be sorted.
* This file was created from the lat/long data at
* https://developers.google.com/public-data/docs/canonical/countries_csv
* using the convertLatLongFile() method below.
*/
private static void readMercatorFile() {
InputStream is = MapMaker.class.getResourceAsStream("/net/i2p/router/web/resources/" + MERCATOR_DEFAULT);
if (is == null) {
System.out.println("Country file not found");
return;
}
BufferedReader br = null;
try {
br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
String line = null;
while ( (line = br.readLine()) != null) {
try {
if (line.charAt(0) == '#')
continue;
String[] s = DataHelper.split(line, ",", 3);
if (s.length < 3)
continue;
int x = Integer.parseInt(s[1]);
int y = Integer.parseInt(s[2]);
_mercator.put(s[0], new Mercator(x, y));
} catch (NumberFormatException nfe) {
System.out.println("Bad line " + nfe);
}
}
} catch (IOException ioe) {
System.out.println("Error reading the Country File " + ioe);
} finally {
if (is != null) try { is.close(); } catch (IOException ioe) {}
if (br != null) try { br.close(); } catch (IOException ioe) {}
}
}
/**
* Read in and parse the lat/long file.
* The file need not be sorted.
* Convert the lat/long data from
* https://developers.google.com/public-data/docs/canonical/countries_csv
* to a 1200x1200 web mercator (85 degree) format.
* latlong.csv input format: XX,lat,long,countryname (lat and long are signed floats)
* mercator.txt output format: xx,x,y (x and y are integers 0-1200, not adjusted for a cropped projection)
* Output is sorted by country code.
*/
/****
private static void convertLatLongFile() {
Map<String, LatLong> latlong = new HashMap<String, LatLong>();
InputStream is = null;
BufferedReader br = null;
try {
is = new FileInputStream(LATLONG_DEFAULT);
br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
String line = null;
while ( (line = br.readLine()) != null) {
try {
if (line.charAt(0) == '#')
continue;
String[] s = DataHelper.split(line, ",", 4);
if (s.length < 3)
continue;
String lc = s[0].toLowerCase(Locale.US);
float lat = Float.parseFloat(s[1]);
float lon = Float.parseFloat(s[2]);
latlong.put(lc, new LatLong(lat, lon));
} catch (NumberFormatException nfe) {
System.out.println("Bad line " + nfe);
}
}
} catch (IOException ioe) {
System.out.println("Error reading the Country File " + ioe);
} finally {
if (is != null) try { is.close(); } catch (IOException ioe) {}
if (br != null) try { br.close(); } catch (IOException ioe) {}
}
Map<String, Mercator> mercator = new TreeMap<String, Mercator>();
for (Map.Entry<String, LatLong> e : latlong.entrySet()) {
String c = e.getKey();
LatLong ll = e.getValue();
mercator.put(c, convert(ll));
}
for (Map.Entry<String, Mercator> e : mercator.entrySet()) {
String c = e.getKey();
Mercator m = e.getValue();
System.out.println(c + ',' + m.x + ',' + m.y);
}
}
****/
/**
* https://stackoverflow.com/questions/57322997/convert-geolocation-to-pixels-on-a-mercator-projection-image
*/
/****
private static Mercator convert(LatLong latlong) {
double rad = latlong.lat * Math.PI / 180;
double mercn = Math.log(Math.tan((Math.PI / 4) + (rad / 2)));
double x = (latlong.lon + 180d) * (WIDTH / 360d);
double y = (HEIGHT / 2d) - ((WIDTH * mercn) / (2 * Math.PI));
return new Mercator((int) Math.round(x), (int) Math.round(y));
}
public static void main(String args[]) {
convertLatLongFile();
}
****/
}

View File

@ -56,6 +56,7 @@ import net.i2p.util.Addresses;
import net.i2p.util.ConvertToHash;
import net.i2p.util.Log;
import net.i2p.util.ObjectCounterUnsafe;
import net.i2p.util.SystemVersion;
import net.i2p.util.Translate;
import net.i2p.util.VersionComparator;
@ -1021,7 +1022,15 @@ class NetDbRenderer {
// the summary table
buf.append("<table id=\"netdboverview\" border=\"0\" cellspacing=\"30\"><tr><th colspan=\"3\">");
buf.append(_t("Network Database Router Statistics"));
buf.append("</th></tr><tr><td style=\"vertical-align: top;\">");
buf.append("</th></tr>");
if (!SystemVersion.isSlow() && !_context.commSystem().isDummy()) {
// https://stackoverflow.com/questions/48474/how-do-i-position-one-image-on-top-of-another-in-html
buf.append("<tr><td class=\"mapcontainer\" colspan=\"3\">" +
"<img class=\"mapbase\" src=\"/themes/console/images/mapbase72.png\" width=\"1600\" height=\"828\">" +
"<img class=\"mapoverlay\" src=\"viewmap.jsp\" width=\"1600\" height=\"828\">" +
"</td></tr>");
}
buf.append("<tr><td style=\"vertical-align: top;\">");
// versions table
List<String> versionList = new ArrayList<String>(versions.objects());
if (!versionList.isEmpty()) {

View File

@ -4643,6 +4643,24 @@ ul#upnphelp li:last-child,
/* netdb (main section) */
.mapcontainer {
position: relative;
top: 0;
left: 0;
}
.mapbase {
position: relative;
top: 0;
left: 0;
}
.mapoverlay {
position: absolute;
top: 0;
left: 0;
}
table#netdboverview {
margin-bottom: 10px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@ -5811,6 +5811,24 @@ tt a {
/* netdb */
.mapcontainer {
position: relative;
top: 0;
left: 0;
}
.mapbase {
position: relative;
top: 0;
left: 0;
}
.mapoverlay {
position: absolute;
top: 0;
left: 0;
}
.confignav+.netdbentry {
margin-top: 10px;
}

View File

@ -0,0 +1,22 @@
<%
/*
* USE CAUTION WHEN EDITING
* Trailing whitespace OR NEWLINE on the last line will cause
* IllegalStateExceptions !!!
*
* Do not tag this file for translation.
*/
response.setContentType("image/png");
response.setHeader("Content-Disposition", "inline; filename=\"i2pmap.png\"");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Accept-Ranges", "none");
response.setHeader("Connection", "Close");
java.io.OutputStream cout = response.getOutputStream();
net.i2p.router.web.helpers.MapMaker mm = new net.i2p.router.web.helpers.MapMaker();
boolean rendered = mm.render(0, cout);
if (rendered)
cout.close();
else
response.sendError(403, "Map not available");
%>

View File

@ -0,0 +1,244 @@
ad,807,591
ae,1039,693
af,1101,639
ag,525,723
ai,520,718
al,890,599
am,1000,605
an,493,745
ao,879,850
aq,800,1321
ar,517,985
as,44,864
at,865,559
au,1395,916
aw,489,744
az,1011,605
ba,879,582
bb,535,741
bd,1202,692
be,820,539
bf,793,745
bg,913,590
bh,1025,681
bi,933,815
bj,810,758
bm,512,648
bn,1310,780
bo,517,873
br,569,864
bs,456,685
bt,1202,673
bv,815,1089
bw,910,902
by,924,516
bz,407,722
ca,327,497
cc,1231,854
cd,897,818
cf,893,771
cg,870,801
ch,837,564
ci,775,766
ck,90,897
cl,482,970
cm,855,767
cn,1263,629
co,470,780
cr,428,756
cu,454,702
cv,693,728
cx,1270,847
cy,949,633
cz,869,544
de,846,534
dj,989,747
dk,842,496
dm,527,731
do,488,715
dz,807,670
ec,453,808
ee,911,477
eg,937,676
eh,743,689
er,977,732
es,783,603
et,980,759
fi,914,447
fj,1597,875
fk,535,1070
fm,1469,767
fo,769,447
fr,810,568
ga,852,804
gb,785,503
gd,526,745
ge,993,592
gf,564,783
gg,789,546
gh,795,765
gi,776,628
gl,611,335
gm,732,740
gn,757,756
gp,524,723
gq,846,793
gr,897,611
gs,637,1090
gt,399,729
gu,1444,740
gw,733,747
gy,538,778
gz,952,653
hk,1307,698
hm,1127,1079
hn,417,732
hr,868,575
ht,479,714
hu,887,562
id,1306,804
ie,763,518
il,955,655
im,780,512
in,1151,706
io,1119,828
iq,994,643
ir,1039,648
is,715,417
it,856,595
je,791,548
jm,456,718
jo,961,657
jp,1414,627
ke,968,800
kg,1132,599
kh,1267,744
ki,50,815
km,995,853
kn,521,722
kp,1367,604
kr,1368,629
kw,1011,664
ky,442,712
kz,1097,556
la,1256,710
lb,959,640
lc,529,738
li,842,562
lk,1159,765
lr,758,771
ls,925,938
lt,906,505
lu,827,544
lv,909,491
ly,877,679
ma,768,651
mc,833,583
md,926,560
me,886,590
mg,1008,885
mh,1561,768
mk,897,596
ml,782,721
mm,1226,700
mn,1262,564
mo,1305,699
mp,1446,722
mq,529,734
mr,751,704
ms,524,725
mt,864,629
mu,1056,892
mv,1125,786
mw,952,859
mx,344,692
my,1253,781
mz,958,884
na,882,905
nc,1536,895
ne,836,720
nf,1546,935
ng,839,759
ni,421,742
nl,824,528
no,838,460
np,1174,668
nr,1542,802
nu,45,886
nz,1577,1000
om,1049,702
pa,441,762
pe,467,841
pf,136,880
pg,1440,828
ph,1341,742
pk,1108,658
pl,885,529
pm,550,563
pn,234,913
pr,504,718
ps,957,650
pt,763,609
pw,1398,767
py,540,907
qa,1027,683
re,1047,896
ro,911,570
rs,893,582
ru,1268,451
rw,933,809
sa,1000,691
sb,1512,843
sc,1047,821
sd,934,742
se,883,463
sg,1261,794
sh,755,911
si,867,568
sj,905,236
sk,888,552
sl,748,762
sm,855,582
sn,736,735
so,1005,777
sr,551,783
st,829,799
sv,405,738
sy,973,635
sz,940,922
tc,481,701
td,883,730
tf,1108,1052
tg,804,762
th,1249,729
tj,1117,612
tk,36,840
tl,1359,840
tm,1065,612
tn,842,640
to,21,896
tr,957,612
tt,528,752
tv,1590,832
tw,1338,692
tz,955,828
ua,939,554
ug,944,794
us,375,622
uy,552,953
uz,1087,598
va,855,595
vc,528,742
ve,504,771
vg,513,717
vi,512,717
vn,1281,737
vu,1542,869
wf,13,862
ws,35,862
xk,893,590
ye,1016,730
yt,1001,857
za,902,943
zm,924,859
zw,930,886