Index: binary-improvements/webserver/js/index.js
===================================================================
--- binary-improvements/webserver/js/index.js	(revision 244)
+++ binary-improvements/webserver/js/index.js	(revision 245)
@@ -1,426 +1,4 @@
-// ===============================================================================================
-// Constants
+InitializeTabs ();
+SetupInventoryDialog ();
+InitPermissions ();
 
-var mapinfo = {
-	regionsize: 512,
-	chunksize: 16,
-	tilesize: 128,
-	maxzoom: 4
-}
-
-var BAG_COLS = 8;
-var BAG_ROWS = 4;
-var BELT_COLS = 8;
-var INV_ITEM_WIDTH = 58;
-var INV_ITEM_HEIGHT = 40;
-
-var userdata = false;
-
-
-function initMap() {
-	// ===============================================================================================
-	// 7dtd coordinate transformations
-
-	SDTD_Projection = {
-		project: function (latlng) {
-			return new L.Point(
-				(latlng.lat) / Math.pow(2, mapinfo.maxzoom),
-				(latlng.lng) / Math.pow(2, mapinfo.maxzoom) );
-		},
-		
-		unproject: function (point) {
-			return new L.LatLng(
-				point.x * Math.pow(2, mapinfo.maxzoom),
-				point.y * Math.pow(2, mapinfo.maxzoom) );
-		}
-	};
-
-	SDTD_CRS = L.extend({}, L.CRS.Simple, {
-		projection: SDTD_Projection,
-		transformation: new L.Transformation(1, 0, -1, 0),
-
-		scale: function (zoom) {
-			return Math.pow(2, zoom);
-		}
-	});
-
-
-
-
-	// ===============================================================================================
-	// Map and basic tile layers
-
-	var initTime = new Date().getTime();
-
-	map = L.map('tab_map', {
-		zoomControl: false, // Added by Zoomslider
-		zoomsliderControl: true,
-		attributionControl: false,
-		crs: SDTD_CRS
-	}).setView([0, 0], Math.max(0, mapinfo.maxzoom - 5));
-
-	var tileLayer = L.tileLayer('../map/{z}/{x}/{y}.png?t={time}', {
-		maxZoom: mapinfo.maxzoom + 1,
-		minZoom: Math.max(0, mapinfo.maxzoom - 5),
-		maxNativeZoom: mapinfo.maxzoom,
-		tileSize: mapinfo.tilesize,
-		continuousWorld: true,
-		tms: true,
-		unloadInvisibleTiles: false,
-		time: initTime
-	});
-	
-	// TileLayer w/ TMS=true fix for zoomlevel >= 8
-	tileLayer._getWrapTileNum = function () {
-		return L.point(0, 0);
-	};
-
-	var tileLayerMiniMap = L.tileLayer('../map/{z}/{x}/{y}.png?t={time}', {
-		maxZoom: mapinfo.maxzoom,
-		minZoom: 0,
-		maxNativeZoom: mapinfo.maxzoom,
-		tileSize: mapinfo.tilesize,
-		continuousWorld: true,
-		tms: true,
-		unloadInvisibleTiles: false,
-		time: initTime
-	});
-
-
-
-
-
-
-
-	// ===============================================================================================
-	// Overlays and controls
-
-	var playersOnlineMarkerGroup = L.layerGroup();
-	var playersOfflineMarkerGroup = L.markerClusterGroup({
-		maxClusterRadius: function(zoom) { return zoom == mapinfo.maxzoom ? 10 : 50; }
-	});
-
-
-	var landClaimsGroup = L.layerGroup();
-	var landClaimsClusterGroup = L.markerClusterGroup({
-		disableClusteringAtZoom: mapinfo.maxzoom,
-		singleMarkerMode: true,
-		maxClusterRadius: 50
-	});
-	var landClaimsRectGroup = L.layerGroup();
-	landClaimsGroup.addLayer(landClaimsClusterGroup);
-	landClaimsGroup.addLayer(landClaimsRectGroup);
-	var maxZoomForCluster = 4;
-
-	var baseLayers = {
-		//"Map": tileLayer
-	};
-
-	var layerControl = L.control.layers(baseLayers, null, {
-		collapsed: false
-	});
-	
-	var layerCount = 0;
-
-
-	if (hasPermission ("web.map")) {
-		tileLayer.addTo(map);
-		new L.Control.Coordinates({}).addTo(map);
-		new L.Control.ReloadTiles({layers: [tileLayer, tileLayerMiniMap]}).addTo(map);
-		layerControl.addOverlay (GetRegionLayer (mapinfo), "Region files");
-		var miniMap = new L.Control.MiniMap(tileLayerMiniMap, {
-			zoomLevelOffset: -6,
-			toggleDisplay: true
-		}).addTo(map);
-		
-		if (hasPermission ("webapi.getstats")) {
-			new L.Control.GameTime({}).addTo(map);
-		}
-	}
-	if (hasPermission ("webapi.getlandclaims")) {
-		layerControl.addOverlay (landClaimsGroup, "Land claims");
-		layerCount++;
-	}
-	if (hasPermission ("webapi.getplayerslocation")) {
-		layerControl.addOverlay (playersOfflineMarkerGroup, "Players (offline) (<span id='mapControlOfflineCount'>0</span>)");
-		layerControl.addOverlay (playersOnlineMarkerGroup, "Players (online) (<span id='mapControlOnlineCount'>0</span>)");
-		layerCount++;
-	}
-
-	if (layerCount > 0) {
-		layerControl.addTo(map);
-	}
-
-
-	var playersMappingList = {};
-
-
-
-	// ===============================================================================================
-	// Inventory dialog
-
-	var showInv = function(steamid) {
-		$.getJSON( "../api/getplayerinventory", { steamid: steamid  })
-		.done(function(data) {
-			$("#invPlayerName").text(playersMappingList[steamid].name);
-			for (var y = 0; y < BAG_ROWS; y++) {
-				for (var x = 0; x < BAG_COLS; x++) {
-					if (data.bag[y*BAG_COLS+x].count > 0) {
-						$("#bagField"+x+"_"+y).attr("style", "background-image: url(../itemicons/" + data.bag[y*BAG_COLS+x].name + ".png);").attr("title", data.bag[y*BAG_COLS+x].name);
-						$("#bagFieldText"+x+"_"+y).text(data.bag[y*BAG_COLS+x].count);
-					} else {
-						$("#bagField"+x+"_"+y).attr("style", "background-image: none;");
-						$("#bagFieldText"+x+"_"+y).text("");
-					}
-				}
-			}
-
-			for (var x = 0; x < BELT_COLS; x++) {
-				if (data.belt[x].count > 0) {
-					$("#beltField"+x).attr("style", "background-image: url(../itemicons/" + data.belt[x].name + ".png);").attr("title", data.belt[x].name);
-					$("#beltFieldText"+x).text(data.belt[x].count);
-				} else {
-					$("#beltField"+x).attr("style", "background-image: none;");
-					$("#beltFieldText"+x).text("");
-				}
-			}
-
-			$( "#playerInventoryDialog" ).css("z-index", "1010").dialog({
-				modal: true,
-				width: BAG_COLS*(INV_ITEM_WIDTH+14) + 20,
-				buttons: {
-					Ok: function() {
-						$( this ).dialog( "close" );
-					}
-				}
-			});
-		})
-		.fail(function(jqxhr, textStatus, error) {
-			console.log("Error fetching player inventory");
-		})
-		.always(function() {
-		});
-	};
-
-	for (var y = 0; y < BAG_ROWS; y++) {
-		$("#bagTable").append("<tr id=\"bagRow"+y+"\"></tr>");
-		for (var x = 0; x < BAG_COLS; x++) {
-			$("#bagRow"+y).append(
-				"<td class=\"invField\" id=\"bagField"+x+"_"+y+"\">" +
-				"<span class=\"invFieldText\" id=\"bagFieldText"+x+"_"+y+"\"></span>" +
-				"</td>");
-		}
-	}
-
-	$("#beltTable").append("<tr id=\"beltRow0\"></tr>");
-	for (var x = 0; x < BELT_COLS; x++) {
-		$("#beltRow0").append(
-			"<td class=\"invField\" id=\"beltField"+x+"\">" +
-			"<span class=\"invFieldText\" id=\"beltFieldText"+x+"\"></span>" +
-			"</td>");
-	}
-
-
-
-	// ===============================================================================================
-	// Player markers
-
-	$(".leaflet-popup-pane").on('click.action', '.inventoryButton', function(event) {
-		showInv($(this).data('steamid'));
-	});
-
-	var setPlayerMarkers = function(data) {
-		var online = 0;
-		var offline = 0;
-		$.each( data, function( key, val ) {
-			var marker;
-			if (playersMappingList.hasOwnProperty(val.steamid)) {
-				marker = playersMappingList[val.steamid].currentPosMarker;
-				marker.setLatLng([val.position.x, val.position.z]);
-			} else {
-				marker = L.marker([val.position.x, val.position.z]).bindPopup(
-					"Player: " + val.name +
-					(hasPermission ("webapi.getplayerinventory") ?
-						"<br/><a class='inventoryButton' data-steamid='"+val.steamid+"'>Show inventory</a>"
-						: "")
-				);
-				playersMappingList[val.steamid] = { online: !val.online };
-			}
-			if (playersMappingList[val.steamid].online != val.online) {
-				if (val.online) {
-					marker.setOpacity(1.0);
-					playersOfflineMarkerGroup.removeLayer(marker);
-					playersOnlineMarkerGroup.addLayer(marker);
-				} else {
-					marker.setOpacity(0.5);
-					playersOnlineMarkerGroup.removeLayer(marker);
-					playersOfflineMarkerGroup.addLayer(marker);
-				}
-			}
-			val.currentPosMarker = marker;
-			playersMappingList[val.steamid] = val;
-		
-			if (val.online)
-				online++;
-			else
-				offline++;
-		});
-		$( "#mapControlOnlineCount" ).text( online );
-		$( "#mapControlOfflineCount" ).text( offline );
-	}
-
-	var updatePlayerEvent = function() {
-		$.getJSON( "../api/getplayerslocation")
-		.done(setPlayerMarkers)
-		.fail(function(jqxhr, textStatus, error) {
-			console.log("Error fetching players list");
-		})
-		.always(function() {
-			window.setTimeout(updatePlayerEvent, 2000);
-		});
-	}
-
-	if (hasPermission ("webapi.getplayerslocation")) {
-		window.setTimeout(updatePlayerEvent, 0);
-	}
-
-
-
-	// ===============================================================================================
-	// Land claim markers
-
-	var setLandClaims = function(data) {
-		landClaimsClusterGroup.clearLayers();
-		landClaimsRectGroup.clearLayers();
-	
-		var claimPower = Math.floor(Math.log(data.claimsize) / Math.LN2);
-		var maxClusterZoomUnlimited = mapinfo.maxzoom - (claimPower - 3);
-		var maxClusterZoomLimitedMax = Math.min(maxClusterZoomUnlimited, mapinfo.maxzoom+1);
-		maxZoomForCluster = Math.max(maxClusterZoomLimitedMax, 0);
-	
-		checkClaimClustering({target: map});
-
-		var sizeHalf = Math.floor(data.claimsize / 2);
-
-		$.each( data.claimowners, function( key, val ) {
-			var steamid = val.steamid;
-			var active = val.claimactive;
-			var color = active ? "#55ff55" : "#ff0000";
-		
-			$.each( val.claims, function( key, val ) {
-				var pos = L.latLng(val.x, val.z);
-				var bounds = L.latLngBounds(L.latLng(val.x - sizeHalf, val.z - sizeHalf), L.latLng(val.x + sizeHalf, val.z + sizeHalf));
-				var r = L.rectangle(bounds, {color: color, weight: 1, opacity: 0.8, fillOpacity: 0.15});
-				var m = L.marker(pos, { clickable: false, keyboard: false, zIndexOffset:-1000, iconSize: [0,0], icon: L.divIcon({className: 'invisIcon', iconSize:[0,0]}) });
-				if (playersMappingList.hasOwnProperty(steamid)) {
-					var name = playersMappingList[steamid].name;
-				} else {
-					var name = "unknown"
-				}
-				r.bindPopup("Owner: " + name + " ("+steamid+")<br/>Position: " + val.x + " / " + val.y + " / " + val.z);
-				landClaimsRectGroup.addLayer(r);
-				landClaimsClusterGroup.addLayer(m);
-			});
-		});
-	}
-
-	var updateClaimsEvent = function() {
-		$.getJSON( "../api/getlandclaims")
-		.done(setLandClaims)
-		.fail(function(jqxhr, textStatus, error) {
-			console.log("Error fetching land claim list");
-		})
-		.always(function() {
-			//updateClaimTimer = window.setTimeout(updateClaimsEvent, 3000);
-		});
-	}
-
-
-
-	// ===============================================================================================
-	// Layer events
-
-	var updateClaimTimer;
-	map.on('overlayadd', function(e) {
-		if (e.layer == landClaimsGroup) {
-			updateClaimsEvent();
-		}
-	});
-
-	map.on('overlayremove', function(e) {
-		if (e.layer == landClaimsGroup) {
-			//window.clearTimeout(updateClaimTimer);
-		}
-	});
-
-	var checkClaimClustering = function(e) {
-		if (e.target._zoom >= maxZoomForCluster) {
-			landClaimsGroup.removeLayer(landClaimsClusterGroup);	
-		} else {
-			landClaimsGroup.addLayer(landClaimsClusterGroup);	
-		}
-	};
-
-	map.on('zoomend', checkClaimClustering);
-
-}
-
-
-
-
-function doTabs () {
-	$(".adminmenu > ul > li").addClass ("menu_button");
-	$(".admincontent > div").addClass ("contenttab");
-	$(".adminmenu .menu_button").first ().addClass ("current_tab");
-	$(".menu_button").on ('click.action', null, function (event) {
-		var menuElement = $(this);
-		var linkElement = menuElement.children ("a");
-		var linkName = linkElement.attr ("href");
-		
-		$("*").removeClass ("current_tab");
-		menuElement.addClass ("current_tab");
-		$(linkName).addClass ("current_tab");
-	});
-	
-	$(".adminmenu .menu_button").first ().click ();
-}
-
-function initMapInfo () {
-	$.getJSON( "../map/mapinfo.json")
-	.done(function(data) {
-		mapinfo.tilesize = data.blockSize;
-		mapinfo.maxzoom = data.maxZoom;
-	})
-	.fail(function(jqxhr, textStatus, error) {
-		console.log("Error fetching map information");
-	})
-	.always(function() {
-		initMap();
-	});
-}
-
-function initUser () {
-	$.getJSON( "../userstatus")
-	.done(function(data) {
-		userdata = data;
-		
-		var userdataDiv = $("#userstate");
-		if (userdata.loggedin == true) {
-			var data = userdataDiv.children ("#userstate_loggedin");
-			data.attr ("style", "display: block");
-			data.children ("#username").attr ("href", "http://steamcommunity.com/profiles/" + userdata.username);
-			data.children ("#username").html (userdata.username);
-		} else {
-			var data = userdataDiv.children ("#userstate_loggedout");
-			data.attr ("style", "display: block");
-		}
-
-		initMapInfo ();
-	})
-	.fail(function(jqxhr, textStatus, error) {
-		console.log("Error fetching user data");
-	})
-}
-
-doTabs ();
-initUser ();
Index: binary-improvements/webserver/js/inventory_dialog.js
===================================================================
--- binary-improvements/webserver/js/inventory_dialog.js	(revision 245)
+++ binary-improvements/webserver/js/inventory_dialog.js	(revision 245)
@@ -0,0 +1,109 @@
+var ITEMICONBASEURL = "../itemicons/";
+
+var BAG_COLS = 8;
+var BAG_ROWS = 4;
+var BELT_COLS = 8;
+var INV_ITEM_WIDTH = 58;
+var INV_ITEM_HEIGHT = 40;
+
+function ShowInventoryDialog (steamid) {
+	var SetCellItem = function (containerTypeName, cellIdent, itemdata) {
+		var cell = $("#" + containerTypeName + "Field"+cellIdent);
+		var text = $("#" + containerTypeName + "FieldText"+cellIdent);
+		if (itemdata.count > 0) {
+			cell.attr("style", "background-image: url(" + ITEMICONBASEURL + itemdata.name + ".png);");
+			cell.attr("title", itemdata.name);
+			text.text(itemdata.count);
+		} else {
+			cell.attr("style", "background-image: none;");
+			cell.removeAttr("title");
+			text.text("");
+		}
+	}
+	
+	var SetEquipmentItem = function (data, name, cellIdent) {
+		if (data.equipment [name] == false) {
+			SetCellItem ("equipment", cellIdent, { count: 0, name: "" });
+		} else {
+			SetCellItem ("equipment", cellIdent, { count: 1, name: data.equipment [name] });
+		}
+	}
+
+	$.getJSON( "../api/getplayerinventory", { steamid: steamid  })
+	.done(function(data) {
+		$("#invPlayerName").text(data.playername);
+		
+		for (var y = 0; y < BAG_ROWS; y++) {
+			for (var x = 0; x < BAG_COLS; x++) {
+				SetCellItem ("bag", x + "_" + y, data.bag[y*BAG_COLS+x]);
+			}
+		}
+
+		for (var x = 0; x < BELT_COLS; x++) {
+			SetCellItem ("belt", x, data.belt[x]);
+		}
+		
+		SetEquipmentItem (data, "head", "0_0");
+		SetEquipmentItem (data, "eyes", "0_1");
+		SetEquipmentItem (data, "face", "0_2");
+		SetEquipmentItem (data, "armor", "1_0");
+		SetEquipmentItem (data, "jacket", "1_1");
+		SetEquipmentItem (data, "shirt", "1_2");
+		SetEquipmentItem (data, "legarmor", "2_0");
+		SetEquipmentItem (data, "pants", "2_1");
+		SetEquipmentItem (data, "boots", "2_2");
+		SetEquipmentItem (data, "gloves", "0_4");
+		SetEquipmentItem (data, "backpack", "2_4");
+
+		$( "#playerInventoryDialog" ).css("z-index", "1010").dialog({
+			modal: true,
+			width: BAG_COLS*(INV_ITEM_WIDTH+14) + 3*(INV_ITEM_WIDTH+14) + 20,
+			buttons: {
+				Ok: function() {
+					$( this ).dialog( "close" );
+				}
+			}
+		});
+	})
+	.fail(function(jqxhr, textStatus, error) {
+		console.log("Error fetching player inventory");
+	})
+	.always(function() {
+	});
+}
+
+function SetupInventoryDialog () {
+	var CreateInvCell = function (containerTypeName, cellIdent) {
+		return "<td class=\"invField\" id=\"" + containerTypeName + "Field"+cellIdent+"\">" +
+			"<span class=\"invFieldText\" id=\"" + containerTypeName + "FieldText"+cellIdent+"\"></span>" +
+			"</td>";
+	}
+
+	for (var y = 0; y < BAG_ROWS; y++) {
+		$("#bagTable").append("<tr id=\"bagRow"+y+"\"></tr>");
+		for (var x = 0; x < BAG_COLS; x++) {
+			$("#bagRow"+y).append(CreateInvCell ("bag", x + "_" + y));
+		}
+	}
+
+	$("#beltTable").append("<tr id=\"beltRow0\"></tr>");
+	for (var x = 0; x < BELT_COLS; x++) {
+		$("#beltRow0").append(CreateInvCell ("belt", x));
+	}
+	
+	for (var y = 0; y < 5; y++) {
+		$("#equipmentTable").append("<tr id=\"equipmentRow"+y+"\"></tr>");
+		if (y == 3) {
+			$("#equipmentRow"+y).append("<td colspan=\"3\"></td>");
+		} else {
+			for (var x = 0; x < 3; x++) {
+				if (y == 4 && x == 1) {
+					$("#equipmentRow"+y).append("<td></td>");
+				} else {
+					$("#equipmentRow"+y).append(CreateInvCell ("equipment", x + "_" + y));
+				}
+			}
+		}
+	}
+}
+
Index: binary-improvements/webserver/js/leaflet.control.gametime.js
===================================================================
--- binary-improvements/webserver/js/leaflet.control.gametime.js	(revision 244)
+++ 	(revision )
@@ -1,47 +1,0 @@
-L.Control.GameTime = L.Control.extend({
-	options: {
-		position: 'bottomright'
-	},
-
-	onAdd: function (map) {
-		var name = 'control-gametime',
-		    container = L.DomUtil.create('div', name + ' webmap-control');
-	
-		container.innerHTML = "Loading ..."
-		L.DomEvent.on (container, 'mousemove', L.DomEvent.stopPropagation);
-
-		this._map = map;
-		this._div = container;
-
-		window.setTimeout($.proxy(this._updateGameTimeEvent, this), 0);
-
-		return container;
-	},
-
-	onRemove: function (map) {
-	},
-
-	_updateGameTimeEvent: function() {
-		var div = this._div;
-		$.getJSON( "../api/getstats")
-		.done(function(data) {
-			var time = "Day " + data.gametime.days + ", ";
-			if (data.gametime.hours < 10)
-				time += "0";
-			time += data.gametime.hours;
-			time += ":";
-			if (data.gametime.minutes < 10)
-				time += "0";
-			time += data.gametime.minutes;
-			div.innerHTML = time;
-		})
-		.fail(function(jqxhr, textStatus, error) {
-			console.log("Error fetching game stats");
-		})
-		.always(function() {
-		});
-		window.setTimeout($.proxy(this._updateGameTimeEvent, this), 2000);
-	}
-
-});
-
Index: binary-improvements/webserver/js/leaflet.layer.landclaims.js
===================================================================
--- binary-improvements/webserver/js/leaflet.layer.landclaims.js	(revision 245)
+++ binary-improvements/webserver/js/leaflet.layer.landclaims.js	(revision 245)
@@ -0,0 +1,76 @@
+function GetLandClaimsLayer (map, mapinfo) {
+	var landClaimsGroup = L.layerGroup();
+	var landClaimsClusterGroup = L.markerClusterGroup({
+		disableClusteringAtZoom: mapinfo.maxzoom,
+		singleMarkerMode: true,
+		maxClusterRadius: 50
+	});
+	var landClaimsRectGroup = L.layerGroup();
+	landClaimsGroup.addLayer(landClaimsClusterGroup);
+	landClaimsGroup.addLayer(landClaimsRectGroup);
+	var maxZoomForCluster = -1;
+
+
+	var setLandClaims = function(data) {
+		landClaimsClusterGroup.clearLayers();
+		landClaimsRectGroup.clearLayers();
+	
+		var claimPower = Math.floor(Math.log(data.claimsize) / Math.LN2);
+		var maxClusterZoomUnlimited = mapinfo.maxzoom - (claimPower - 3);
+		var maxClusterZoomLimitedMax = Math.min(maxClusterZoomUnlimited, mapinfo.maxzoom+1);
+		maxZoomForCluster = Math.max(maxClusterZoomLimitedMax, 0);
+	
+		checkClaimClustering({target: map});
+
+		var sizeHalf = Math.floor(data.claimsize / 2);
+
+		$.each( data.claimowners, function( key, val ) {
+			var steamid = val.steamid;
+			var active = val.claimactive;
+			var color = active ? "#55ff55" : "#ff0000";
+			if (val.playername) {
+				var name = val.playername;
+			} else {
+				var name = "&lt;unknown&gt;"
+			}
+		
+			$.each( val.claims, function( key, val ) {
+				var pos = L.latLng(val.x, val.z);
+				var bounds = L.latLngBounds(L.latLng(val.x - sizeHalf, val.z - sizeHalf), L.latLng(val.x + sizeHalf, val.z + sizeHalf));
+				var r = L.rectangle(bounds, {color: color, weight: 1, opacity: 0.8, fillOpacity: 0.15});
+				var m = L.marker(pos, { clickable: false, keyboard: false, zIndexOffset:-1000, iconSize: [0,0], icon: L.divIcon({className: 'invisIcon', iconSize:[0,0]}) });
+				r.bindPopup("Owner: " + name + " ("+steamid+")<br/>Position: " + val.x + " / " + val.y + " / " + val.z);
+				landClaimsRectGroup.addLayer(r);
+				landClaimsClusterGroup.addLayer(m);
+			});
+		});
+	}
+
+	var updateClaimsEvent = function() {
+		$.getJSON( "../api/getlandclaims")
+		.done(setLandClaims)
+		.fail(function(jqxhr, textStatus, error) {
+			console.log("Error fetching land claim list");
+		})
+	}
+
+
+	var checkClaimClustering = function(e) {
+		if (e.target._zoom >= maxZoomForCluster) {
+			landClaimsGroup.removeLayer(landClaimsClusterGroup);	
+		} else {
+			landClaimsGroup.addLayer(landClaimsClusterGroup);	
+		}
+	};
+
+	map.on('zoomend', checkClaimClustering);
+	
+	map.on('overlayadd', function(e) {
+		if (e.layer == landClaimsGroup) {
+			updateClaimsEvent();
+		}
+	});
+
+	return landClaimsGroup;
+}
+
Index: binary-improvements/webserver/js/map.js
===================================================================
--- binary-improvements/webserver/js/map.js	(revision 245)
+++ binary-improvements/webserver/js/map.js	(revision 245)
@@ -0,0 +1,219 @@
+var mapinfo = {
+	regionsize: 512,
+	chunksize: 16,
+	tilesize: 128,
+	maxzoom: 4
+}
+
+function InitMap() {
+	// ===============================================================================================
+	// 7dtd coordinate transformations
+
+	SDTD_Projection = {
+		project: function (latlng) {
+			return new L.Point(
+				(latlng.lat) / Math.pow(2, mapinfo.maxzoom),
+				(latlng.lng) / Math.pow(2, mapinfo.maxzoom) );
+		},
+		
+		unproject: function (point) {
+			return new L.LatLng(
+				point.x * Math.pow(2, mapinfo.maxzoom),
+				point.y * Math.pow(2, mapinfo.maxzoom) );
+		}
+	};
+
+	SDTD_CRS = L.extend({}, L.CRS.Simple, {
+		projection: SDTD_Projection,
+		transformation: new L.Transformation(1, 0, -1, 0),
+
+		scale: function (zoom) {
+			return Math.pow(2, zoom);
+		}
+	});
+
+
+
+
+	// ===============================================================================================
+	// Map and basic tile layers
+
+	var initTime = new Date().getTime();
+
+	map = L.map('tab_map', {
+		zoomControl: false, // Added by Zoomslider
+		zoomsliderControl: true,
+		attributionControl: false,
+		crs: SDTD_CRS
+	}).setView([0, 0], Math.max(0, mapinfo.maxzoom - 5));
+
+	var tileLayer = L.tileLayer('../map/{z}/{x}/{y}.png?t={time}', {
+		maxZoom: mapinfo.maxzoom + 1,
+		minZoom: Math.max(0, mapinfo.maxzoom - 5),
+		maxNativeZoom: mapinfo.maxzoom,
+		tileSize: mapinfo.tilesize,
+		continuousWorld: true,
+		tms: true,
+		unloadInvisibleTiles: false,
+		time: initTime
+	});
+	
+	// TileLayer w/ TMS=true fix for zoomlevel >= 8
+	tileLayer._getWrapTileNum = function () {
+		return L.point(0, 0);
+	};
+
+	var tileLayerMiniMap = L.tileLayer('../map/{z}/{x}/{y}.png?t={time}', {
+		maxZoom: mapinfo.maxzoom,
+		minZoom: 0,
+		maxNativeZoom: mapinfo.maxzoom,
+		tileSize: mapinfo.tilesize,
+		continuousWorld: true,
+		tms: true,
+		unloadInvisibleTiles: false,
+		time: initTime
+	});
+
+
+
+
+
+
+
+	// ===============================================================================================
+	// Overlays and controls
+
+	var playersOnlineMarkerGroup = L.markerClusterGroup({
+		maxClusterRadius: function(zoom) { return zoom == mapinfo.maxzoom ? 10 : 50; }
+	});
+	var playersOfflineMarkerGroup = L.markerClusterGroup({
+		maxClusterRadius: function(zoom) { return zoom == mapinfo.maxzoom ? 10 : 50; }
+	});
+
+
+
+	var baseLayers = {
+		//"Map": tileLayer
+	};
+
+	var layerControl = L.control.layers(baseLayers, null, {
+		collapsed: false
+	});
+	
+	var layerCount = 0;
+
+
+	tileLayer.addTo(map);
+	new L.Control.Coordinates({}).addTo(map);
+	new L.Control.ReloadTiles({layers: [tileLayer, tileLayerMiniMap]}).addTo(map);
+	layerControl.addOverlay (GetRegionLayer (mapinfo), "Region files");
+	var miniMap = new L.Control.MiniMap(tileLayerMiniMap, {
+		zoomLevelOffset: -6,
+		toggleDisplay: true
+	}).addTo(map);
+
+	if (HasPermission ("webapi.getlandclaims")) {
+		layerControl.addOverlay (GetLandClaimsLayer (map, mapinfo), "Land claims");
+		layerCount++;
+	}
+	if (HasPermission ("webapi.getplayerslocation")) {
+		layerControl.addOverlay (playersOfflineMarkerGroup, "Players (offline) (<span id='mapControlOfflineCount'>0</span>)");
+		layerControl.addOverlay (playersOnlineMarkerGroup, "Players (online) (<span id='mapControlOnlineCount'>0</span>)");
+		layerCount++;
+	}
+
+	if (layerCount > 0) {
+		layerControl.addTo(map);
+	}
+
+
+
+
+	var playersMappingList = {};
+
+
+
+	// ===============================================================================================
+	// Player markers
+
+	$(".leaflet-popup-pane").on('click.action', '.inventoryButton', function(event) {
+		ShowInventoryDialog ($(this).data('steamid'));
+	});
+
+	var setPlayerMarkers = function(data) {
+		var online = 0;
+		var offline = 0;
+		$.each( data, function( key, val ) {
+			var marker;
+			if (playersMappingList.hasOwnProperty(val.steamid)) {
+				marker = playersMappingList[val.steamid].currentPosMarker;
+				marker.setLatLng([val.position.x, val.position.z]);
+			} else {
+				marker = L.marker([val.position.x, val.position.z]).bindPopup(
+					"Player: " + val.name +
+					(HasPermission ("webapi.getplayerinventory") ?
+						"<br/><a class='inventoryButton' data-steamid='"+val.steamid+"'>Show inventory</a>"
+						: "")
+				);
+				playersMappingList[val.steamid] = { online: !val.online };
+			}
+			if (playersMappingList[val.steamid].online != val.online) {
+				if (val.online) {
+					marker.setOpacity(1.0);
+					playersOfflineMarkerGroup.removeLayer(marker);
+					playersOnlineMarkerGroup.addLayer(marker);
+				} else {
+					marker.setOpacity(0.5);
+					playersOnlineMarkerGroup.removeLayer(marker);
+					playersOfflineMarkerGroup.addLayer(marker);
+				}
+			}
+			val.currentPosMarker = marker;
+			playersMappingList[val.steamid] = val;
+		
+			if (val.online)
+				online++;
+			else
+				offline++;
+		});
+		$( "#mapControlOnlineCount" ).text( online );
+		$( "#mapControlOfflineCount" ).text( offline );
+	}
+
+	var updatePlayerEvent = function() {
+		$.getJSON( "../api/getplayerslocation")
+		.done(setPlayerMarkers)
+		.fail(function(jqxhr, textStatus, error) {
+			console.log("Error fetching players list");
+		})
+		.always(function() {
+			window.setTimeout(updatePlayerEvent, 2000);
+		});
+	}
+
+	if (HasPermission ("webapi.getplayerslocation")) {
+		window.setTimeout(updatePlayerEvent, 0);
+	}
+
+
+}
+
+
+
+
+
+function StartMapModule () {
+	$.getJSON( "../map/mapinfo.json")
+	.done(function(data) {
+		mapinfo.tilesize = data.blockSize;
+		mapinfo.maxzoom = data.maxZoom;
+	})
+	.fail(function(jqxhr, textStatus, error) {
+		console.log ("Error fetching map information");
+	})
+	.always(function() {
+		InitMap ();
+	});
+}
+
+
Index: binary-improvements/webserver/js/permissions.js
===================================================================
--- binary-improvements/webserver/js/permissions.js	(revision 245)
+++ binary-improvements/webserver/js/permissions.js	(revision 245)
@@ -0,0 +1,50 @@
+var userdata = false;
+
+function InitPermissions () {
+	$.getJSON( "../userstatus")
+	.done(function(data) {
+		userdata = data;
+		
+		var userdataDiv = $("#userstate");
+		if (userdata.loggedin == true) {
+			var data = userdataDiv.children ("#userstate_loggedin");
+			data.attr ("style", "display: block");
+			data.children ("#username").attr ("href", "http://steamcommunity.com/profiles/" + userdata.username);
+			data.children ("#username").html (userdata.username);
+		} else {
+			var data = userdataDiv.children ("#userstate_loggedout");
+			data.attr ("style", "display: block");
+		}
+		
+		if (HasPermission ("webapi.getstats")) {
+			$("#serverstats").attr ("style", "display: block");
+		}
+		
+		ApplyTabPermissions ();
+
+		if (HasPermission ("web.map")) {
+			StartMapModule ();
+		}		
+		
+		if (HasPermission ("webapi.getstats")) {
+			StartStatsModule ();
+		}
+
+	})
+	.fail(function(jqxhr, textStatus, error) {
+		console.log("Error fetching user data");
+	})
+	.always(function () {
+		$("#nopermissionwarning").attr ("style", "display: block");
+	})
+}
+
+function HasPermission (modulename) {
+	for (var i = 0; i < userdata.permissions.length; i++) {
+		if (userdata.permissions [i].module == modulename) {
+			return userdata.permissions [i].allowed;
+		}
+	}
+	return false;
+}
+
Index: binary-improvements/webserver/js/stats.js
===================================================================
--- binary-improvements/webserver/js/stats.js	(revision 245)
+++ binary-improvements/webserver/js/stats.js	(revision 245)
@@ -0,0 +1,26 @@
+function StartStatsModule () {
+	var updateGameTimeEvent = function() {
+		$.getJSON( "../api/getstats")
+		.done(function(data) {
+			var time = "Day " + data.gametime.days + ", ";
+			if (data.gametime.hours < 10)
+				time += "0";
+			time += data.gametime.hours;
+			time += ":";
+			if (data.gametime.minutes < 10)
+				time += "0";
+			time += data.gametime.minutes;
+
+			$("#stats_time").html (time);
+			$("#stats_players").html (data.players);
+		})
+		.fail(function(jqxhr, textStatus, error) {
+			console.log("Error fetching game stats");
+		})
+		.always(function() {
+		});
+		window.setTimeout(updateGameTimeEvent, 2000);
+	};
+	updateGameTimeEvent();
+}
+
Index: binary-improvements/webserver/js/tabs.js
===================================================================
--- binary-improvements/webserver/js/tabs.js	(revision 245)
+++ binary-improvements/webserver/js/tabs.js	(revision 245)
@@ -0,0 +1,27 @@
+function InitializeTabs () {
+	$("#adminmenu > ul > li").addClass ("menu_button");
+	$(".admincontent > div").addClass ("contenttab");
+	$(".menu_button").on ('click.action', null, function (event) {
+		var menuElement = $(this);
+		var linkElement = menuElement.children ("a");
+		var linkName = linkElement.attr ("href");
+		
+		$("*").removeClass ("current_tab");
+		menuElement.addClass ("current_tab");
+		$(linkName).addClass ("current_tab");
+	});
+}
+
+function ApplyTabPermissions () {
+	$("#adminmenu .menu_button").each (function () {
+		if ($(this).children ("a").data ("permission")) {
+			var perm = $(this).children ("a").data ("permission");
+			if (HasPermission (perm)) {
+				$(this).addClass ("allowed");
+			}
+		}
+	});
+
+	$("#adminmenu .allowed").first ().click ();
+}
+
Index: binary-improvements/webserver/js/util.js
===================================================================
--- binary-improvements/webserver/js/util.js	(revision 244)
+++ binary-improvements/webserver/js/util.js	(revision 245)
@@ -11,11 +11,2 @@
 }
 
-function hasPermission (modulename) {
-	for (var i = 0; i < userdata.permissions.length; i++) {
-		if (userdata.permissions [i].module == modulename) {
-			return userdata.permissions [i].allowed;
-		}
-	}
-	return false;
-}
-
