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
|
||||
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):
|
||||
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.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.io.Serializable;
|
||||
import java.io.Writer;
|
||||
import java.math.BigInteger; // debug
|
||||
@ -58,6 +61,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;
|
||||
|
||||
@ -1027,6 +1031,7 @@ class NetDbRenderer {
|
||||
/**
|
||||
* @param mode 0: charts only; 1: full routerinfos; 2: abbreviated routerinfos
|
||||
* 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 {
|
||||
if (!_context.netDb().isInitialized()) {
|
||||
@ -1135,7 +1140,28 @@ 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()) {
|
||||
// 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
|
||||
List<String> versionList = new ArrayList<String>(versions.objects());
|
||||
if (!versionList.isEmpty()) {
|
||||
@ -1239,6 +1265,30 @@ class NetDbRenderer {
|
||||
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
|
||||
* @param code two-letter country code
|
||||
|
@ -42,7 +42,11 @@
|
||||
response.setHeader("X-Frame-Options", "SAMEORIGIN");
|
||||
// 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
|
||||
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-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" %>
|
||||
<%=intl.title("network database")%>
|
||||
<%@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>
|
||||
<%@include file="summary.jsi" %>
|
||||
<h1><%=intl._t("I2P Network Database")%></h1>
|
||||
|
@ -4666,6 +4666,15 @@ table#netdboverview {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#geomap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
path.mapoverlaytunnel:hover {
|
||||
stroke: #ff33cc;
|
||||
stroke-width: 7;
|
||||
}
|
||||
|
||||
#netdboverview td {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -5933,6 +5933,15 @@ table#leasesetsummary th a:hover {
|
||||
padding: 8px 5px 8px 32px;
|
||||
}
|
||||
|
||||
#geomap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
path.mapoverlaytunnel:hover {
|
||||
stroke: #ff33cc;
|
||||
stroke-width: 7;
|
||||
}
|
||||
|
||||
#netdblookup th {
|
||||
text-transform: uppercase;
|
||||
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