Skip to content

Commit b8b6baf

Browse files
Current work on getting the wiki to be the source of truth.
1 parent 6130bc7 commit b8b6baf

File tree

7 files changed

+659
-4
lines changed

7 files changed

+659
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
*.mw-datamaps
12
*.png
23
*.tiled-session
34
node_modules

extensions/includes/api.mjs

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { InterwikiDataImpl, MetadataImpl } from './metadata.mjs';
12
import { getStringProperty, getWikiUrl } from './util.mjs';
23

34
const USER_AGENT = `tiled-datamaps/1.0 (https://github.com/utdrwiki/maps; admin@undertale.wiki) tiled/${tiled.version}`;
@@ -28,13 +29,18 @@ export function getRestUrl(language = 'en') {
2829
* @param {(value: any|PromiseLike<any>) => void} resolve Promise
2930
* resolution function
3031
* @param {(reason: any?) => void} reject Promise rejection function
32+
* @param {boolean} isArrayBuffer Whether the request expects a binary response
3133
* @returns {() => void} Ready state change handler
3234
*/
33-
const readyStateChange = (xhr, resolve, reject) => () => {
35+
const readyStateChange = (xhr, resolve, reject, isArrayBuffer = false) => () => {
3436
if (xhr.readyState === XMLHttpRequest.DONE) {
3537
if (xhr.status === 200) {
3638
try {
37-
resolve(JSON.parse(xhr.responseText));
39+
if (isArrayBuffer) {
40+
resolve(xhr.response);
41+
} else {
42+
resolve(JSON.parse(xhr.responseText));
43+
}
3844
} catch (error) {
3945
reject(new Error(`Failed to parse response: ${xhr.responseText}`));
4046
}
@@ -160,3 +166,107 @@ export function edit(title, text, summary, accessToken, language = 'en') {
160166
return response.edit;
161167
});
162168
}
169+
170+
/**
171+
* Retrieves maps from the wiki under specific criteria.
172+
* @param {object} options Options for retrieving maps
173+
* @param {string} language Wiki language
174+
* @returns {Promise<DataMap[]>} List of maps on the wiki
175+
*/
176+
function getMaps(options, language = 'en') {
177+
return httpGet(getApiUrl(language), Object.assign({
178+
action: 'query',
179+
prop: 'revisions',
180+
rvprop: 'ids|content',
181+
rvslots: 'main',
182+
format: 'json',
183+
formatversion: '2',
184+
}, options)).then(data => data.query.pages
185+
.filter((/** @type {any} */ page) =>
186+
page.revisions &&
187+
page.revisions.length > 0 &&
188+
page.revisions[0].slots &&
189+
page.revisions[0].slots.main &&
190+
page.revisions[0].slots.main.contentmodel === 'datamap'
191+
)
192+
.map((/** @type {any} */ page) => {
193+
const {slots, revid} = page.revisions[0];
194+
const /** @type {DataMap} */ datamap = JSON.parse(slots.main.content);
195+
datamap.custom = datamap.custom || new MetadataImpl();
196+
datamap.custom.interwiki = datamap.custom.interwiki || {};
197+
datamap.custom.interwiki[language] = new InterwikiDataImpl({
198+
mapName: page.title.split(':').slice(1).join(':'),
199+
});
200+
datamap.custom.interwiki[language].revision = revid;
201+
return datamap;
202+
})
203+
.filter((/** @type {DataMap} */ datamap) => !datamap.$fragment)
204+
);
205+
}
206+
207+
/**
208+
* Retrieves all maps from the wiki.
209+
* @param {string} language Wiki language
210+
* @returns {Promise<DataMap[]>} List of maps on the wiki
211+
*/
212+
export function getAllMaps(language = 'en') {
213+
return getMaps({
214+
generator: 'allpages',
215+
gapnamespace: '2900',
216+
gapfilterredir: 'nonredirects',
217+
gaplimit: 'max',
218+
}, language);
219+
}
220+
221+
/**
222+
* Retrieves a single map from the wiki.
223+
* @param {string} name Map name
224+
* @param {string} language Wiki language
225+
* @returns {Promise<DataMap>} Specified map from the wiki
226+
*/
227+
export function getMap(name, language = 'en') {
228+
return getMaps({
229+
titles: `Map:${name}`
230+
}, language).then(maps => maps[0]);
231+
}
232+
233+
/**
234+
* Returns the URLs of the given map files on the wiki.
235+
* @param {string[]} filenames Map file names
236+
* @param {string} language Wiki language
237+
* @returns {Promise<string[]>} URLs of the given map files on the wiki
238+
*/
239+
export function getFileUrls(filenames, language = 'en') {
240+
return httpGet(getApiUrl(language), {
241+
action: 'query',
242+
titles: filenames.map(name => `File:${name}`).join('|'),
243+
prop: 'imageinfo',
244+
iiprop: 'url',
245+
format: 'json',
246+
formatversion: '2'
247+
}).then(data => filenames.map(filename => data.query.pages
248+
.find((/** @type {any} */ page) => page.title === `File:${
249+
data.query.normalized
250+
?.find((/** @type {any} */ n) => n.from === filename)
251+
?.to ||
252+
filename
253+
}`)
254+
?.imageinfo[0].url)
255+
.filter(Boolean));
256+
}
257+
258+
/**
259+
* Downloads a file from a URL and returns it as an ArrayBuffer.
260+
* @param {string} url URL to download the file from
261+
* @returns {Promise<ArrayBuffer>} Downloaded file data
262+
*/
263+
export function downloadFile(url) {
264+
return new Promise((resolve, reject) => {
265+
const xhr = new XMLHttpRequest();
266+
xhr.open('GET', url, true);
267+
xhr.responseType = 'arraybuffer';
268+
xhr.onreadystatechange = readyStateChange(xhr, resolve, reject, true);
269+
xhr.setRequestHeader('User-Agent', USER_AGENT);
270+
xhr.send();
271+
});
272+
}

extensions/includes/format.mjs

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@ import {
77
getColorProperty,
88
getListProperty,
99
getNumberProperty,
10-
getStringProperty
10+
getStringProperty,
11+
getTiledColor,
12+
isBoxOverlay,
13+
isImageBackground,
14+
isPolylineOverlay,
15+
setProperty,
16+
validateTiledPoint,
17+
validateTiledRectangle
1118
} from './util.mjs';
1219

1320
/**
@@ -233,6 +240,108 @@ export function convertTiledToDataMaps(map, mapName, language = 'en') {
233240
return datamap;
234241
}
235242

243+
/**
244+
* Converts a DataMaps map to the Tiled map format.
245+
* @param {DataMaps} datamaps DataMaps from all wikis to convert
246+
* @returns {TileMap} Converted Tiled map object
247+
*/
248+
export function convertDataMapsToTiled(datamaps) {
249+
const datamap = datamaps.en;
250+
if (!datamap) {
251+
throw new Error('English map data is required for conversion');
252+
}
253+
const metadata = new MetadataImpl(datamap.custom);
254+
const primaryBg = datamap.backgrounds.find(isImageBackground);
255+
if (!primaryBg) {
256+
throw new Error('At least one background with an image is required to convert to Tiled format');
257+
}
258+
const crsBR = validateTiledPoint(datamap.crs.bottomRight);
259+
const mapWidth = Math.ceil(crsBR[0] / metadata.tileWidth);
260+
const mapHeight = Math.ceil(crsBR[1] / metadata.tileHeight);
261+
const map = new TileMap();
262+
map.tileWidth = metadata.tileWidth;
263+
map.tileHeight = metadata.tileHeight;
264+
map.width = mapWidth;
265+
map.height = mapHeight;
266+
if (datamap.disclaimer) {
267+
map.setProperty('disclaimer', datamap.disclaimer);
268+
}
269+
if (datamap.settings && datamap.settings.leaflet) {
270+
map.setProperty('popzoom', datamap.settings.leaflet.uriPopupZoom);
271+
}
272+
if (datamap.include) {
273+
map.setProperty('include', datamap.include.join('\n'));
274+
}
275+
for (const bg of datamap.backgrounds.filter(isImageBackground)) {
276+
const layer = new ImageLayer(bg.name || bg.image);
277+
const fileName = metadata.getBackgroundFileName(bg.image);
278+
const filePath = FileInfo.joinPaths(
279+
tiled.project.folders[0],
280+
'images',
281+
fileName
282+
);
283+
layer.imageFileName = filePath;
284+
layer.setProperty('image', bg.image);
285+
map.addLayer(layer);
286+
}
287+
let annotationLayer = new ObjectGroup('annotations');
288+
map.addLayer(annotationLayer);
289+
for (const overlay of (primaryBg.overlays || [])) {
290+
const obj = new MapObject(overlay.name);
291+
if (isBoxOverlay(overlay)) {
292+
obj.shape = MapObject.Rectangle;
293+
const [[x, y], [x2, y2]] = validateTiledRectangle(overlay.at);
294+
obj.pos = { x, y };
295+
obj.width = x2 - x;
296+
obj.height = y2 - y;
297+
if (overlay.color) {
298+
obj.setProperty('fill', getTiledColor(overlay.color));
299+
}
300+
if (overlay.borderColor) {
301+
obj.setProperty('border', getTiledColor(overlay.borderColor));
302+
}
303+
} else if (isPolylineOverlay(overlay)) {
304+
obj.polygon = overlay.path
305+
.map(p => validateTiledPoint(p))
306+
.map(p => ({ x: p[1], y: p[0] }));
307+
obj.pos = { x: 0, y: 0 };
308+
obj.shape = MapObject.Polyline;
309+
if (overlay.color) {
310+
obj.setProperty('color', getTiledColor(overlay.color));
311+
}
312+
if (overlay.thickness) {
313+
obj.setProperty('thickness', overlay.thickness);
314+
}
315+
}
316+
annotationLayer.addObject(obj);
317+
}
318+
// TODO: Support nested layers
319+
for (const [layerName, markers] of Object.entries(datamap.markers).reverse()) {
320+
const layer = new ObjectGroup(layerName);
321+
for (const m of markers) {
322+
const obj = new MapObject(m.name);
323+
obj.pos = {
324+
x: m.x,
325+
y: m.y
326+
};
327+
obj.shape = MapObject.Point;
328+
obj.setProperties({
329+
page: m.article,
330+
description: m.description,
331+
image: m.image,
332+
plain: m.isWikitext === undefined ? undefined : !m.isWikitext
333+
});
334+
layer.addObject(obj);
335+
}
336+
map.addLayer(layer);
337+
}
338+
for (const [language, interwiki] of Object.entries(metadata.interwiki)) {
339+
setProperty(map, 'name', interwiki.mapName, language);
340+
setProperty(map, 'revision', interwiki.revision, language);
341+
}
342+
return map;
343+
}
344+
236345
/**
237346
* Converts a Tiled map to multiple DataMaps format for each language.
238347
* @param {TileMap} map Tiled map to convert to DataMaps
@@ -276,14 +385,30 @@ function generateWrite(multiple) {
276385
};
277386
}
278387

388+
/**
389+
* Converts a DataMaps map to the Tiled map format.
390+
* @param {string} filePath Path to the file with the DataMaps map
391+
* @returns {TileMap} Tiled map
392+
*/
393+
function read(filePath) {
394+
const file = new TextFile(filePath, TextFile.ReadOnly);
395+
const content = file.readAll();
396+
file.close();
397+
const datamap = JSON.parse(content);
398+
const isMultiple = typeof datamap.en === 'object';
399+
return convertDataMapsToTiled(isMultiple ? datamap : { en: datamap });
400+
}
401+
279402
tiled.registerMapFormat('dataMaps', {
280403
extension: 'mw-datamaps',
281404
name: 'DataMaps (all wikis)',
405+
read,
282406
write: generateWrite(true)
283407
});
284408

285409
tiled.registerMapFormat('dataMap', {
286410
extension: 'mw-datamaps',
287411
name: 'DataMaps (single wiki)',
412+
read,
288413
write: generateWrite(false)
289414
});

0 commit comments

Comments
 (0)