MinedMap/viewer/MinedMap.js

472 lines
10 KiB
JavaScript
Raw Normal View History

// bsearch-based array element check
function contains(array, elem) {
let min = 0, max = array.length;
while (min < max) {
const i = min + Math.floor((max-min)/2);
const cur = array[i];
if (cur === elem)
return true;
else if (cur < elem)
min = i + 1;
else
max = i;
}
return false;
}
const signKinds = {
sign: {
iconSize: [26, 28],
popupAnchor: [0, -20],
},
wall_sign: {
iconSize: [26, 18],
popupAnchor: [0, -15],
},
hanging_sign: {
iconSize: [28, 24],
popupAnchor: [0, -18],
},
hanging_wall_sign: {
iconSize: [28, 28],
popupAnchor: [0, -20],
},
}
const params = {};
const signIcons = {};
const markers = {};
let updateHash = () => {};
function coordKey(coords) {
if (!coords)
return null;
return `${coords[0]},${coords[1]}`;
}
function getMarker(coords) {
return markers[coordKey(coords)];
}
function signIcon(material, kind) {
function createSignIcon(material, kind) {
2024-01-07 01:56:03 +01:00
const {iconSize, popupAnchor} = signKinds[kind];
return L.icon({
iconUrl: `images/icon/${material}_${kind}.png`,
2024-01-07 01:56:03 +01:00
iconSize,
popupAnchor,
shadowUrl: `images/icon/shadow_${kind}.png`,
shadowSize: [iconSize[0]+8, iconSize[1]+8],
className: 'overzoomed',
});
}
let icons = signIcons[material] ??= {};
return icons[kind] ??= createSignIcon(material, kind);
}
2024-01-03 04:05:41 +01:00
const MinedMapLayer = L.TileLayer.extend({
initialize: function (mipmaps, layer) {
2024-01-03 04:05:41 +01:00
L.TileLayer.prototype.initialize.call(this, '', {
detectRetina: true,
tileSize: 512,
zoomReverse: true,
minZoom: -(mipmaps.length-1),
maxZoom: 0,
attribution: 'Generated by <a href="https://github.com/neocturne/MinedMap">MinedMap</a>',
});
this.options.maxNativeZoom = this.options.maxZoom;
this.options.maxZoom = undefined;
this.mipmaps = mipmaps;
this.layer = layer;
2015-02-02 09:08:36 +01:00
},
createTile: function (coords, done) {
2024-01-03 04:05:41 +01:00
const tile = L.TileLayer.prototype.createTile.call(this, coords, done);
2024-01-03 04:05:41 +01:00
if (coords.z - this.options.zoomOffset >= 0)
L.DomUtil.addClass(tile, 'overzoomed');
2015-02-02 09:08:36 +01:00
return tile;
},
2024-01-03 04:05:41 +01:00
getTileUrl: function (coords) {
let z = -coords.z + this.options.zoomOffset;
if (z < 0)
z = 0;
2015-02-02 09:08:36 +01:00
2024-01-03 04:05:41 +01:00
const mipmap = this.mipmaps[z];
2020-06-20 14:04:59 +02:00
2024-01-03 04:05:41 +01:00
if (coords.x < mipmap.bounds.minX || coords.x > mipmap.bounds.maxX ||
coords.y < mipmap.bounds.minZ || coords.y > mipmap.bounds.maxZ ||
!contains(mipmap.regions[coords.y] || [], coords.x))
return L.Util.emptyImageUrl;
2020-06-20 14:04:59 +02:00
2024-01-03 04:05:41 +01:00
return 'data/'+this.layer+'/'+z+'/r.'+coords.x+'.'+coords.y+'.png';
2020-06-20 14:04:59 +02:00
},
2015-02-02 09:08:36 +01:00
});
const CoordControl = L.Control.extend({
2015-02-04 07:22:49 +01:00
initialize: function () {
this.options.position = 'bottomleft';
},
onAdd: function (map) {
this._container = L.DomUtil.create('div', 'leaflet-control-attribution');
return this._container;
},
update: function (x, z) {
if (!this._map) { return; }
this._container.innerHTML = 'X: ' + x + '&nbsp;&nbsp;&nbsp;Z: ' + z;
}
});
const parseHash = function () {
const args = {};
2015-02-04 17:00:53 +01:00
if (window.location.hash) {
const parts = window.location.hash.substring(1).split('&');
2015-02-04 17:00:53 +01:00
for (const part of parts) {
const key_value = part.split('=');
const key = key_value[0], value = key_value.slice(1).join('=');
2015-02-04 17:00:53 +01:00
args[key] = value;
}
}
return args;
}
function formatSignLine(line) {
const el = document.createElement('span');
el.style.whiteSpace = 'pre';
for (const span of line) {
const child = document.createElement('span');
child.textContent = span.text;
let color = span.color ?? '';
if (color[0] !== '#')
color = '#000000';
if (span.bold)
child.style.fontWeight = 'bold';
if (span.italic)
child.style.fontStyle = 'italic';
child.style.textDecoration = '';
if (span.underlined)
child.style.textDecoration += ' underline';
if (span.strikethrough)
child.style.textDecoration += ' line-through';
child.style.color = color;
if (span.obfuscated) {
child.style.backgroundColor = color;
child.className = 'obfuscated';
}
el.appendChild(child);
}
return el;
}
2024-01-07 16:01:29 +01:00
function createSign(sign, back) {
// standing signs
function px(base) {
const scale = 11;
return (base*scale)+'px';
}
// hanging signs
function pxh(base) {
const scale = 16;
return (base*scale)+'px';
}
const sizes = {
sign: {
width: px(24),
height: px(12),
paddingTop: px(0),
paddingBottom: px(14),
},
wall_sign: {
width: px(24),
height: px(12),
paddingTop: px(0),
paddingBottom: px(0),
},
hanging_sign: {
width: pxh(16),
height: pxh(10),
paddingTop: pxh(4),
paddingBottom: pxh(0),
},
hanging_wall_sign: {
width: pxh(16),
height: pxh(10),
paddingTop: pxh(6),
paddingBottom: pxh(0),
},
};
const size = sizes[sign.kind];
const wrapper = document.createElement('div');
wrapper.classList = 'sign-wrapper';
const title = document.createElement('div');
title.classList = 'sign-title'
title.textContent = `Sign at ${sign.x}/${sign.y}/${sign.z}`;
if (back)
title.textContent += ' (back)';
title.textContent += ':';
wrapper.appendChild(title);
const container = document.createElement('div');
container.style.width = size.width;
container.style.height = size.height;
container.style.paddingTop = size.paddingTop;
container.style.paddingBottom = size.paddingBottom;
container.style.backgroundImage = `url(images/bg/${sign.material}_${sign.kind}.png)`;
container.classList = 'sign-container overzoomed';
const content = document.createElement('div');
content.classList = 'sign-content';
let text = [];
if (!back && sign.front_text)
text = sign.front_text;
else if (back && sign.back_text)
text = sign.back_text;
for (const line of text) {
content.appendChild(formatSignLine(line));
content.appendChild(document.createElement('br'));
}
container.appendChild(content);
wrapper.appendChild(container);
return wrapper;
}
async function loadSigns(signLayer) {
const response = await fetch('data/entities.json', {cache: 'no-store'});
const res = await response.json();
const groups = {};
// Group signs by x,z coordinates
for (const sign of res.signs) {
const key = coordKey([sign.x, sign.z]);
const group = groups[key] ??= [];
group.push(sign);
}
for (const [key, group] of Object.entries(groups)) {
const el = document.createElement('div');
let material;
let kind;
// Sort from top to bottom
group.sort((a, b) => b.y - a.y);
for (const sign of group) {
el.appendChild(createSign(sign, false));
if (sign.back_text)
el.appendChild(createSign(sign, true));
material ??= sign.material;
kind ??= sign.kind;
}
// Default material
material ??= 'oak';
const [x, z] = key.split(',').map((i) => +i);
const popup = L.popup().setContent(el);
popup.on('add', () => {
params.marker = [x, z];
updateHash();
});
popup.on('remove', () => {
params.marker = null;
updateHash();
});
const marker = L.marker([-z-0.5, x+0.5], {
icon: signIcon(material, kind),
}).addTo(signLayer).bindPopup(popup);
markers[coordKey([x, z])] = marker;
if (params.marker && x === params.marker[0] && z === params.marker[1])
marker.openPopup();
}
}
2015-02-04 17:00:53 +01:00
2015-02-02 09:08:36 +01:00
window.createMap = function () {
(async function () {
const response = await fetch('data/info.json', {cache: 'no-store'});
const res = await response.json();
const {mipmaps, spawn} = res;
const features = res.features || {};
2015-02-02 09:08:36 +01:00
const updateParams = function () {
const args = parseHash();
2015-02-04 22:01:14 +01:00
params.zoom = parseInt(args['zoom']);
params.x = parseFloat(args['x']);
params.z = parseFloat(args['z']);
params.light = parseInt(args['light']);
params.signs = parseInt(args['signs'] ?? '1');
params.marker = (args['marker'] ?? '').split(',').map((i) => +i);
if (isNaN(params.zoom))
params.zoom = 0;
if (isNaN(params.x))
params.x = spawn.x;
if (isNaN(params.z))
params.z = spawn.z;
if (!features.signs || isNaN(params.marker[0]) || isNaN(params.marker[1]))
params.marker = null;
2015-02-04 22:01:14 +01:00
};
updateParams();
2015-02-04 17:00:53 +01:00
const map = L.map('map', {
center: [-params.z, params.x],
zoom: params.zoom,
minZoom: -(mipmaps.length-1),
maxZoom: 5,
2015-02-02 09:08:36 +01:00
crs: L.CRS.Simple,
maxBounds: [
[-512*(mipmaps[0].bounds.maxZ+1), 512*mipmaps[0].bounds.minX],
[-512*mipmaps[0].bounds.minZ, 512*(mipmaps[0].bounds.maxX+1)],
2015-02-02 09:08:36 +01:00
],
});
const overlayMaps = {};
2015-02-03 13:17:36 +01:00
const mapLayer = new MinedMapLayer(mipmaps, 'map');
2015-02-03 13:17:36 +01:00
mapLayer.addTo(map);
const lightLayer = new MinedMapLayer(mipmaps, 'light');
overlayMaps['Illumination'] = lightLayer;
if (params.light)
2015-02-04 17:00:53 +01:00
map.addLayer(lightLayer);
let signLayer;
if (features.signs) {
signLayer = L.layerGroup();
loadSigns(signLayer);
if (params.signs)
map.addLayer(signLayer);
overlayMaps['Signs'] = signLayer;
}
2015-02-03 13:17:36 +01:00
L.control.layers({}, overlayMaps).addTo(map);
2015-02-04 07:22:49 +01:00
const coordControl = new CoordControl();
2015-02-04 07:22:49 +01:00
coordControl.addTo(map);
map.on('mousemove', function(e) {
coordControl.update(Math.round(e.latlng.lng), Math.round(-e.latlng.lat));
});
2015-02-04 17:00:53 +01:00
const makeHash = function () {
let ret = '#x='+params.x+'&z='+params.z;
2015-02-04 22:01:14 +01:00
if (params.zoom != 0)
ret += '&zoom='+params.zoom;
2015-02-04 17:00:53 +01:00
if (map.hasLayer(lightLayer))
2015-02-04 22:01:14 +01:00
ret += '&light=1';
if (features.signs && !map.hasLayer(signLayer))
ret += '&signs=0';
if (params.marker) {
ret += `&marker=${params.marker[0]},${params.marker[1]}`;
}
2015-02-04 17:00:53 +01:00
return ret;
};
updateHash = function () {
2015-02-04 17:00:53 +01:00
window.location.hash = makeHash();
};
const refreshHash = function (ev) {
if (ev.type === 'layeradd' || ev.type === 'layerremove') {
if (ev.layer !== lightLayer && ev.layer !== signLayer)
return;
}
const center = map.getCenter();
params.zoom = map.getZoom();
params.x = Math.round(center.lng);
params.z = Math.round(-center.lat);
2015-02-04 22:01:14 +01:00
updateHash();
}
2015-02-04 17:19:56 +01:00
updateHash();
2015-02-04 22:01:14 +01:00
map.on('moveend', refreshHash);
map.on('zoomend', refreshHash);
map.on('layeradd', refreshHash);
map.on('layerremove', refreshHash);
2015-02-04 17:00:53 +01:00
window.onhashchange = function () {
if (window.location.hash === makeHash())
2015-02-04 17:00:53 +01:00
return;
const prevMarkerCoords = params.marker;
2015-02-04 17:00:53 +01:00
updateParams();
2015-02-04 17:00:53 +01:00
if (params.light)
2015-02-04 17:00:53 +01:00
map.addLayer(lightLayer);
else
map.removeLayer(lightLayer);
if (features.signs) {
if (params.signs)
map.addLayer(signLayer);
else
map.removeLayer(signLayer);
if (coordKey(prevMarkerCoords) !== coordKey(params.marker))
getMarker(params.marker)?.openPopup();
}
2015-02-04 22:01:14 +01:00
map.setView([-params.z, params.x], params.zoom);
2015-02-04 22:01:14 +01:00
updateHash();
2015-02-04 17:00:53 +01:00
};
})();
2015-02-02 09:08:36 +01:00
}