There are 7 previous versions of this script.
// ==UserScript==
// @name Ikariam Aquarium
// @namespace overkill_gm
// @description Graphical City Selector
// @include http://s*.ikariam.*/index.php*
// @version 0.1.0
// ==/UserScript==
function debug() { var msg = []; for (var i = 0, n = arguments.length; i<n; ++i) msg.push(arguments[i]); setTimeout(function() { throw new Error("[debug] " + msg.join(' ')); }, 0);}
$ = document.getElementById;
function $x( xpath, root ) { var doc = root ? root.evaluate ? root : root.ownerDocument : document, next; var got = doc.evaluate( xpath, root||doc, null, 0, null ), result = []; switch (got.resultType) { case got.STRING_TYPE: return got.stringValue; case got.NUMBER_TYPE: return got.numberValue; case got.BOOLEAN_TYPE: return got.booleanValue; default: while (next = got.iterateNext()) result.push( next ); return result; } }
function $X( xpath, root ) { var got = $x( xpath, root ); return got instanceof Array ? got[0] : got; }
function node(type, className, styles, content) { var n = document.createElement(type||"div"); if (className) n.className = className; if (styles) for (var prop in styles) n.style[prop] = styles[prop]; if (content) n.innerHTML = "string" == typeof content ? content : content.toXMLString(); return n; }
function onClick(node, fn, capture, e) { node.addEventListener((e||"") + "click", fn, !!capture); }
function limit(n,lower,upper) { return Math.max(Math.min(n,upper),lower); }
function serialize(name,val){
if(document.domain)
GM_setValue(name+'_'+document.domain , uneval(val));
}
function deserialize(name, def) {
if(document.domain)
return eval(GM_getValue(name+'_'+document.domain , (def || '({})') ));
}
function generateSelfCache(){
var cache = {};
// todo add ability to exclude a city
for each (var town in $x('//*[@id="citySelect"]/option')){
//for each (var town in $x('./option',$('citySelect'))){
if (town.innerHTML.charAt(0) == '[') { //todo: move this if statement outside the loop
// COORDS in town navigation
cache[town.value] = {
'name' : town.innerHTML.replace(' ',' ').replace(/\[[0-9:\s]+\]\s+/,''),
'position' : town.innerHTML.match(/\[([0-9:\s]+)\]/,'')[1].replace(/00/g,'100'),
'tradegood': town.title.substring(12)
};
} else {
// TRADE GOOD in town navigation
var coords = town.title;
coords = coords.substr(1,coords.length-2);
cache[town.value] = {
'name':town.innerHTML,
'position':coords,
'tradegood':unsafeWindow.LocalizationStrings['resources'][town.className.charAt(town.className.indexOf('tradegood')+9)]
};
}
if (town.className.indexOf('deployed') == -1) {
cache[town.value].me = true;
}
}
return cache;
}
var LINE_HEIGHT = 10;
function Cities(cityObj,ctx){
this.id = cityObj.id;
this.name = cityObj.name;
this.me = !!cityObj.me;
this.current = !!cityObj.current;
this.x = parseInt(cityObj.position.split(':')[0],10);
this.y = parseInt(cityObj.position.split(':')[1],10);
this.width = 60;
this.anchor = { x : this.x, y : this.y };
this.size = cityObj.name.split(',').length;
this.height = LINE_HEIGHT * cityObj.name.split(',').length;
this.tradeGood = cityObj.tradeGood;
this.context = ctx;
this.toString = function(){ return this.name; }
this.transform = function(scale,left,top){
this.x = limit(scale*this.x+left,0,Map.width);
this.y = limit(scale*this.y+top,0,Map.height) - this.size*LINE_HEIGHT/2;
this.anchor = { x : this.x, y : this.y };
}
this.draw = function(jedi){
//debug('draw',this.name,scale*(this.x+left),scale*(this.y+top));
var names = this.name.split(',');
//debug(Map.force(this.x,this.y));
if (jedi) { // jedi = use force
var newX = this.x, newY = this.y;
var force = Map.force(newX,newY,this.width,this.height,this.anchor.x,this.anchor.y);
//if (this.id == 80210) debug(force);
//todo, turn force into velocity instead of directly into position
var maxDeflection = 4-Map.updates/20; // decay period
newX += limit(force[0],-1*maxDeflection,maxDeflection);
newY += limit(force[1],-1*maxDeflection,maxDeflection);
this.x = limit(newX,0,Map.width -this.width);
this.y = limit(newY,0,Map.height-this.height);
}
// DRAW LOCATOR CIRCLE
if (this.current) {
this.context.beginPath();
this.context.arc(this.anchor.x,this.anchor.y,15,0,Math.PI*2,false);
this.context.strokeStyle = 'yellow';
this.context.stroke();
}
// DRAW ANCHOR LINE
this.context.beginPath();
this.context.arc(this.anchor.x,this.anchor.y,2.5,0,Math.PI*2,false);
this.context.strokeStyle = this.context.fillStyle = this.current ? 'yellow' : this.me ? "rgb(254 , 245 , 228)" : "rgb(192 , 244 , 169)";
this.context.fill();
//this.lineWidth = "10px";
//this.context.strokeStyle = "rgb(255 , 255 , 255)";
this.context.stroke();
this.context.strokeStyle = 'rgba(235,20,20,0.3)';
this.lineWidth = "2px";
this.context.beginPath();
var pin_x = (this.x+(this.width>>1)) < this.anchor.x ? this.x + this.width : this.x;
var pin_y = (this.y+(this.height>>1)) < this.anchor.y ? this.y + this.height : this.y;
this.context.moveTo(pin_x,pin_y);
this.context.lineTo(this.anchor.x,this.anchor.y);
this.context.closePath();
this.context.stroke();
// DRAW BOX, BORDER, TEXT
this.context.strokeStyle = this.current ? 'yellow' : 'rgba(0,0,0, 0.8)';
this.context.font = "9px sans-serif";
this.context.textBaseline = "top"
//this.lineWidth = "1px";
this.context.fillStyle = (this.me) ? "rgba(254 , 245 , 228,0.8)" : "rgba(192 , 244 , 169 , 0.8)";
this.context.fillRect(this.x,this.y,this.width,this.size*LINE_HEIGHT);
this.context.strokeRect(this.x,this.y,this.width,this.size*LINE_HEIGHT);
this.context.fillStyle = "black";
var textWidth = 0;
for (var i = 0; i < names.length; ++i){
this.context.fillText(names[i],this.x + 1,this.y+LINE_HEIGHT*i + 1,this.width-2);
textWidth = Math.max(textWidth,this.context.measureText(names[i]).width);
}
this.width = textWidth + 4;
}
}
var Map = function() {
var cities = [];
// elements
var canvasContainer,canvas,debugDiv,canvasPos,timer;
var ctx;
var width = 630, height = 400;
var mouseX = mouseY = 0;
var updates = 0;
function init(){
// initialize canvas
canvasContainer = $('mainview').
insertBefore(node('div','',{
width:width+'px',display:'none',position:'absolute',right:'0',top:'16px',zIndex:'600',
},'<canvas id="citySelectMapCanvas" width="'+width+'" height="'+height+'"></canvas>'),
$('mainview').firstChild
);
var showHideCtrlElem = $X('//div[@id="cityNav"]//div[@class="citySelect smallFont jsSelect"]');
onClick(showHideCtrlElem,function(){
if ($X('./ul',showHideCtrlElem).style.display == 'block') { hide(); }
else { show(); }
});
canvas = canvasContainer.firstChild;
debugDiv = canvasContainer.insertBefore(node('div','','','DEBUG INFO'),canvas);
debugDiv.style.textAlign = 'right';
ctx = canvas.getContext('2d');
addControls();
// initialize cities
var selfCache = generateSelfCache();
var currentCityId = $X('//*[@id="citySelect"]/option[@selected="selected"]').value;
//debug(uneval(selfCache));
var islandCache = {};
for (var cityID in selfCache){
if (islandCache[selfCache[cityID].position]) {
var temp = islandCache[selfCache[cityID].position];
temp.id += ',' + cityID;
temp.name += ',' + selfCache[cityID].name;
} else {
islandCache[selfCache[cityID].position] = {
id : cityID,
name:selfCache[cityID].name,
position:selfCache[cityID].position,
tradegood:selfCache[cityID].tradegood,
me:selfCache[cityID].me,
};
}
if (cityID == currentCityId) {
islandCache[selfCache[cityID].position].current = true;
}
}
//debug(uneval(islandCache));
for each (var island in islandCache){
cities.push(new Cities(island,ctx));
}
zoom();
var savedPositions = deserialize('positions','');
if (savedPositions){
for each (var city in cities) {
var temp = savedPositions[city.id];
if (temp) {
city.x = temp[0];
city.y = temp[1];
}
}
}
var grabbed = { drag:false, hOff:0, vOff:0 };
canvas.addEventListener("mousemove",function(e){
mouseX = (e.pageX - canvasPos.x);
mouseY = (e.pageY - canvasPos.y);
if (grabbed.drag){
grabbed.city.x = mouseX-grabbed.hOff;
grabbed.city.y = mouseY-grabbed.vOff;
grabbed.city.draw();
}
var temp = force(mouseX,mouseY);
debugDiv.firstChild.nodeValue = mouseX + ' ' + mouseY + ' force: ' + temp[0].toFixed(3) + ',' + temp[1].toFixed(3) + ' ' + canvasPos.x;
//debug.innerHTML = mouseX + ' ' + mouseY + ' => ' + islandX + ' : ' + islandY + ' | dx:' + dx + '/' + xOffset + ' dy:' + dy + '/' + yOffset + ' x ' + scale;
},false);
canvas.addEventListener("mousedown",function(e){
function findCity(x,y){
for each (var city in cities) {
if ((x > city.x) && (x < city.x + city.width) && (y > city.y) && (y < city.y + city.height)) return city;
}
return false;
}
canvasPos = getAbsolutePosition(canvas);
mouseX = (e.pageX - canvasPos.x);
mouseY = (e.pageY - canvasPos.y);
var city = findCity(mouseX,mouseY);
if (city) {
grabbed.drag = true;
grabbed.city = city;
grabbed.hOff = mouseX - city.x;
grabbed.vOff = mouseY - city.y;
grabbed.startX = mouseX;
grabbed.startY = mouseY;
}
debugDiv.firstChild.nodeValue = "down "+city;
e.stopPropagation();
e.preventDefault();
},false);
canvas.addEventListener("mouseup",function(e){
savePositions();
if (grabbed.drag) {
grabbed.drag = false;
mouseX = (e.pageX - canvasPos.x);
mouseY = (e.pageY - canvasPos.y);
if ((mouseX == grabbed.startX) && (mouseY == grabbed.startY)) {
var id = grabbed.city.id;
if (grabbed.city.size > 1) {
var i = Math.floor((grabbed.startY - grabbed.city.y) / LINE_HEIGHT);
id = id.split(',')[limit(i,0,grabbed.city.size - 1)];
}
debugDiv.firstChild.nodeValue = "change city to "+grabbed.city + ' ' + id;
changeCity(id);
}
}
e.stopPropagation();
e.preventDefault();
},false);
}
function addControls(){
function checkDeployed(){
function showDeployed(){
var last, collection = $x('//div[@id="cityNav"]//div[@class="citySelect smallFont jsSelect"]/ul/li');
for each (var li in collection){
li.style.display = 'block';
if (!last && (li.className.indexOf(' last') != -1)) { last = li; }
}
if (last && (li != last)) last.className = last.className.replace(/ last/,'');
}
function hideDeployed(){
var last, collection = $x('//div[@id="cityNav"]//div[@class="citySelect smallFont jsSelect"]/ul/li');
for each (var li in collection){
if (li.className.indexOf('deployedCities') != -1) li.style.display = 'none';
if (li.style.display != 'none') last = li;
}
if (last && (last.className.indexOf(' last') == -1)) { last.className += ' last'; }
}
if (options.deployedCities) showDeployed();
else hideDeployed();
}
var options = deserialize('options',{deployedCities:true});
GM_addStyle(".aquariumCtrl { float:right; cursor:pointer; margin: 0 3px; background:#ffe; -moz-border-radius:3px; }");
var close = node('div','aquariumCtrl','','close');
close.title = "Close Map";
onClick(close,hide);
debugDiv.appendChild(close);
var deployedCities = node('div','aquariumCtrl','',options.deployedCities ? 'shown' : 'hidden');
deployedCities.title = "Show/Hide deployed cities in the city selection dropdown box?";
onClick(deployedCities,function(){
options.deployedCities = !options.deployedCities;
checkDeployed();
this.innerHTML = options.deployedCities ? 'shown' : 'hidden';
serialize('options',options);
});
debugDiv.appendChild(deployedCities);
checkDeployed();
}
function show(){
canvasContainer.style.display = 'block';
canvasPos = getAbsolutePosition(canvas);
timer = setInterval(update,100);
}
function hide(){
canvasContainer.style.display = 'none';
clearInterval(timer);
savePositions();
}
function getAbsolutePosition(element) {
var r = { x: element.offsetLeft, y: element.offsetTop };
if (element.offsetParent) {
var tmp = getAbsolutePosition(element.offsetParent);
r.x += tmp.x;
r.y += tmp.y;
}
return r;
};
function zoom(){
var minX = width, minY = height, maxX = maxY = 0;
for each (var city in cities){
minX = Math.min(minX,city.x);
maxX = Math.max(maxX,city.x);
minY = Math.min(minY,city.y);
maxY = Math.max(maxY,city.y);
}
var scale = 1, left = 0, top = 0;
scale = Math.min(width/(maxX - minX),height/(maxY - minY)) * 0.9;
left = (width - (maxX*scale) - (minX*scale))/2;
top = (height - (maxY*scale) - (minY*scale))/2;
for each (var city in cities) city.transform(scale,left,top);
}
function update(){
function drawBG(){
//ctx.clearRect(0,0,width,height);
ctx.fillStyle = "rgba(28,123,193,0.5)";
//ctx.fillStyle = "rgb(28,123,193)";
ctx.fillRect(0,0,width,height);
}
if (Map.updates < 75) Map.updates++;
drawBG();
for each (var city in cities) {
city.draw(true);
}
}
function force(x,y,w,h,ax,ay){ //todo input origin's width and height
var Kspace = 3e2; // repulsion coeff
var Kattract = 20; // attraction coeff
w = w || 0;
h = h || 0;
if (mouseX && mouseY) {
var d = Math.abs(x - mouseX) + Math.abs(y - mouseY); // rectangular distance metric
Kspace *= limit(200/d,1,5);
}
var forceX = 10/Math.pow(x,2) - 10/Math.pow(width-x,2); // left right edge
var forceY = 10/Math.pow(y,2) - 10/Math.pow(height-y,2); // top bottom edge
//var out = []; // for debugging
// SIGMA repulsion between like ions
for each (var city in cities) {
if ((x != city.x) && (y != city.y)) {
var a;
if (((x + w) > city.x ) && (x < (city.x+city.width+1))) { // case direct overlap
a = x <= city.x ? -13 : 13; // todo update this with (x - city.x) + city.width
//out.push(x);
} else {
var a_l = x - city.x - city.width;
var a_r = x + w - city.x;
a = (Math.abs(a_l) < Math.abs(a_r)) ? a_l : a_r;
}
var a2 = Math.pow(a,2);
var b;
if ( ((y + h) > city.y) && (y < (city.y+city.height+1)) ) { // case direct overlap
b = y < city.y ? -10 : 10; // todo update this
//out.push(':'+y);
} else {
var b_t = y - city.y - city.height;
var b_b = y + h - city.y;
b = (Math.abs(b_t) < Math.abs(b_b)) ? b_t : b_b;
}
var b2 = Math.pow(b,2);
var c2 = a2 + b2;
if (c2 && c2 < 900) { // only consider close object
var f = Kspace / c2;
var c = Math.sqrt(c2);
forceX += 1 * f*(a/c); // anisotropy * force * cos()
forceY += city.size * f*(b/c); // anisotropy * force * sin()
}
}
}
// attracted to anchor point USE SPRING MODEL
if (ax && ay){
var a_l = x - ax;
var a_r = x + w - ax;
a = (Math.abs(a_l) < Math.abs(a_r)) ? a_l : a_r;
a2 = Math.pow(a,2);
var b_t = y - ay;
var b_b = y + h - ay;
b = (Math.abs(b_t) < Math.abs(b_b)) ? b_t : b_b;
b2 = Math.pow(b,2);
c2 = a2 + b2;
if (c2 > 0) {
c = Math.sqrt(c2);
f = -1/Kattract*c;
forceX += f*(a/c); // force * cos()
forceY += f*(b/c); // force * sin()
}
}
//if (out.length>1) debug(out);
return [forceX,forceY];
}
function collision(x,y){
for each (var city in cities) {
if ((x == city.x) && (y == city.y)) return true;
}
return false;
}
function savePositions(){
var output = {};
for each (var city in cities) {
output[city.id] = [ city.x , city.y ];
}
serialize('positions',output);
}
return {
init : init,
force : force,
width : width,
height : height,
collision: collision,
updates : updates,
}
}();
changeCity = function(city_id) {
var go = false;
for each (var option in $x('//select[@id="citySelect"]/option')) {
if (option.value == city_id) {
option.selected = go = true;
} else {
option.selected = false;
}
}
if (go) document.getElementById('changeCityForm').submit();
}
Map.init();