/* 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 = $('
') .css({ position: 'absolute', width: '256px', textAlign: 'justify' }) ) .append( self.$images = $('
') .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) { $('') .attr({href: image, target: '_blank'}) .append( $('') .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( $('
').addClass('textTitle').html(section) ); Ox.forEach(keys, function(value, key) { self.$controlsPanel .append( $('
').addClass('textKey').html( key.split(' ').map(function(key) { return key.split('+').map(function(key) { return Ox.UI.symbols[key] || key; }).join('') }).join(' ') ) ) .append( $('
').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( $('').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 = $('') .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 '' + (host || 'Me') + '' + (ip != host ? '
' + 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'}); $('') .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) { $('') .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) { $('') .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); }); }); });