forked from I2P_Developers/i2p.i2p
Merge branch 'netdb-map' into 'master'
Console: Add world map with locations of routers and tunnels See merge request i2p-hackers/i2p.i2p!234
This commit is contained in:
@ -316,6 +316,10 @@ Applications:
|
|||||||
Copyright (c) 2013-2023 David J. Bradshaw
|
Copyright (c) 2013-2023 David J. Bradshaw
|
||||||
See licenses/LICENSE-Iframe-resizer.txt
|
See licenses/LICENSE-Iframe-resizer.txt
|
||||||
|
|
||||||
|
Router Console country coordinates:
|
||||||
|
Adapted from Google Dataset Publishing Language to convert to Mercator
|
||||||
|
CC BY 4.0 https://creativecommons.org/licenses/by/4.0/
|
||||||
|
|
||||||
SAM (sam.jar):
|
SAM (sam.jar):
|
||||||
Public domain.
|
Public domain.
|
||||||
|
|
||||||
|
@ -0,0 +1,610 @@
|
|||||||
|
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.rrd4j.SimpleSVGGraphics2D;
|
||||||
|
import static net.i2p.rrd4j.SimpleSVGGraphics2D.*;
|
||||||
|
import net.i2p.rrd4j.SimpleSVGMaker;
|
||||||
|
import net.i2p.router.RouterContext;
|
||||||
|
import net.i2p.router.TunnelInfo;
|
||||||
|
import net.i2p.router.TunnelManagerFacade;
|
||||||
|
import net.i2p.router.tunnel.HopConfig;
|
||||||
|
import net.i2p.router.tunnel.pool.TunnelPool;
|
||||||
|
import net.i2p.router.web.ContextHelper;
|
||||||
|
import net.i2p.router.web.Messages;
|
||||||
|
import net.i2p.util.Log;
|
||||||
|
import net.i2p.util.ObjectCounterUnsafe;
|
||||||
|
import net.i2p.util.Translate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 final Map<Object, Object> hints = new HashMap<Object, Object>(4);
|
||||||
|
|
||||||
|
private static final Map<String, Mercator> _mercator = new HashMap<String, Mercator>(256);
|
||||||
|
|
||||||
|
static {
|
||||||
|
readMercatorFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String MERCATOR_DEFAULT = "mercator.txt";
|
||||||
|
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 34 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;
|
||||||
|
private static final Color TEXT_COLOR = new Color(20, 20, 20);
|
||||||
|
private static final String FONT_NAME = "Dialog";
|
||||||
|
private static final int FONT_STYLE = Font.BOLD;
|
||||||
|
private static final int FONT_SIZE = 16;
|
||||||
|
// center text on the spot
|
||||||
|
private static final int TEXT_Y_OFF = (FONT_SIZE / 2) - 2;
|
||||||
|
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 = 2.75;
|
||||||
|
private static final int MIN_CIRCLE_SIZE = 7;
|
||||||
|
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 CLIENT_COLOR = new Color(255, 100, 0);
|
||||||
|
private static final Color EXPL_COLOR = new Color(255, 160, 160);
|
||||||
|
private static final Color PART_COLOR = new Color(255, 0, 100);
|
||||||
|
private static final Color ANIMATE_COLOR = new Color(100, 0, 255);
|
||||||
|
private static final Color TRANSPARENT = new Color(0, 0, 0, 0);
|
||||||
|
private static final BasicStroke STROKE = new BasicStroke(1);
|
||||||
|
private static final BasicStroke STROKE2 = new BasicStroke(3);
|
||||||
|
private static final int MODE_ROUTERS = 1;
|
||||||
|
private static final int MODE_EXPL = 2;
|
||||||
|
private static final int MODE_CLIENT = 4;
|
||||||
|
private static final int MODE_PART = 8;
|
||||||
|
private static final int MODE_ANIM = 16;
|
||||||
|
private static final int MODE_FF = 32;
|
||||||
|
private static final int MODE_DEFAULT = MODE_ROUTERS | MODE_EXPL | MODE_CLIENT | MODE_ANIM;
|
||||||
|
|
||||||
|
private int tunnelCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public MapMaker() {
|
||||||
|
this(ContextHelper.getContext(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public MapMaker(RouterContext ctx) {
|
||||||
|
_context = ctx;
|
||||||
|
_log = ctx.logManager().getLog(MapMaker.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mode bitmask, 0 for default, see MODE definitions above
|
||||||
|
*/
|
||||||
|
public boolean render(int mode, OutputStream out) throws IOException {
|
||||||
|
if (_mercator.isEmpty()) {
|
||||||
|
_log.warn("mercator file not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.write(render(mode).getBytes("UTF-8"));
|
||||||
|
out.flush();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mode see above
|
||||||
|
*/
|
||||||
|
public String render(int mode) throws IOException {
|
||||||
|
if (mode == 0)
|
||||||
|
mode = MODE_DEFAULT;
|
||||||
|
ObjectCounterUnsafe<String> countries = new ObjectCounterUnsafe<String>();
|
||||||
|
if ((mode & (MODE_ROUTERS | MODE_FF)) != 0) {
|
||||||
|
boolean ff = (mode & MODE_FF) != 0;
|
||||||
|
for (RouterInfo ri : _context.netDb().getRouters()) {
|
||||||
|
if (ff && ri.getCapabilities().indexOf('f') < 0)
|
||||||
|
continue;
|
||||||
|
Hash key = ri.getIdentity().getHash();
|
||||||
|
String country = _context.commSystem().getCountry(key);
|
||||||
|
if (country != null)
|
||||||
|
countries.increment(country);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return render(mode, countries);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mode see above
|
||||||
|
*/
|
||||||
|
public String render(int mode, ObjectCounterUnsafe<String> countries) throws IOException {
|
||||||
|
// only for string widths
|
||||||
|
Graphics2D gg = new SimpleSVGGraphics2D(1, 1);
|
||||||
|
StringBuilder buf = new StringBuilder(32768);
|
||||||
|
SimpleSVGMaker g = new SimpleSVGMaker(buf);
|
||||||
|
g.startSVG(WIDTH, MAP_HEIGHT, TRANSPARENT, "mapoverlaysvg", null);
|
||||||
|
Font large = new Font(FONT_NAME, FONT_STYLE, FONT_SIZE);
|
||||||
|
|
||||||
|
buf.append("<a href=\"/netdb?f=16\">");
|
||||||
|
g.drawText(_t("Routers"), 25, 700, TEXT_COLOR, large, null, hints);
|
||||||
|
buf.append("</a>\n");
|
||||||
|
buf.append("<a href=\"/netdb?f=512\">");
|
||||||
|
g.drawText(_t("Floodfills"), 25, 725, TEXT_COLOR, large, null, hints);
|
||||||
|
buf.append("</a>\n");
|
||||||
|
buf.append("<a href=\"/netdb?f=32\">");
|
||||||
|
g.drawText(_t("Exploratory Tunnels"), 25, 750, TEXT_COLOR, large, null, hints);
|
||||||
|
buf.append("</a>\n");
|
||||||
|
buf.append("<a href=\"/netdb?f=64\">");
|
||||||
|
g.drawText(_t("Client Tunnels"), 25, 775, TEXT_COLOR, large, null, hints);
|
||||||
|
buf.append("</a>\n");
|
||||||
|
buf.append("<a href=\"/netdb?f=128\">");
|
||||||
|
g.drawText(_t("Participating Tunnels"), 25, 800, TEXT_COLOR, large, null, hints);
|
||||||
|
buf.append("</a>\n");
|
||||||
|
|
||||||
|
if ((mode & (MODE_ROUTERS | MODE_FF)) != 0) {
|
||||||
|
for (String c : countries.objects()) {
|
||||||
|
Mercator m = _mercator.get(c);
|
||||||
|
if (m == null)
|
||||||
|
continue;
|
||||||
|
int count = countries.count(c);
|
||||||
|
String title = getTranslatedCountry(c) + ": " + ngettext("{0} router", "{0} routers", count);
|
||||||
|
|
||||||
|
hints.put(KEY_ELEMENT_ID, "mapoverlaytext-" + c);
|
||||||
|
hints.put(KEY_ELEMENT_CLASS, "mapoverlaytext dynamic");
|
||||||
|
//hints.put(KEY_ELEMENT_TITLE, title);
|
||||||
|
c = c.toUpperCase(Locale.US);
|
||||||
|
double width = getStringWidth(c, large, gg);
|
||||||
|
int xoff = (int) (width / 2);
|
||||||
|
g.drawText(c.toUpperCase(Locale.US), rotate(m.x) - xoff, m.y + IMG_Y_OFF + TEXT_Y_OFF, TEXT_COLOR, large, null, hints);
|
||||||
|
hints.clear();
|
||||||
|
|
||||||
|
// put the circle on top of the text so it captures the title
|
||||||
|
int sz = Math.max(MIN_CIRCLE_SIZE, (int) (CIRCLE_SIZE_FACTOR * Math.sqrt(count)));
|
||||||
|
// add count to ID so it will be replaced on change by ajaxchanges
|
||||||
|
hints.put(KEY_ELEMENT_ID, "mapoverlaycircle-" + c + '-' + count);
|
||||||
|
hints.put(KEY_ELEMENT_CLASS, "mapoverlaycircle dynamic");
|
||||||
|
hints.put(KEY_ELEMENT_TITLE, title);
|
||||||
|
drawCircle(g, rotate(m.x), m.y + IMG_Y_OFF, sz);
|
||||||
|
hints.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String us = _context.commSystem().getOurCountry();
|
||||||
|
if (us != null) {
|
||||||
|
Mercator mus = _mercator.get(us);
|
||||||
|
if (mus != null) {
|
||||||
|
hints.put(KEY_ELEMENT_ID, "mapoverlaysquare-me");
|
||||||
|
hints.put(KEY_ELEMENT_CLASS, "mapoverlaysquare");
|
||||||
|
hints.put(KEY_ELEMENT_TITLE, _t("My router"));
|
||||||
|
drawSquare(g, rotate(mus.x), mus.y + IMG_Y_OFF, 24);
|
||||||
|
if ((mode & (MODE_EXPL | MODE_CLIENT)) != 0) {
|
||||||
|
TunnelManagerFacade tm = _context.tunnelManager();
|
||||||
|
tunnelCount = 0;
|
||||||
|
if ((mode & MODE_EXPL) != 0) {
|
||||||
|
renderPool(mode, g, mus, tm.getInboundExploratoryPool(), EXPL_COLOR);
|
||||||
|
renderPool(mode, g, mus, tm.getOutboundExploratoryPool(), EXPL_COLOR);
|
||||||
|
}
|
||||||
|
if ((mode & MODE_CLIENT) != 0) {
|
||||||
|
Map<Hash, TunnelPool> pools = tm.getInboundClientPools();
|
||||||
|
for (TunnelPool tp : pools.values()) {
|
||||||
|
if (tp.getSettings().getAliasOf() != null)
|
||||||
|
continue;
|
||||||
|
renderPool(mode, g, mus, tp, CLIENT_COLOR);
|
||||||
|
}
|
||||||
|
pools = tm.getOutboundClientPools();
|
||||||
|
for (TunnelPool tp : pools.values()) {
|
||||||
|
if (tp.getSettings().getAliasOf() != null)
|
||||||
|
continue;
|
||||||
|
renderPool(mode, g, mus, tp, CLIENT_COLOR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((mode & MODE_PART) != 0) {
|
||||||
|
ObjectCounterUnsafe<String> tunnels = new ObjectCounterUnsafe<String>();
|
||||||
|
List<HopConfig> participating = _context.tunnelDispatcher().listParticipatingTunnels();
|
||||||
|
for (int i = 0; i < participating.size(); i++) {
|
||||||
|
HopConfig cfg = participating.get(i);
|
||||||
|
Hash from = cfg.getReceiveFrom();
|
||||||
|
Hash to = cfg.getSendTo();
|
||||||
|
String c = null;
|
||||||
|
if (from != null) {
|
||||||
|
c = _context.commSystem().getCountry(from);
|
||||||
|
if (c != null)
|
||||||
|
tunnels.increment(c);
|
||||||
|
}
|
||||||
|
if (to != null) {
|
||||||
|
String d = _context.commSystem().getCountry(to);
|
||||||
|
// only count once if to and from same country
|
||||||
|
if (d != null && !d.equals(c))
|
||||||
|
tunnels.increment(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderParticipating(g, mus, tunnels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.endSVG();
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw circle centered on x,y with a radius given
|
||||||
|
*/
|
||||||
|
private void drawCircle(SimpleSVGMaker g, int x, int y, int radius) {
|
||||||
|
g.drawCircle(x, y, radius, CIRCLE_BORDER_COLOR, CIRCLE_COLOR, STROKE, null, hints);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw square centered on x,y with a width/height given
|
||||||
|
*/
|
||||||
|
private void drawSquare(SimpleSVGMaker g, int x, int y, int sz) {
|
||||||
|
g.drawSquare(x, y, sz, SQUARE_BORDER_COLOR, SQUARE_COLOR, STROKE, null, hints);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @param mode see above
|
||||||
|
*/
|
||||||
|
private void renderPool(int mode, SimpleSVGMaker g, Mercator mus, TunnelPool tp, Color color) {
|
||||||
|
boolean isInbound = tp.getSettings().isInbound();
|
||||||
|
boolean isExpl = tp.getSettings().isExploratory();
|
||||||
|
// shift to 4 corners of box
|
||||||
|
int off = 12;
|
||||||
|
if (isExpl) {
|
||||||
|
if (isInbound)
|
||||||
|
mus = new Mercator(mus.x - off, mus.y - off);
|
||||||
|
else
|
||||||
|
mus = new Mercator(mus.x + off, mus.y - off);
|
||||||
|
} else {
|
||||||
|
if (isInbound)
|
||||||
|
mus = new Mercator(mus.x - off, mus.y + off);
|
||||||
|
else
|
||||||
|
mus = new Mercator(mus.x + off, mus.y + off);
|
||||||
|
}
|
||||||
|
List<TunnelInfo> tunnels = tp.listTunnels();
|
||||||
|
List<Mercator> hops = new ArrayList<Mercator>(8);
|
||||||
|
String nick = isExpl ? null : tp.getSettings().getDestinationNickname();
|
||||||
|
int[] x = new int[8];
|
||||||
|
int[] y = new int[8];
|
||||||
|
for (TunnelInfo info : tunnels) {
|
||||||
|
int length = info.getLength();
|
||||||
|
if (length < 2)
|
||||||
|
continue;
|
||||||
|
StringBuilder cbuf = new StringBuilder(16);
|
||||||
|
if (!isInbound)
|
||||||
|
cbuf.append("(me)");
|
||||||
|
// 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 {
|
||||||
|
if (cbuf.length() > 0)
|
||||||
|
cbuf.append("->"); // SVGMaker will escape
|
||||||
|
Hash peer = info.getPeer(j);
|
||||||
|
String country = _context.commSystem().getCountry(peer);
|
||||||
|
if (country == null) {
|
||||||
|
cbuf.append('?');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cbuf.append(country.toUpperCase(Locale.US));
|
||||||
|
Mercator mc = _mercator.get(country);
|
||||||
|
if (mc == null)
|
||||||
|
continue;
|
||||||
|
m = mc;
|
||||||
|
}
|
||||||
|
if (hops.isEmpty() || !m.equals(hops.get(hops.size() - 1))) {
|
||||||
|
hops.add(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isInbound)
|
||||||
|
cbuf.append("->(me)"); // SVGMaker will escape
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
long tid = isInbound ? info.getReceiveTunnelId(length - 1).getTunnelId()
|
||||||
|
: info.getSendTunnelId(0).getTunnelId();
|
||||||
|
String svgid = "mapoverlaytunnel-" + tid;
|
||||||
|
String title;
|
||||||
|
if (isInbound) {
|
||||||
|
if (isExpl)
|
||||||
|
title = _t("Inbound exploratory tunnel");
|
||||||
|
else
|
||||||
|
title = _t("Inbound client tunnel");
|
||||||
|
} else {
|
||||||
|
if (isExpl)
|
||||||
|
title = _t("Outbound exploratory tunnel");
|
||||||
|
else
|
||||||
|
title = _t("Outbound client tunnel");
|
||||||
|
}
|
||||||
|
if (nick != null)
|
||||||
|
title += " (" + nick + ')';
|
||||||
|
title += " " + tid + " " + cbuf;
|
||||||
|
hints.put(KEY_ELEMENT_ID, svgid);
|
||||||
|
hints.put(KEY_ELEMENT_CLASS, "mapoverlaytunnel dynamic");
|
||||||
|
hints.put(KEY_ELEMENT_TITLE, title);
|
||||||
|
g.drawPolyline(x, y, sz, color, STROKE2, null, hints);
|
||||||
|
hints.clear();
|
||||||
|
if ((mode & MODE_ANIM) != 0) {
|
||||||
|
hints.put(KEY_ELEMENT_ID, "mapoverlayanim-" + tid);
|
||||||
|
hints.put(KEY_ELEMENT_CLASS, "mapoverlayanim dynamic");
|
||||||
|
// 3 hops is 10 sec
|
||||||
|
String anim = "<animateMotion dur=\"" + String.format(Locale.US, "%.1f", (sz - 1) * 3.3f) + "s\" repeatCount=\"2\" " +
|
||||||
|
"onbegin=\"beginCircleAnim('mapoverlayanim-" + tid + "')\" " +
|
||||||
|
"onend=\"endCircleAnim('mapoverlayanim-" + tid + "')\" " +
|
||||||
|
// wait for line drawing animation to stop,
|
||||||
|
// and spread out the start times
|
||||||
|
"begin=\"" + (5000 + (250 * (++tunnelCount))) + "ms\" " +
|
||||||
|
" >\n" +
|
||||||
|
" <mpath href=\"#" + svgid + "\" />\n" +
|
||||||
|
" </animateMotion>";
|
||||||
|
hints.put(KEY_ELEMENT_INNERSVG, anim);
|
||||||
|
// place them off-screen until animation starts
|
||||||
|
// the js will reset to (0,0) on begin, and remove them on end
|
||||||
|
drawCircle(g, -5, 0, 5);
|
||||||
|
hints.clear();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_log.shouldDebug())
|
||||||
|
_log.debug("Can't draw tunnel path " + cbuf);
|
||||||
|
}
|
||||||
|
hops.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* One line to each country, width = number of tunnels
|
||||||
|
*/
|
||||||
|
private void renderParticipating(SimpleSVGMaker g, Mercator mus, ObjectCounterUnsafe<String> tunnels) {
|
||||||
|
int usx = rotate(mus.x);
|
||||||
|
int usy = mus.y + IMG_Y_OFF;
|
||||||
|
int off = 12;
|
||||||
|
for (String c : tunnels.objects()) {
|
||||||
|
Mercator m = _mercator.get(c);
|
||||||
|
if (m == null)
|
||||||
|
continue;
|
||||||
|
if (m.equals(mus))
|
||||||
|
continue;
|
||||||
|
int count = tunnels.count(c);
|
||||||
|
hints.put(KEY_ELEMENT_ID, "part-" + c + '-' + count);
|
||||||
|
hints.put(KEY_ELEMENT_CLASS, "mapoverlaytunnel dynamic");
|
||||||
|
String title = getTranslatedCountry(c) + ": " + ngettext("{0} participating tunnel", "{0} participating tunnels", count);
|
||||||
|
hints.put(KEY_ELEMENT_TITLE, title);
|
||||||
|
// shift to 4 corners of box
|
||||||
|
int mx, my;
|
||||||
|
int tx = rotate(m.x);
|
||||||
|
int ty = m.y + IMG_Y_OFF;
|
||||||
|
if (tx > usx) {
|
||||||
|
tx -= off;
|
||||||
|
mx = usx + off;
|
||||||
|
} else {
|
||||||
|
tx += off;
|
||||||
|
mx = usx - off;
|
||||||
|
}
|
||||||
|
if (ty > usy) {
|
||||||
|
ty -= off;
|
||||||
|
my = usy + off;
|
||||||
|
} else {
|
||||||
|
ty += off;
|
||||||
|
my = usy - off;
|
||||||
|
}
|
||||||
|
g.drawLine(tx, ty, mx, my, PART_COLOR,
|
||||||
|
new BasicStroke(Math.max(3, Math.min(30, 3 * count / 2))),
|
||||||
|
null, hints);
|
||||||
|
hints.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Countries now in a separate bundle
|
||||||
|
* @param code two-letter country code
|
||||||
|
*/
|
||||||
|
private String getTranslatedCountry(String code) {
|
||||||
|
String name = _context.commSystem().getCountryName(code);
|
||||||
|
return Translate.getString(name, _context, Messages.COUNTRY_BUNDLE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** translate a string */
|
||||||
|
private String _t(String s) {
|
||||||
|
return Messages.getString(s, _context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** translate a string */
|
||||||
|
private String ngettext(String s, String p, int n) {
|
||||||
|
return Messages.getString(n, s, p, _context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Following is code to convert the latlong.csv file from Google
|
||||||
|
// to our mercator.txt file which is bundled in the war.
|
||||||
|
|
||||||
|
/****
|
||||||
|
private static final String LATLONG_DEFAULT = "latlong.csv";
|
||||||
|
|
||||||
|
private static class LatLong {
|
||||||
|
public final float lat, lon;
|
||||||
|
public LatLong(float lat, float lon) {
|
||||||
|
this.lat = lat; this.lon = lon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
****/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 1600x1600 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();
|
||||||
|
}
|
||||||
|
****/
|
||||||
|
}
|
@ -10,6 +10,9 @@ package net.i2p.router.web.helpers;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.Reader;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
import java.math.BigInteger; // debug
|
import java.math.BigInteger; // debug
|
||||||
@ -58,6 +61,7 @@ import net.i2p.util.Addresses;
|
|||||||
import net.i2p.util.ConvertToHash;
|
import net.i2p.util.ConvertToHash;
|
||||||
import net.i2p.util.Log;
|
import net.i2p.util.Log;
|
||||||
import net.i2p.util.ObjectCounterUnsafe;
|
import net.i2p.util.ObjectCounterUnsafe;
|
||||||
|
import net.i2p.util.SystemVersion;
|
||||||
import net.i2p.util.Translate;
|
import net.i2p.util.Translate;
|
||||||
import net.i2p.util.VersionComparator;
|
import net.i2p.util.VersionComparator;
|
||||||
|
|
||||||
@ -1027,6 +1031,7 @@ class NetDbRenderer {
|
|||||||
/**
|
/**
|
||||||
* @param mode 0: charts only; 1: full routerinfos; 2: abbreviated routerinfos
|
* @param mode 0: charts only; 1: full routerinfos; 2: abbreviated routerinfos
|
||||||
* mode 3: Same as 0 but sort countries by count
|
* mode 3: Same as 0 but sort countries by count
|
||||||
|
* Codes greater than 16 are map codes * 16
|
||||||
*/
|
*/
|
||||||
public void renderStatusHTML(Writer out, int pageSize, int page, int mode) throws IOException {
|
public void renderStatusHTML(Writer out, int pageSize, int page, int mode) throws IOException {
|
||||||
if (!_context.netDb().isInitialized()) {
|
if (!_context.netDb().isInitialized()) {
|
||||||
@ -1135,7 +1140,28 @@ class NetDbRenderer {
|
|||||||
// the summary table
|
// the summary table
|
||||||
buf.append("<table id=\"netdboverview\" border=\"0\" cellspacing=\"30\"><tr><th colspan=\"3\">");
|
buf.append("<table id=\"netdboverview\" border=\"0\" cellspacing=\"30\"><tr><th colspan=\"3\">");
|
||||||
buf.append(_t("Network Database Router Statistics"));
|
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()) {
|
||||||
|
// svg inline part 1
|
||||||
|
out.append(buf);
|
||||||
|
buf.setLength(0);
|
||||||
|
buf.append("<tr><td id=\"mapcontainer\" colspan=\"3\">");
|
||||||
|
boolean ok = embedResource(buf, "mapbase75p1.svg");
|
||||||
|
if (ok) {
|
||||||
|
out.append(buf);
|
||||||
|
buf.setLength(0);
|
||||||
|
// overlay
|
||||||
|
MapMaker mm = new MapMaker(_context);
|
||||||
|
out.write(mm.render(mode >> 4));
|
||||||
|
// svg inline part 2
|
||||||
|
embedResource(buf, "mapbase75p2.svg");
|
||||||
|
buf.append("</td></tr>");
|
||||||
|
out.append(buf);
|
||||||
|
}
|
||||||
|
buf.setLength(0);
|
||||||
|
}
|
||||||
|
mode &= 0x0f;
|
||||||
|
buf.append("<tr><td style=\"vertical-align: top;\">");
|
||||||
// versions table
|
// versions table
|
||||||
List<String> versionList = new ArrayList<String>(versions.objects());
|
List<String> versionList = new ArrayList<String>(versions.objects());
|
||||||
if (!versionList.isEmpty()) {
|
if (!versionList.isEmpty()) {
|
||||||
@ -1239,6 +1265,30 @@ class NetDbRenderer {
|
|||||||
out.flush();
|
out.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return success
|
||||||
|
* @since 0.9.66
|
||||||
|
*/
|
||||||
|
private boolean embedResource(StringBuilder buf, String rsc) {
|
||||||
|
InputStream is = this.getClass().getResourceAsStream("/net/i2p/router/web/resources/" + rsc);
|
||||||
|
if (is == null)
|
||||||
|
return false;
|
||||||
|
Reader br = null;
|
||||||
|
try {
|
||||||
|
br = new InputStreamReader(is, "UTF-8");
|
||||||
|
char[] c = new char[4096];
|
||||||
|
int read;
|
||||||
|
while ( (read = br.read(c)) >= 0) {
|
||||||
|
buf.append(c, 0, read);
|
||||||
|
}
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
if (br != null) try { br.close(); } catch (IOException ioe) {}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Countries now in a separate bundle
|
* Countries now in a separate bundle
|
||||||
* @param code two-letter country code
|
* @param code two-letter country code
|
||||||
|
@ -42,7 +42,11 @@
|
|||||||
response.setHeader("X-Frame-Options", "SAMEORIGIN");
|
response.setHeader("X-Frame-Options", "SAMEORIGIN");
|
||||||
// unsafe-inline is a fallback for browsers not supporting nonce
|
// unsafe-inline is a fallback for browsers not supporting nonce
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src
|
||||||
response.setHeader("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'nonce-" + cspNonce + "'; form-action 'self'; frame-ancestors 'self'; object-src 'none'; media-src 'none'");
|
// we need unsafe-inline for the /netdb SVG
|
||||||
|
if ("/netdb.jsp".equals(request.getServletPath()))
|
||||||
|
response.setHeader("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; form-action 'self'; frame-ancestors 'self'; object-src 'none'; media-src 'none'");
|
||||||
|
else
|
||||||
|
response.setHeader("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'nonce-" + cspNonce + "'; form-action 'self'; frame-ancestors 'self'; object-src 'none'; media-src 'none'");
|
||||||
}
|
}
|
||||||
response.setHeader("X-XSS-Protection", "1; mode=block");
|
response.setHeader("X-XSS-Protection", "1; mode=block");
|
||||||
response.setHeader("X-Content-Type-Options", "nosniff");
|
response.setHeader("X-Content-Type-Options", "nosniff");
|
||||||
|
88
apps/routerconsole/jsp/js/ajaxchanges.js
Normal file
88
apps/routerconsole/jsp/js/ajaxchanges.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/* @license http://creativecommons.org/publicdomain/zero/1.0/legalcode CC0-1.0 */
|
||||||
|
|
||||||
|
// This component is dedicated to the public domain. It uses the CC0
|
||||||
|
// as a formal dedication to the public domain and in circumstances where
|
||||||
|
// a public domain is not usable.
|
||||||
|
|
||||||
|
var __ajaxchanges_fails = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Add/remove elements of changeclass that are inside target.
|
||||||
|
* All elements of the changeclass must have unique ids.
|
||||||
|
* All elements with the same id are assumed unchanged.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function ajaxchanges(url, target, changeclass, refresh) {
|
||||||
|
// native XMLHttpRequest object
|
||||||
|
if (window.XMLHttpRequest) {
|
||||||
|
var req = new XMLHttpRequest();
|
||||||
|
req.onreadystatechange = function() {ajaxchangesDone(req, url, target, changeclass, refresh);};
|
||||||
|
req.open("GET", url, true);
|
||||||
|
req.setRequestHeader("If-Modified-Since","Sat, 1 Jan 2000 00:00:00 GMT");
|
||||||
|
req.send(null);
|
||||||
|
} else if (window.ActiveXObject) {
|
||||||
|
var req = new ActiveXObject("Microsoft.XMLDOM");
|
||||||
|
if (req) {
|
||||||
|
req.onreadystatechange = function() {ajaxchangesDone(target);};
|
||||||
|
req.open("GET", url, true);
|
||||||
|
req.setRequestHeader("If-Modified-Since","Sat, 1 Jan 2000 00:00:00 GMT");
|
||||||
|
req.send(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Add/remove elements of changeclass that are inside target.
|
||||||
|
* All elements of the changeclass must have unique ids.
|
||||||
|
* All elements with the same id are assumed unchanged. TODO another class for changes
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function ajaxchangesDone(req, url, target, changeclass, refresh) {
|
||||||
|
if (req.readyState == 4) {
|
||||||
|
if (req.status == 200) {
|
||||||
|
__ajax_fails = 0;
|
||||||
|
const results = req.responseXML;
|
||||||
|
const oldtgt = document.getElementById(target);
|
||||||
|
const newtgt = results.getElementById(target);
|
||||||
|
const oldelements = oldtgt.getElementsByClassName(changeclass);
|
||||||
|
const newelements = newtgt.getElementsByClassName(changeclass);
|
||||||
|
//var added = 0;
|
||||||
|
//var removed = 0;
|
||||||
|
// remove old ones not in new
|
||||||
|
for (var i = 0; i < oldelements.length; i++) {
|
||||||
|
let e = oldelements[i];
|
||||||
|
let id = e.id;
|
||||||
|
let e2 = newtgt.getElementById(id);
|
||||||
|
if (e2 == null) {
|
||||||
|
//console.warn("Removing " + id);
|
||||||
|
e.remove();
|
||||||
|
//removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// add new ones not in old
|
||||||
|
for (var i = 0; i < newelements.length; i++) {
|
||||||
|
let e = newelements[i];
|
||||||
|
let id = e.id;
|
||||||
|
let e2 = oldtgt.getElementById(id);
|
||||||
|
if (e2 == null) {
|
||||||
|
//console.warn("Adding " + id);
|
||||||
|
oldtgt.appendChild(e);
|
||||||
|
//added++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//console.warn("Added " + added + " removed " + removed);
|
||||||
|
} else if (__ajaxchanges_fails == 0) {
|
||||||
|
__ajaxchanges_fails++;
|
||||||
|
} else {
|
||||||
|
document.getElementById(target).innerHTML = failMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refresh > 0) {
|
||||||
|
setTimeout(function() {ajaxchanges(url, target, changeclass, refresh);}, refresh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* @license-end */
|
54
apps/routerconsole/jsp/js/map.js
Normal file
54
apps/routerconsole/jsp/js/map.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/* */
|
||||||
|
|
||||||
|
function initMap() {
|
||||||
|
drawTunnels();
|
||||||
|
setTimeout(updateMap, 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMap() {
|
||||||
|
var url = "/viewmap.jsp";
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const f = urlParams.get('f');
|
||||||
|
if (f != null)
|
||||||
|
url += "?f=" + f;
|
||||||
|
ajaxchanges(url, "mapoverlaysvg", "dynamic", 60*1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTunnels() {
|
||||||
|
var paths = document.getElementsByClassName("mapoverlaytunnel");
|
||||||
|
for (var i = 0; i < paths.length; i++) {
|
||||||
|
drawTunnel(paths[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTunnel(path) {
|
||||||
|
// https://jakearchibald.com/2013/animated-line-drawing-svg/
|
||||||
|
var len = path.getTotalLength();
|
||||||
|
path.style.strokeDasharray = len + ' ' + len;
|
||||||
|
path.style.strokeDashoffset = len;
|
||||||
|
path.getBoundingClientRect();
|
||||||
|
path.style.transition = 'stroke-dashoffset 5s ease-in-out';
|
||||||
|
path.style.strokeDashoffset = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginCircleAnim(circleid) {
|
||||||
|
// move onscreen
|
||||||
|
let circle = document.getElementById(circleid);
|
||||||
|
if (circle != null) {
|
||||||
|
circle.setAttribute('cx', '0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function endCircleAnim(circleid) {
|
||||||
|
// remove it
|
||||||
|
let circle = document.getElementById(circleid);
|
||||||
|
if (circle != null) {
|
||||||
|
// move them offscreen, the first ajaxchanges will remove them
|
||||||
|
//circle.remove();
|
||||||
|
circle.setAttribute('cx', '-5');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
initMap();
|
||||||
|
});
|
@ -6,6 +6,8 @@
|
|||||||
<%@include file="css.jsi" %>
|
<%@include file="css.jsi" %>
|
||||||
<%=intl.title("network database")%>
|
<%=intl.title("network database")%>
|
||||||
<%@include file="summaryajax.jsi" %>
|
<%@include file="summaryajax.jsi" %>
|
||||||
|
<script src="/js/ajaxchanges.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
|
||||||
|
<script src="/js/map.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
|
||||||
</head><body>
|
</head><body>
|
||||||
<%@include file="summary.jsi" %>
|
<%@include file="summary.jsi" %>
|
||||||
<h1><%=intl._t("I2P Network Database")%></h1>
|
<h1><%=intl._t("I2P Network Database")%></h1>
|
||||||
|
@ -4666,6 +4666,15 @@ table#netdboverview {
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#geomap {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
path.mapoverlaytunnel:hover {
|
||||||
|
stroke: #ff33cc;
|
||||||
|
stroke-width: 7;
|
||||||
|
}
|
||||||
|
|
||||||
#netdboverview td {
|
#netdboverview td {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
@ -5933,6 +5933,15 @@ table#leasesetsummary th a:hover {
|
|||||||
padding: 8px 5px 8px 32px;
|
padding: 8px 5px 8px 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#geomap {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
path.mapoverlaytunnel:hover {
|
||||||
|
stroke: #ff33cc;
|
||||||
|
stroke-width: 7;
|
||||||
|
}
|
||||||
|
|
||||||
#netdblookup th {
|
#netdblookup th {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 11pt !important;
|
font-size: 11pt !important;
|
||||||
|
28
apps/routerconsole/jsp/viewmap.jsp
Normal file
28
apps/routerconsole/jsp/viewmap.jsp
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<%
|
||||||
|
/*
|
||||||
|
* 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/svg+xml");
|
||||||
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
response.setHeader("Content-Disposition", "inline; filename=\"i2pmap.svg\"");
|
||||||
|
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();
|
||||||
|
// don't include animations on updates, because the browser doesn't render them
|
||||||
|
int mode = 7;
|
||||||
|
String f = request.getParameter("f");
|
||||||
|
if (f != null)
|
||||||
|
mode = Integer.parseInt(f) >> 4;
|
||||||
|
boolean rendered = mm.render(mode, cout);
|
||||||
|
|
||||||
|
if (rendered)
|
||||||
|
cout.close();
|
||||||
|
else
|
||||||
|
response.sendError(403, "Map not available");
|
||||||
|
%>
|
244
apps/routerconsole/resources/mapbase75p1.svg
Normal file
244
apps/routerconsole/resources/mapbase75p1.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 71 KiB |
7
apps/routerconsole/resources/mapbase75p2.svg
Normal file
7
apps/routerconsole/resources/mapbase75p2.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<!-- overlay goes above here -->
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- border -->
|
||||||
|
<rect x="0" y="0" id="frame" pathLength="1" width="1600" height="828"/>
|
||||||
|
</svg>
|
251
apps/routerconsole/resources/mercator.txt
Normal file
251
apps/routerconsole/resources/mercator.txt
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
#
|
||||||
|
# Country coordinates on a square 1600x1600 Mercator projection
|
||||||
|
# This file was created from the lat/long data at
|
||||||
|
# https://developers.google.com/public-data/docs/canonical/countries_csv
|
||||||
|
# License: CC-BY 4.0
|
||||||
|
# https://creativecommons.org/licenses/by/4.0/
|
||||||
|
#
|
||||||
|
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
|
Reference in New Issue
Block a user