1064 lines
37 KiB
JavaScript
1064 lines
37 KiB
JavaScript
/*
|
|
The code below implements a map widget to navigate a zoomable globe made of
|
|
tiled images — similar to Google Maps. The catch is that the map doesn't
|
|
show the surface of the Earth, but the Internet — instead of geographical
|
|
or political boundaries, it maps the country-wise allocation of the IPv4 address
|
|
space.
|
|
|
|
Inline comments, below, are minimal — but the maps has its own "About"
|
|
section that has more information on how it was done.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
/*
|
|
Load the UI module.
|
|
*/
|
|
Ox.load('UI', function() {
|
|
|
|
/*
|
|
Create a new Ox.UI widget.
|
|
*/
|
|
Ox.IPv4Map = function(options, self) {
|
|
|
|
self = self || {};
|
|
var that = Ox.Element({}, self)
|
|
.defaults({
|
|
bookmarks: [{id: 'google.com', title: 'Google'}],
|
|
ip: '127.0.0.1',
|
|
lookupURL: '',
|
|
tilesURL: '',
|
|
zoom: 0
|
|
})
|
|
.options(options || {})
|
|
.attr({id: 'map'})
|
|
.bindEvent({
|
|
/*
|
|
Keyboard events trigger in case the element has focus.
|
|
*/
|
|
key_a: function() {
|
|
self.$panel.selectTab('about');
|
|
self.$about.trigger('click');
|
|
},
|
|
key_comma: function() { toggleOverlay('xkcd'); },
|
|
key_control_a: function() {
|
|
$('.marker').addClass('selected');
|
|
},
|
|
key_control_shift_a: function() {
|
|
$('.marker').removeClass('selected');
|
|
},
|
|
key_c: function() {
|
|
self.$panel.selectTab('controls');
|
|
self.$about.trigger('click');
|
|
},
|
|
key_delete: function() {
|
|
$('.marker.selected').remove();
|
|
},
|
|
key_dot: function() { toggleOverlay('projection'); },
|
|
key_down: function() { panBy([0, 0.5]); },
|
|
key_enter: function() { find(self.$find.options('value')); },
|
|
key_equal: function() { zoomBy(1); },
|
|
key_f: function() {
|
|
setTimeout(function() {
|
|
self.$find.focusInput();
|
|
});
|
|
},
|
|
key_i: function() { self.$about.trigger('click'); },
|
|
key_left: function() { panBy([-0.5, 0]); },
|
|
key_m: function() { self.$toggle.trigger('click'); },
|
|
key_minus: function() { zoomBy(-1); },
|
|
key_right: function() { panBy([0.5, 0]); },
|
|
key_shift_down: function() { panBy([0, 1]); },
|
|
key_shift_enter: function() { find(''); },
|
|
key_shift_equal: function() { zoomBy(2); },
|
|
key_shift_left: function() { panBy([-1, 0]); },
|
|
key_shift_minus: function() { zoomBy(-2); },
|
|
key_shift_right: function() { panBy([1, 0]); },
|
|
key_shift_up: function() { panBy([0, -1]); },
|
|
key_slash: function() { toggleOverlay('milliondollarhomepage'); },
|
|
key_up: function() { panBy([0, -0.5]); },
|
|
mousedown: function(e) {
|
|
!$(e.target).is('.OxInput') && that.gainFocus();
|
|
}
|
|
})
|
|
.gainFocus();
|
|
|
|
Ox.loop(self.maxZoom + 1, function(z) {
|
|
that.bindEvent('key_' + z, function() {
|
|
zoomTo(z);
|
|
});
|
|
});
|
|
|
|
Ox.$window.on({
|
|
hashchange: onHashchange,
|
|
resize: onResize
|
|
});
|
|
|
|
self.hashchange = true;
|
|
self.mapCenter = [
|
|
Math.floor(window.innerWidth / 2),
|
|
Math.floor(window.innerHeight / 2)
|
|
];
|
|
self.maxZoom = 8;
|
|
self.path = 'U';
|
|
self.projection = {
|
|
U: [[0, 2, 3, 1], 'DUUC'],
|
|
D: [[0, 1, 3, 2], 'UDDA'],
|
|
C: [[3, 2, 0, 1], 'ACCU'],
|
|
A: [[3, 1, 0, 2], 'CAAD']
|
|
};
|
|
self.tileAreaInIPs = Math.pow(4, 16 - self.options.zoom);
|
|
self.tileSize = 256;
|
|
self.tileSizeInXY = Math.pow(2, 16 - self.options.zoom);
|
|
self.xy = getXYByIP(self.options.ip);
|
|
self.mapSize = Math.pow(2, self.options.zoom) * self.tileSize;
|
|
|
|
/*
|
|
Tiles layer with a dynamic tooltip that reacts to mousewheel, drag and
|
|
click events.
|
|
*/
|
|
self.$tiles = Ox.Element({
|
|
tooltip: function(e) {
|
|
return $(e.target).is('.tile')
|
|
? getIPByMouseXY(e)
|
|
: '';
|
|
}
|
|
})
|
|
.attr({id: 'tiles'})
|
|
.css({
|
|
position: 'absolute',
|
|
width: self.mapSize + 'px',
|
|
height: self.mapSize + 'px'
|
|
})
|
|
.on({
|
|
mousewheel: onMousewheel
|
|
})
|
|
.bindEvent({
|
|
doubleclick: onDoubleclick,
|
|
dragstart: function(e) {
|
|
onDragstart(e, false);
|
|
},
|
|
drag: onDrag,
|
|
singleclick: onSingleclick,
|
|
})
|
|
.appendTo(that);
|
|
|
|
/*
|
|
About button that opens a dialog.
|
|
*/
|
|
self.$about = Ox.Button({
|
|
selectable: false,
|
|
title: 'IPv4 Map of the Internet',
|
|
width: 256
|
|
})
|
|
.addClass('interface')
|
|
.attr({id: 'about'})
|
|
.bindEvent({
|
|
click: function() {
|
|
if (!self.text) {
|
|
Ox.get('html/about.html', function(data) {
|
|
self.text = data;
|
|
self.$text.html(data);
|
|
self.$dialog.open();
|
|
});
|
|
} else {
|
|
self.$dialog.open();
|
|
}
|
|
}
|
|
})
|
|
.appendTo(that);
|
|
|
|
/*
|
|
The tabbed panel inside the about dialog.
|
|
*/
|
|
self.$panel = Ox.TabPanel({
|
|
content: {
|
|
about: Ox.Element()
|
|
.css({padding: '16px', overflowY: 'auto'})
|
|
.append(
|
|
self.$text = $('<div>')
|
|
.css({
|
|
position: 'absolute',
|
|
width: '256px',
|
|
textAlign: 'justify'
|
|
})
|
|
)
|
|
.append(
|
|
self.$images = $('<div>')
|
|
.css({
|
|
position: 'absolute',
|
|
left: '288px',
|
|
width: '256px'
|
|
})
|
|
),
|
|
controls: self.$controlsPanel = Ox.Element()
|
|
.css({padding: '16px', overflowY: 'auto'})
|
|
},
|
|
tabs: [
|
|
{id: 'about', title: 'About', selected: true},
|
|
{id: 'controls', title: 'Mouse & Keyboard Controls'}
|
|
]
|
|
});
|
|
|
|
/*
|
|
The images in the "About" section.
|
|
*/
|
|
[
|
|
'png/xkcd.png', 'png/projection.png', 'png/flags.png', 'png/map.png'
|
|
].forEach(function(image) {
|
|
$('<a>')
|
|
.attr({href: image, target: '_blank'})
|
|
.append(
|
|
$('<img>')
|
|
.attr({src: image})
|
|
.css({width: '256px', marginBottom: '16px'})
|
|
)
|
|
.appendTo(self.$images);
|
|
});
|
|
|
|
/*
|
|
The contents of the "Mouse & Keyboard Controls" panel.
|
|
*/
|
|
Ox.forEach({
|
|
'Mouse': {
|
|
'Click': 'Pan to position / Select marker',
|
|
'shift Click': 'Add marker to selection',
|
|
'command Click': 'Add/remove marker to/from selection',
|
|
'Doubleclick': 'Zoom to position',
|
|
'Drag': 'Pan',
|
|
'Wheel': 'Zoom to position'
|
|
},
|
|
'Keyboard': {
|
|
'arrow_left arrow_right arrow_up arrow_down':
|
|
'Pan by half the window size',
|
|
'shift+arrow_left shift+arrow_right shift+arrow_up shift+arrow_down':
|
|
'Pan by the full window size',
|
|
'0 1 2 3 4 5 6 7 8': 'Set zoom level',
|
|
'- =': 'Zoom by one level',
|
|
'shift+- shift+=': 'Zoom by two levels',
|
|
'A': 'About',
|
|
'C': 'Mouse & Keyboard Controls',
|
|
'F': 'Find',
|
|
'M': 'Toggle overview map',
|
|
', . /': 'Toggle map overlay',
|
|
'return': 'Pan to selected marker',
|
|
'shift+return': 'Pan to "Me" marker',
|
|
'control+A': 'Select all markers',
|
|
'shift+control+A': 'Deselect all markers',
|
|
'delete': 'Clear selected markers',
|
|
'control+delete': 'Clear all markers'
|
|
}
|
|
}, function(keys, section) {
|
|
self.$controlsPanel.append(
|
|
$('<div>').addClass('textTitle').html(section)
|
|
);
|
|
Ox.forEach(keys, function(value, key) {
|
|
self.$controlsPanel
|
|
.append(
|
|
$('<div>').addClass('textKey').html(
|
|
key.split(' ').map(function(key) {
|
|
return key.split('+').map(function(key) {
|
|
return Ox.SYMBOLS[key] || key;
|
|
}).join('')
|
|
}).join(' ')
|
|
)
|
|
)
|
|
.append(
|
|
$('<div>').addClass('textValue').html(value)
|
|
);
|
|
});
|
|
});
|
|
|
|
/*
|
|
The dialog itself.
|
|
*/
|
|
self.$dialog = Ox.Dialog({
|
|
buttons: [
|
|
Ox.Button({
|
|
id: 'close',
|
|
title: 'Close'
|
|
})
|
|
.bindEvent({
|
|
click: function() {
|
|
self.$dialog.close();
|
|
}
|
|
})
|
|
],
|
|
closeButton: true,
|
|
content: self.$panel,
|
|
keys: {enter: 'close', escape: 'close'},
|
|
fixedSize: true,
|
|
height: 288,
|
|
title: 'IPv4 Map of the Internet',
|
|
width: 560 + Ox.UI.SCROLLBAR_SIZE
|
|
});
|
|
|
|
/*
|
|
Find element to search for host names or IP addresses, with a menu to
|
|
select pre-defined bookmarks.
|
|
*/
|
|
self.$findElement = Ox.FormElementGroup({
|
|
elements: [
|
|
Ox.MenuButton({
|
|
items: [{id: '', title: 'Me'}, {}].concat(
|
|
self.options.bookmarks,
|
|
[{}, {id: 'clear', title: 'Clear Markers'}]
|
|
),
|
|
overlap: 'right',
|
|
selectable: false,
|
|
title: 'map',
|
|
type: 'image'
|
|
})
|
|
.addClass('OxOverlapRight')
|
|
.bindEvent({
|
|
click: function(data) {
|
|
if (data.id == 'clear') {
|
|
self.$tiles.$element.find('.marker').remove();
|
|
} else {
|
|
self.$find.options({value: data.id});
|
|
find(data.id);
|
|
}
|
|
that.gainFocus();
|
|
}
|
|
}),
|
|
self.$find = Ox.Input({
|
|
clear: true,
|
|
placeholder: 'Find host name or IP address',
|
|
width: 240
|
|
})
|
|
.bindEvent({
|
|
submit: function(data) {
|
|
find(data.value);
|
|
}
|
|
})
|
|
]
|
|
})
|
|
.addClass('interface')
|
|
.attr({id: 'find'})
|
|
.appendTo(that);
|
|
|
|
/*
|
|
Zoom control. 0 is the lowest zoom level, 8 is the highest.
|
|
*/
|
|
self.$zoom = Ox.Range({
|
|
arrows: true,
|
|
min: 0,
|
|
max: 8,
|
|
size: 256,
|
|
thumbSize: 32,
|
|
thumbValue: true,
|
|
value: self.options.zoom
|
|
})
|
|
.addClass('interface')
|
|
.attr({id: 'zoom'})
|
|
.bindEvent({
|
|
change: function(data) {
|
|
zoomTo(data.value);
|
|
}
|
|
})
|
|
.appendTo(that);
|
|
|
|
/*
|
|
Button that toggles the overview map.
|
|
*/
|
|
self.$toggle = Ox.Button({
|
|
title: 'Show Overview Map',
|
|
width: 256
|
|
})
|
|
.addClass('interface')
|
|
.attr({id: 'toggle'})
|
|
.bindEvent({
|
|
click: toggleWorld
|
|
})
|
|
.appendTo(that);
|
|
|
|
/*
|
|
The overview map itself, showing the entire internet.
|
|
*/
|
|
self.$world = Ox.Element({
|
|
tooltip: function(e) {
|
|
return getWorldIP(e);
|
|
}
|
|
})
|
|
.attr({id: 'world'})
|
|
.addClass('interface')
|
|
.bindEvent({
|
|
dragstart: function(e) {
|
|
onDragstart(e, true);
|
|
},
|
|
drag: onDrag,
|
|
singleclick: function(e) {
|
|
setIP(getWorldIP(e));
|
|
panTo(self.xy);
|
|
}
|
|
})
|
|
.append(
|
|
$('<img>').attr({
|
|
src: self.options.tilesURL + '0/0.0.0.0-255.255.255.255.png'
|
|
})
|
|
)
|
|
.hide()
|
|
.appendTo(that);
|
|
|
|
/*
|
|
The position marker on overview map.
|
|
*/
|
|
self.$point = Ox.Element()
|
|
.addClass('marker')
|
|
.appendTo(self.$world);
|
|
|
|
/*
|
|
Off-screen regions on overview map.
|
|
*/
|
|
self.$regions = Ox.Element()
|
|
.attr({id: 'regions'})
|
|
.appendTo(self.$world);
|
|
|
|
/*
|
|
The regions of the overview map, 'center' being the visible area.
|
|
*/
|
|
[
|
|
'center', 'left', 'right', 'top', 'bottom'
|
|
].forEach(function(region) {
|
|
self['$' + region] = Ox.Element()
|
|
.addClass('region')
|
|
.attr({id: region})
|
|
.appendTo(self.$regions);
|
|
});
|
|
[
|
|
'topleft', 'topright', 'bottomleft', 'bottomright', 'square'
|
|
].forEach(function(region) {
|
|
self['$' + region] = Ox.Element()
|
|
.addClass('region ui')
|
|
.appendTo(self.$regions);
|
|
});
|
|
|
|
renderMap();
|
|
renderMarker({host: 'Me', ip: self.options.ip});
|
|
document.location.hash && onHashchange();
|
|
|
|
/*
|
|
Looks up a given host name or IP address and then, if there is a result,
|
|
pans to its position and adds a marker.
|
|
*/
|
|
function find(value) {
|
|
var isHost, query = '';
|
|
value = value.toLowerCase().replace(/\s/g, '');
|
|
isHost = !isIP(value);
|
|
if (value) {
|
|
if (
|
|
isHost && value != 'localhost'
|
|
&& value.indexOf('.') == -1
|
|
) {
|
|
value += '.com';
|
|
}
|
|
query = '&' + (isHost ? 'host' : 'ip') + '='
|
|
+ encodeURIComponent(value);
|
|
}
|
|
self.$find.options({value: value});
|
|
getJSONP(self.options.lookupURL + query, function(data) {
|
|
if (isHost && !isIP(data.ip)) {
|
|
self.$find.addClass('OxError');
|
|
} else {
|
|
data.host = value ? data.host : 'Me';
|
|
setHash(data.host);
|
|
setIP(data.ip);
|
|
panTo(self.xy, function() {
|
|
setHash(data.host);
|
|
});
|
|
renderMarker(data);
|
|
}
|
|
});
|
|
};
|
|
|
|
/*
|
|
Returns an IP address for a given mouse event.
|
|
*/
|
|
function getIPByMouseXY(e) {
|
|
return getIPByXY(getXYByMouseXY(e));
|
|
}
|
|
|
|
/*
|
|
Translates an given integer into an IP address.
|
|
*/
|
|
function getIPByN(n) {
|
|
return Ox.range(4).map(function(i) {
|
|
return (Math.floor(n / Math.pow(256, 3 - i)) % 256).toString();
|
|
}).join('.');
|
|
}
|
|
|
|
/*
|
|
Returns an IP address for given XY coordinates and zoom level.
|
|
*/
|
|
function getIPByXY(xy, zoom) {
|
|
var n = 0, path = self.path,
|
|
z = zoom === void 0 ? self.options.zoom : zoom;
|
|
Ox.loop(8 + z, function(i) {
|
|
var p2 = Math.pow(2, 7 + z - i),
|
|
p4 = Math.pow(4, 7 + z - i),
|
|
xy_ = xy.map(function(v) {
|
|
return Math.floor(v / p2);
|
|
}),
|
|
q = self.projection[path][0].indexOf(xy_[0] + xy_[1] * 2);
|
|
n += q * p4;
|
|
xy = xy.map(function(v, i) {
|
|
return v - xy_[i] * p2;
|
|
});
|
|
path = self.projection[path][1][q];
|
|
});
|
|
return getIPByN(n * Math.pow(4, 8 - z));
|
|
}
|
|
|
|
/*
|
|
Cached getJSONP method.
|
|
*/
|
|
var getJSONP = Ox.cache(Ox.getJSONP, {async: true});
|
|
|
|
/*
|
|
Returns the marker that recieved a given click event, or null.
|
|
*/
|
|
function getMarker(e) {
|
|
var $target = $(e.target);
|
|
return $target.is('.marker')
|
|
? ($target.is('img') ? $target.parent() : $target)
|
|
: null;
|
|
}
|
|
|
|
/*
|
|
Translates a given IP adress into an integer.
|
|
*/
|
|
function getNByIP(ip) {
|
|
return ip.split('.').reduce(function(prev, curr, i) {
|
|
return prev + parseInt(curr) * Math.pow(256, 3 - i);
|
|
}, 0);
|
|
}
|
|
|
|
/*
|
|
Returns overlay image CSS, taking into account that xkcd.png has a white
|
|
border.
|
|
*/
|
|
function getOverlayCSS(image) {
|
|
var src = $('#overlay').attr('src');
|
|
image = image || (src && src.slice(4, -4));
|
|
return image == 'xkcd' ? {
|
|
width: 24 * Math.pow(2, self.options.zoom)
|
|
+ self.mapSize + 'px',
|
|
height: 24 * Math.pow(2, self.options.zoom)
|
|
+ self.mapSize + 'px',
|
|
margin: -12 * Math.pow(2, self.options.zoom) + 'px',
|
|
} : {
|
|
width: self.mapSize + 'px',
|
|
height: self.mapSize + 'px'
|
|
};
|
|
}
|
|
|
|
/*
|
|
Returns the IP address on the overview map for a given mouse event.
|
|
*/
|
|
function getWorldIP(e) {
|
|
var parts = getIPByXY([
|
|
(e.clientX - window.innerWidth + 272),
|
|
(e.clientY - window.innerHeight + 304)
|
|
], 0).split('.');
|
|
return [parts[0], parts[1], 0, 0].join('.');
|
|
}
|
|
|
|
/*
|
|
Returns XY coordinates for a given IP address.
|
|
*/
|
|
function getXYByIP(ip) {
|
|
var path = self.path, x = 0, y = 0, z = self.options.zoom,
|
|
n = Math.floor(getNByIP(ip) / Math.pow(4, 8 - z));
|
|
Ox.loop(8 + z, function(i) {
|
|
var p2 = Math.pow(2, 7 + z - i),
|
|
p4 = Math.pow(4, 7 + z - i),
|
|
q = Math.floor(n / p4),
|
|
xy = self.projection[path][0][q];
|
|
x += xy % 2 * p2;
|
|
y += Math.floor(xy / 2) * p2;
|
|
n -= q * p4;
|
|
path = self.projection[path][1][q];
|
|
});
|
|
return [x, y];
|
|
}
|
|
|
|
/*
|
|
Returns XY coordinates for a given mouse event.
|
|
*/
|
|
function getXYByMouseXY(e) {
|
|
var mouseXY = [e.clientX, e.clientY];
|
|
return self.xy.map(function(v, i) {
|
|
return v - self.mapCenter[i] + mouseXY[i];
|
|
});
|
|
}
|
|
|
|
/*
|
|
Tests if a given string is an IP address.
|
|
*/
|
|
function isIP(str) {
|
|
var parts = str.split('.');
|
|
return parts.length == 4 && Ox.every(parts, function(v) {
|
|
var n = parseInt(v);
|
|
return n == v && n >= 0 && n < 256;
|
|
});
|
|
}
|
|
|
|
/*
|
|
Handles doubleclick events by delegating to the mousewheel handler.
|
|
*/
|
|
function onDoubleclick(e) {
|
|
onMousewheel(e, 0, 0, e.shiftKey ? - 1 : 1);
|
|
}
|
|
|
|
/*
|
|
Handles drag events for both the main map and the overview map.
|
|
*/
|
|
function onDrag(e) {
|
|
var delta = [e.clientDX, e.clientDY];
|
|
setXY(self.dragstartXY.map(function(v, i) {
|
|
return Ox.limit(v - delta[i] * self.dragfactor, 0, self.mapSize - 1);
|
|
}));
|
|
renderMap();
|
|
}
|
|
|
|
/*
|
|
Handles dragstart events for both the main map and the overview map.
|
|
*/
|
|
function onDragstart(e, isWorld) {
|
|
self.dragstartXY = self.xy;
|
|
self.dragfactor = isWorld ? -Math.pow(2, self.options.zoom) : 1
|
|
}
|
|
|
|
/*
|
|
Handles hashchange events.
|
|
*/
|
|
function onHashchange() {
|
|
var parts;
|
|
if (self.hashchange) {
|
|
parts = document.location.hash.substr(1).split(',');
|
|
if (parts[1] != self.options.zoom) {
|
|
zoomTo(parts[1]);
|
|
}
|
|
self.$find.options({value: parts[0]});
|
|
find(parts[0]);
|
|
}
|
|
}
|
|
|
|
/*
|
|
Handles mousewheel events (zooms in or out, but maintains the position
|
|
that the mouse is pointing at).
|
|
*/
|
|
function onMousewheel(e, delta, deltaX, deltaY) {
|
|
var $marker = getMarker(e),
|
|
deltaZ = 0,
|
|
mouseXY = getXYByMouseXY(e);
|
|
if (!self.zooming && Math.abs(deltaY) > Math.abs(deltaX)) {
|
|
if (deltaY < 0 && self.options.zoom > 0) {
|
|
deltaZ = -1;
|
|
} else if (deltaY > 0 && self.options.zoom < self.maxZoom) {
|
|
deltaZ = 1;
|
|
}
|
|
if (deltaZ) {
|
|
if ($marker) {
|
|
setIP($marker.attr('id'));
|
|
} else {
|
|
setXY(self.xy.map(function(xy, i) {
|
|
return Ox.limit(
|
|
deltaZ == -1
|
|
? 2 * xy - mouseXY[i]
|
|
: (xy + mouseXY[i]) / 2,
|
|
0, self.mapSize - 1
|
|
);
|
|
}));
|
|
}
|
|
zoomBy(deltaZ);
|
|
self.zooming = true;
|
|
setTimeout(function() {
|
|
self.zooming = false;
|
|
}, 50);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
Handles window resize events.
|
|
*/
|
|
function onResize() {
|
|
self.mapCenter = [
|
|
Math.floor(window.innerWidth / 2),
|
|
Math.floor(window.innerHeight / 2)
|
|
];
|
|
if (window.innerHeight < 352) {
|
|
if (self.$world.is(':visible')) {
|
|
self.worldWasVisible = true;
|
|
self.$toggle.trigger('click');
|
|
}
|
|
self.$toggle.options({disabled: true});
|
|
} else {
|
|
self.$toggle.options({disabled: false});
|
|
if (self.worldWasVisible) {
|
|
self.worldWasVisible = false;
|
|
self.$toggle.trigger('click');
|
|
}
|
|
}
|
|
renderMap();
|
|
}
|
|
|
|
/*
|
|
Handles singleclick events. Clicking on a marker selects it, but holding
|
|
the Meta key toggles its selected state instead, and holding shift adds
|
|
it to the current selection. Clicking on the map and holding shift adds
|
|
a marker at that place.
|
|
*/
|
|
function onSingleclick(e) {
|
|
var $marker = getMarker(e), ip;
|
|
if ($marker) {
|
|
if (e.metaKey) {
|
|
$marker.toggleClass('selected');
|
|
} else {
|
|
!e.shiftKey && $('.marker').removeClass('selected');
|
|
$marker.detach().addClass('selected').appendTo(self.$tiles);
|
|
}
|
|
} else {
|
|
$('.marker.selected').removeClass('selected');
|
|
ip = getIPByMouseXY(e);
|
|
panTo(getXYByMouseXY(e), function() {
|
|
e.shiftKey && find(ip);
|
|
});
|
|
}
|
|
}
|
|
|
|
/*
|
|
Pans the map by XY.
|
|
*/
|
|
function panBy(xy) {
|
|
panTo(xy.map(function(v, i) {
|
|
return Ox.limit(self.xy[i] + v * window[
|
|
i == 0 ? 'innerWidth' : 'innerHeight'
|
|
], 0, self.mapSize - 1);
|
|
}));
|
|
}
|
|
|
|
/*
|
|
Pans the map to XY.
|
|
*/
|
|
function panTo(xy, callback) {
|
|
if (!self.panning) {
|
|
self.panning = true;
|
|
setXY(xy);
|
|
self.$tiles.animate({
|
|
left: self.mapCenter[0] - self.xy[0] + 'px',
|
|
top: self.mapCenter[1] - self.xy[1] + 'px'
|
|
}, 250, function() {
|
|
self.panning = false;
|
|
renderMap();
|
|
callback && callback();
|
|
});
|
|
updateWorld(true);
|
|
}
|
|
}
|
|
|
|
/*
|
|
Renders the tiles of the map.
|
|
*/
|
|
function renderMap() {
|
|
var halfWidth, halfHeight;
|
|
//if (isIP(document.location.hash.substr(1).split(',')[0])) {
|
|
setHash(self.options.ip);
|
|
//}
|
|
self.$tiles.css({
|
|
left: self.mapCenter[0] - self.xy[0] + 'px',
|
|
top: self.mapCenter[1] - self.xy[1] + 'px'
|
|
});
|
|
self.widthTiles = Math.floor(
|
|
window.innerWidth / self.tileSize / 2
|
|
) * 2 + 3;
|
|
self.heightTiles = Math.floor(
|
|
window.innerHeight / self.tileSize / 2
|
|
) * 2 + 3;
|
|
halfWidth = self.widthTiles / 2,
|
|
halfHeight = self.heightTiles / 2;
|
|
Ox.loop(-Math.floor(halfHeight), Math.ceil(halfHeight), function(dy) {
|
|
Ox.loop(-Math.floor(halfWidth), Math.ceil(halfWidth), function(dx) {
|
|
var xy = [
|
|
self.xy[0] + dx * self.tileSize,
|
|
self.xy[1] + dy * self.tileSize
|
|
];
|
|
if (
|
|
xy[0] >= 0 && xy[0] < self.mapSize
|
|
&& xy[1] >= 0 && xy[1] < self.mapSize
|
|
) {
|
|
renderTile(getIPByXY(xy));
|
|
}
|
|
});
|
|
});
|
|
updateWorld();
|
|
$('.OxTooltip').remove();
|
|
}
|
|
|
|
/*
|
|
Renders a map marker with the given properties.
|
|
*/
|
|
function renderMarker(data) {
|
|
var $icon, $marker = self.$tiles.find('div[id="' + data.ip + '"]'),
|
|
src, timeout;
|
|
if (!$marker.length) {
|
|
src = data.host == 'Me'
|
|
? Ox.UI.getImageURL('symbolUser')
|
|
: 'http://' + data.host + '/favicon.ico';
|
|
timeout = setTimeout(function() {
|
|
$icon = $('<img>')
|
|
.addClass('marker')
|
|
.attr({src: 'png/favicon.png'})
|
|
.css({opacity: 0})
|
|
.appendTo($marker)
|
|
.animate({opacity: 1}, 250);
|
|
}, 1000);
|
|
$('.marker').removeClass('selected');
|
|
$marker = Ox.Element({
|
|
tooltip: function() {
|
|
var host = data.host, ip = $marker.attr('id');
|
|
return '<b>' + (host || 'Me') + '</b>'
|
|
+ (ip != host ? '<br>' + ip : '');
|
|
}
|
|
})
|
|
.attr({id: data.ip || self.options.ip})
|
|
.addClass('marker')
|
|
.css({
|
|
left: self.xy[0] + 'px',
|
|
top: self.xy[1] + 'px',
|
|
opacity: 0
|
|
})
|
|
.data({host: data.host || data.ip})
|
|
.appendTo(self.$tiles)
|
|
.animate({opacity: 1}, 250);
|
|
$marker.$tooltip.css({textAlign: 'center'});
|
|
$('<img>')
|
|
.on({
|
|
load: function() {
|
|
var $this = $(this);
|
|
clearTimeout(timeout);
|
|
if ($icon) {
|
|
$icon.stop().animate({
|
|
opacity: 0
|
|
}, 125, callback);
|
|
} else {
|
|
callback();
|
|
}
|
|
function callback() {
|
|
$marker.empty().append($this.animate({
|
|
opacity: 1
|
|
}, $icon ? 125 : 250));
|
|
}
|
|
}
|
|
})
|
|
.addClass('marker')
|
|
.attr({src: src})
|
|
.css({opacity: 0});
|
|
}
|
|
$marker.addClass('selected');
|
|
}
|
|
|
|
/*
|
|
Renders a tile at a given IP address.
|
|
*/
|
|
function renderTile(ip) {
|
|
var n = getNByIP(ip),
|
|
firstN = Math.floor(n / self.tileAreaInIPs) * self.tileAreaInIPs,
|
|
lastN = firstN + self.tileAreaInIPs - 1,
|
|
firstIP = getIPByN(firstN),
|
|
lastIP = getIPByN(lastN),
|
|
src = self.options.tilesURL + firstIP.split('.')[0] + '/'
|
|
+ firstIP + '-' + lastIP + '.png',
|
|
xy = getXYByIP(firstIP).map(function(v) {
|
|
return Math.floor(v / 256) * 256;
|
|
});
|
|
if (!self.$tiles.$element.find('img[src="' + src + '"]').length) {
|
|
$('<img>')
|
|
.addClass('tile')
|
|
.attr({src: src})
|
|
.css({
|
|
left: xy[0] + 'px',
|
|
top: xy[1] + 'px',
|
|
})
|
|
.appendTo(self.$tiles);
|
|
}
|
|
}
|
|
|
|
/*
|
|
Sets the hash to a given value, or to the current IP address.
|
|
*/
|
|
function setHash(value) {
|
|
/*
|
|
Temporarily disable the hashchange handler
|
|
*/
|
|
self.hashchange = false;
|
|
document.location.hash = [
|
|
value || self.options.ip, self.options.zoom
|
|
].join(',');
|
|
setTimeout(function() {
|
|
self.hashchange = true;
|
|
});
|
|
}
|
|
|
|
/*
|
|
Updates XY when setting the IP address.
|
|
*/
|
|
function setIP(ip) {
|
|
self.options.ip = ip;
|
|
self.xy = getXYByIP(ip);
|
|
}
|
|
|
|
/*
|
|
Updates the IP address when setting XY.
|
|
*/
|
|
function setXY(xy) {
|
|
self.xy = xy;
|
|
self.options.ip = getIPByXY(xy);
|
|
}
|
|
|
|
/*
|
|
Toggles the overlay image.
|
|
*/
|
|
function toggleOverlay(image) {
|
|
var $overlay = $('#overlay'), src = 'png/' + image + '.png';
|
|
$overlay.stop().animate({opacity: 0}, 250, function() {
|
|
$overlay.remove();
|
|
});
|
|
if (!$('#overlay[src="' + src + '"]').length) {
|
|
$('<img>')
|
|
.attr({id: 'overlay', src: src})
|
|
.css(getOverlayCSS(image))
|
|
.appendTo(self.$tiles)
|
|
.animate({opacity: 0.5}, 250);
|
|
}
|
|
}
|
|
|
|
/*
|
|
Toggles the overview map.
|
|
*/
|
|
function toggleWorld() {
|
|
var action = self.$toggle.options('title').substr(0, 4);
|
|
self.$toggle.options({
|
|
title: (action == 'Show' ? 'Hide' : 'Show') + ' Overview Map'
|
|
});
|
|
action == 'Show' && self.$world.show();
|
|
self.$world.animate({
|
|
opacity: action == 'Show' ? 1 : 0
|
|
}, 250, function() {
|
|
action == 'Hide' && self.$world.hide();
|
|
});
|
|
}
|
|
|
|
/*
|
|
Updates the overview map.
|
|
*/
|
|
function updateWorld(animate) {
|
|
var ms = animate ? 250 : 0,
|
|
p = Math.pow(2, self.options.zoom),
|
|
width = Math.round(window.innerWidth / p),
|
|
height = Math.round(window.innerHeight / p),
|
|
left = Math.floor(256 + self.xy[0] / p - width / 2),
|
|
top = Math.floor(256 + self.xy[1] / p - height / 2);
|
|
self.$point.animate({
|
|
left: Math.round(self.xy[0] / p) + 'px',
|
|
top: Math.round(self.xy[1] / p) + 'px'
|
|
}, ms);
|
|
self.$regions.animate({
|
|
left: left - 512 + 'px',
|
|
top: top - 512 + 'px',
|
|
width: width + 512 + 'px',
|
|
height: height + 512 + 'px'
|
|
}, ms);
|
|
self.$center.css({
|
|
width: width + 'px',
|
|
height: height + 'px'
|
|
});
|
|
self.$top.css({
|
|
width: width + 'px'
|
|
});
|
|
self.$bottom.css({
|
|
width: width + 'px'
|
|
});
|
|
var uiwidth = 256 / p + 'px',
|
|
uiheight = 16 / p + 'px',
|
|
uiradius = 8 / p + 'px',
|
|
uimargin = 256 + 16 / p + 'px',
|
|
uibottom = 256 + 48 / p + 'px';
|
|
['top', 'bottom'].forEach(function(topbottom) {
|
|
['left', 'right'].forEach(function(leftright) {
|
|
var $element = self['$' + topbottom + leftright];
|
|
$element.css(topbottom, uimargin);
|
|
$element.css(leftright, uimargin);
|
|
$element.css({
|
|
width: uiwidth,
|
|
height: uiheight,
|
|
borderRadius: uiradius
|
|
});
|
|
});
|
|
})
|
|
self.$square.css({
|
|
right: uimargin,
|
|
bottom: uibottom,
|
|
width: uiwidth,
|
|
height: uiwidth
|
|
});
|
|
}
|
|
|
|
/*
|
|
Zooms the map by Z zoom levels.
|
|
*/
|
|
function zoomBy(z) {
|
|
zoomTo(self.options.zoom + z);
|
|
}
|
|
|
|
/*
|
|
Zooms the map to zoom level Z.
|
|
*/
|
|
function zoomTo(z) {
|
|
if (z >= 0 && z <= self.maxZoom) {
|
|
self.options.zoom = z;
|
|
self.mapSize = Math.pow(2, self.options.zoom) * self.tileSize;
|
|
self.tileSizeInXY = Math.pow(2, 16 - self.options.zoom);
|
|
self.tileAreaInIPs = Math.pow(4, 16 - self.options.zoom);
|
|
self.xy = getXYByIP(self.options.ip);
|
|
self.$zoom.options({value: self.options.zoom});
|
|
self.$tiles.$element.find('.tile').remove();
|
|
self.$tiles.$element.find('div.marker').each(function() {
|
|
var $marker = $(this), xy = getXYByIP($marker.attr('id'));
|
|
$marker.css({
|
|
left: xy[0] + 'px',
|
|
top: xy[1] + 'px'
|
|
});
|
|
});
|
|
self.$tiles.css({
|
|
width: self.mapSize + 'px',
|
|
height: self.mapSize + 'px'
|
|
});
|
|
renderMap();
|
|
$('#overlay').css(getOverlayCSS());
|
|
}
|
|
}
|
|
|
|
return that;
|
|
|
|
};
|
|
|
|
var URL = 'https://0x2620.org/html/ipv4map/',
|
|
lookupURL = URL + 'php/ipv4map.php?callback={callback}',
|
|
tilesURL = URL + 'png/tiles/';
|
|
|
|
Ox.getJSON('json/bookmarks.json', function(bookmarks) {
|
|
Ox.getJSONP(lookupURL, function(data) {
|
|
Ox.IPv4Map({
|
|
bookmarks: bookmarks,
|
|
ip: data.ip.indexOf(':') == -1 ? data.ip : '127.0.0.1',
|
|
lookupURL: lookupURL,
|
|
tilesURL: tilesURL,
|
|
zoom: 4
|
|
}).appendTo(Ox.$body);
|
|
});
|
|
});
|
|
|
|
});
|
|
|