doom-osm-godmode 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +332 -0
- package/examples/colosseum.json +8 -0
- package/examples/demo-site.geojson +101 -0
- package/examples/demo-site.json +7 -0
- package/package.json +56 -0
- package/src/cli.js +68 -0
- package/src/feature-set.js +155 -0
- package/src/geo.js +98 -0
- package/src/geocode.js +26 -0
- package/src/layout.js +233 -0
- package/src/lib/cli.js +67 -0
- package/src/lib/fs.js +38 -0
- package/src/osm.js +110 -0
- package/src/udmf.js +207 -0
- package/src/wad.js +67 -0
- package/src/workflow.js +171 -0
package/src/osm.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const { closeRing } = require('./geo.js');
|
|
2
|
+
|
|
3
|
+
const USER_AGENT = 'doom-osm-wad/0.1.0';
|
|
4
|
+
const OVERPASS_URL = 'https://overpass-api.de/api/interpreter';
|
|
5
|
+
|
|
6
|
+
function buildOverpassQuery(bbox) {
|
|
7
|
+
const bounds = `${bbox.south},${bbox.west},${bbox.north},${bbox.east}`;
|
|
8
|
+
|
|
9
|
+
return `
|
|
10
|
+
[out:json][timeout:45];
|
|
11
|
+
(
|
|
12
|
+
way["building"](${bounds});
|
|
13
|
+
way["natural"="water"](${bounds});
|
|
14
|
+
way["water"](${bounds});
|
|
15
|
+
way["leisure"~"park|garden|pitch"](${bounds});
|
|
16
|
+
way["landuse"~"grass|meadow|recreation_ground|village_green"](${bounds});
|
|
17
|
+
);
|
|
18
|
+
out tags geom;
|
|
19
|
+
`.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function inferKind(tags = {}) {
|
|
23
|
+
if (tags.building) {
|
|
24
|
+
return 'building';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (tags.natural === 'water' || tags.water) {
|
|
28
|
+
return 'water';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
tags.leisure === 'park' ||
|
|
33
|
+
tags.leisure === 'garden' ||
|
|
34
|
+
tags.leisure === 'pitch' ||
|
|
35
|
+
tags.landuse === 'grass' ||
|
|
36
|
+
tags.landuse === 'meadow' ||
|
|
37
|
+
tags.landuse === 'recreation_ground' ||
|
|
38
|
+
tags.landuse === 'village_green'
|
|
39
|
+
) {
|
|
40
|
+
return 'park';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function overpassToFeatureCollection(payload) {
|
|
47
|
+
const features = [];
|
|
48
|
+
|
|
49
|
+
for (const element of payload.elements || []) {
|
|
50
|
+
if (element.type !== 'way' || !Array.isArray(element.geometry) || element.geometry.length < 3) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const kind = inferKind(element.tags);
|
|
55
|
+
if (!kind) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ring = closeRing(
|
|
60
|
+
element.geometry.map((point) => [Number(point.lon), Number(point.lat)])
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (!Array.isArray(ring) || ring.length < 4) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
features.push({
|
|
68
|
+
type: 'Feature',
|
|
69
|
+
properties: {
|
|
70
|
+
id: `way/${element.id}`,
|
|
71
|
+
name: element.tags?.name || null,
|
|
72
|
+
kind,
|
|
73
|
+
tags: element.tags || {}
|
|
74
|
+
},
|
|
75
|
+
geometry: {
|
|
76
|
+
type: 'Polygon',
|
|
77
|
+
coordinates: [ring]
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
type: 'FeatureCollection',
|
|
84
|
+
features
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function fetchFeatureCollectionForBbox(bbox) {
|
|
89
|
+
const query = buildOverpassQuery(bbox);
|
|
90
|
+
const response = await fetch(OVERPASS_URL, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: {
|
|
93
|
+
'content-type': 'text/plain;charset=UTF-8',
|
|
94
|
+
'user-agent': USER_AGENT
|
|
95
|
+
},
|
|
96
|
+
body: query
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
throw new Error(`Overpass request failed with ${response.status}.`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const payload = await response.json();
|
|
104
|
+
return overpassToFeatureCollection(payload);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
fetchFeatureCollectionForBbox
|
|
109
|
+
};
|
|
110
|
+
|
package/src/udmf.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
function formatValue(value) {
|
|
2
|
+
if (typeof value === 'string') {
|
|
3
|
+
return `"${value}"`;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (typeof value === 'boolean') {
|
|
7
|
+
return value ? 'true' : 'false';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (typeof value === 'number') {
|
|
11
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(3);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return String(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function serializeBlock(type, fields) {
|
|
18
|
+
const lines = [`${type}`, '{'];
|
|
19
|
+
|
|
20
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
21
|
+
if (value === undefined || value === null) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
lines.push(` ${key} = ${formatValue(value)};`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
lines.push('}');
|
|
28
|
+
return lines.join('\n');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildTextMap(layout) {
|
|
32
|
+
const vertices = [];
|
|
33
|
+
const vertexIndexByKey = new Map();
|
|
34
|
+
const sectors = [];
|
|
35
|
+
const sidedefs = [];
|
|
36
|
+
const linedefs = [];
|
|
37
|
+
const sectorIndexByCell = new Map();
|
|
38
|
+
|
|
39
|
+
const addVertex = (x, y) => {
|
|
40
|
+
const key = `${x},${y}`;
|
|
41
|
+
if (vertexIndexByKey.has(key)) {
|
|
42
|
+
return vertexIndexByKey.get(key);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const index = vertices.length;
|
|
46
|
+
vertices.push({ x, y });
|
|
47
|
+
vertexIndexByKey.set(key, index);
|
|
48
|
+
return index;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const addSidedef = (fields) => {
|
|
52
|
+
const index = sidedefs.length;
|
|
53
|
+
sidedefs.push(fields);
|
|
54
|
+
return index;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const cellToWorld = (cell) => {
|
|
58
|
+
const x = cell.x * layout.cellSize;
|
|
59
|
+
const y = (layout.gridBounds.maxY - cell.y) * layout.cellSize;
|
|
60
|
+
return {
|
|
61
|
+
topLeft: { x, y: y + layout.cellSize },
|
|
62
|
+
topRight: { x: x + layout.cellSize, y: y + layout.cellSize },
|
|
63
|
+
bottomRight: { x: x + layout.cellSize, y },
|
|
64
|
+
bottomLeft: { x, y }
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
for (const cell of layout.cells) {
|
|
69
|
+
const sectorIndex = sectors.length;
|
|
70
|
+
sectors.push({
|
|
71
|
+
heightfloor: cell.theme.floorHeight,
|
|
72
|
+
heightceiling: cell.theme.ceilingHeight,
|
|
73
|
+
texturefloor: cell.theme.floorTexture,
|
|
74
|
+
textureceiling: cell.theme.ceilingTexture,
|
|
75
|
+
lightlevel: cell.theme.lightLevel
|
|
76
|
+
});
|
|
77
|
+
sectorIndexByCell.set(`${cell.x},${cell.y}`, sectorIndex);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const neighborOffsets = [
|
|
81
|
+
{ dx: 0, dy: -1, edge: 'north' },
|
|
82
|
+
{ dx: 1, dy: 0, edge: 'east' },
|
|
83
|
+
{ dx: 0, dy: 1, edge: 'south' },
|
|
84
|
+
{ dx: -1, dy: 0, edge: 'west' }
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const edgeVertices = (points, edge) => {
|
|
88
|
+
if (edge === 'north') {
|
|
89
|
+
return [points.topLeft, points.topRight];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (edge === 'east') {
|
|
93
|
+
return [points.topRight, points.bottomRight];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (edge === 'south') {
|
|
97
|
+
return [points.bottomRight, points.bottomLeft];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return [points.bottomLeft, points.topLeft];
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
for (const cell of layout.cells) {
|
|
104
|
+
const currentSector = sectorIndexByCell.get(`${cell.x},${cell.y}`);
|
|
105
|
+
const points = cellToWorld(cell);
|
|
106
|
+
|
|
107
|
+
for (const neighborOffset of neighborOffsets) {
|
|
108
|
+
const neighborKey = `${cell.x + neighborOffset.dx},${cell.y + neighborOffset.dy}`;
|
|
109
|
+
const neighborSector = sectorIndexByCell.get(neighborKey);
|
|
110
|
+
|
|
111
|
+
if (neighborSector !== undefined && currentSector > neighborSector) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const [start, end] = edgeVertices(points, neighborOffset.edge);
|
|
116
|
+
const v1 = addVertex(start.x, start.y);
|
|
117
|
+
const v2 = addVertex(end.x, end.y);
|
|
118
|
+
|
|
119
|
+
if (neighborSector === undefined) {
|
|
120
|
+
const front = addSidedef({
|
|
121
|
+
sector: currentSector,
|
|
122
|
+
texturetop: '-',
|
|
123
|
+
texturebottom: '-',
|
|
124
|
+
texturemiddle: cell.theme.wallTexture
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
linedefs.push({
|
|
128
|
+
v1,
|
|
129
|
+
v2,
|
|
130
|
+
sidefront: front,
|
|
131
|
+
blocking: true
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const front = addSidedef({
|
|
137
|
+
sector: currentSector,
|
|
138
|
+
texturetop: '-',
|
|
139
|
+
texturebottom: '-',
|
|
140
|
+
texturemiddle: '-'
|
|
141
|
+
});
|
|
142
|
+
const back = addSidedef({
|
|
143
|
+
sector: neighborSector,
|
|
144
|
+
texturetop: '-',
|
|
145
|
+
texturebottom: '-',
|
|
146
|
+
texturemiddle: '-'
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
linedefs.push({
|
|
150
|
+
v1,
|
|
151
|
+
v2,
|
|
152
|
+
sidefront: front,
|
|
153
|
+
sideback: back,
|
|
154
|
+
twosided: true
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const startWorldX = (layout.playerStart.x + 0.5) * layout.cellSize;
|
|
160
|
+
const startWorldY = (layout.gridBounds.maxY - layout.playerStart.y + 0.5) * layout.cellSize;
|
|
161
|
+
|
|
162
|
+
const blocks = [
|
|
163
|
+
'namespace = "ZDoom";',
|
|
164
|
+
'',
|
|
165
|
+
...vertices.map((vertex) => serializeBlock('vertex', vertex)),
|
|
166
|
+
'',
|
|
167
|
+
...linedefs.map((linedef) => serializeBlock('linedef', linedef)),
|
|
168
|
+
'',
|
|
169
|
+
...sidedefs.map((sidedef) => serializeBlock('sidedef', sidedef)),
|
|
170
|
+
'',
|
|
171
|
+
...sectors.map((sector) => serializeBlock('sector', sector)),
|
|
172
|
+
'',
|
|
173
|
+
serializeBlock('thing', {
|
|
174
|
+
x: startWorldX,
|
|
175
|
+
y: startWorldY,
|
|
176
|
+
angle: 90,
|
|
177
|
+
type: 1,
|
|
178
|
+
skill1: true,
|
|
179
|
+
skill2: true,
|
|
180
|
+
skill3: true,
|
|
181
|
+
skill4: true,
|
|
182
|
+
skill5: true,
|
|
183
|
+
single: true,
|
|
184
|
+
cooperative: true,
|
|
185
|
+
dm: true
|
|
186
|
+
})
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
return `${blocks.join('\n\n')}\n`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function buildMapInfo(mapName, title) {
|
|
193
|
+
return [
|
|
194
|
+
`map ${mapName} "${title}"`,
|
|
195
|
+
'{',
|
|
196
|
+
' sky1 = "SKY1", 0.0',
|
|
197
|
+
' music = "$MUSIC_RUNNIN"',
|
|
198
|
+
'}',
|
|
199
|
+
''
|
|
200
|
+
].join('\n');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = {
|
|
204
|
+
buildMapInfo,
|
|
205
|
+
buildTextMap
|
|
206
|
+
};
|
|
207
|
+
|
package/src/wad.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function encodeName(name) {
|
|
2
|
+
const buffer = Buffer.alloc(8);
|
|
3
|
+
buffer.write(String(name).slice(0, 8).toUpperCase(), 'ascii');
|
|
4
|
+
return buffer;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function createWadBuffer(options) {
|
|
8
|
+
const lumps = [
|
|
9
|
+
{
|
|
10
|
+
name: 'MAPINFO',
|
|
11
|
+
data: Buffer.from(options.mapInfo, 'utf8')
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: options.mapName,
|
|
15
|
+
data: Buffer.alloc(0)
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'TEXTMAP',
|
|
19
|
+
data: Buffer.from(options.textMap, 'utf8')
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'OSMJSON',
|
|
23
|
+
data: Buffer.from(options.metadataJson, 'utf8')
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'ENDMAP',
|
|
27
|
+
data: Buffer.alloc(0)
|
|
28
|
+
}
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const headerSize = 12;
|
|
32
|
+
let dataOffset = headerSize;
|
|
33
|
+
const dataBuffers = [];
|
|
34
|
+
const directoryEntries = [];
|
|
35
|
+
|
|
36
|
+
for (const lump of lumps) {
|
|
37
|
+
dataBuffers.push(lump.data);
|
|
38
|
+
directoryEntries.push({
|
|
39
|
+
filepos: dataOffset,
|
|
40
|
+
size: lump.data.length,
|
|
41
|
+
name: lump.name
|
|
42
|
+
});
|
|
43
|
+
dataOffset += lump.data.length;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const directoryOffset = dataOffset;
|
|
47
|
+
const directoryBuffer = Buffer.alloc(directoryEntries.length * 16);
|
|
48
|
+
|
|
49
|
+
directoryEntries.forEach((entry, index) => {
|
|
50
|
+
const base = index * 16;
|
|
51
|
+
directoryBuffer.writeInt32LE(entry.filepos, base);
|
|
52
|
+
directoryBuffer.writeInt32LE(entry.size, base + 4);
|
|
53
|
+
encodeName(entry.name).copy(directoryBuffer, base + 8);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const header = Buffer.alloc(headerSize);
|
|
57
|
+
header.write('PWAD', 0, 'ascii');
|
|
58
|
+
header.writeInt32LE(directoryEntries.length, 4);
|
|
59
|
+
header.writeInt32LE(directoryOffset, 8);
|
|
60
|
+
|
|
61
|
+
return Buffer.concat([header, ...dataBuffers, directoryBuffer]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
createWadBuffer
|
|
66
|
+
};
|
|
67
|
+
|
package/src/workflow.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const fs = require('node:fs/promises');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { loadFeatureCollectionFromFile, prepareFeatureSet } = require('./feature-set.js');
|
|
4
|
+
const { searchPlace } = require('./geocode.js');
|
|
5
|
+
const { bboxAroundPoint, bboxFromNominatimResult } = require('./geo.js');
|
|
6
|
+
const { ensureDir, readJson, slugify, writeJson, writeText } = require('./lib/fs.js');
|
|
7
|
+
const { buildLayout } = require('./layout.js');
|
|
8
|
+
const { fetchFeatureCollectionForBbox } = require('./osm.js');
|
|
9
|
+
const { buildMapInfo, buildTextMap } = require('./udmf.js');
|
|
10
|
+
const { createWadBuffer } = require('./wad.js');
|
|
11
|
+
|
|
12
|
+
async function buildArtifacts(featureCollection, options) {
|
|
13
|
+
const featureSet = prepareFeatureSet(featureCollection, {
|
|
14
|
+
title: options.title,
|
|
15
|
+
maxRooms: options.maxRooms
|
|
16
|
+
});
|
|
17
|
+
const layout = buildLayout(featureSet);
|
|
18
|
+
const textMap = buildTextMap(layout);
|
|
19
|
+
const mapInfo = buildMapInfo(options.mapName, options.title);
|
|
20
|
+
|
|
21
|
+
const metadata = {
|
|
22
|
+
title: options.title,
|
|
23
|
+
mapName: options.mapName,
|
|
24
|
+
source: options.source,
|
|
25
|
+
featureCount: featureSet.featureCount,
|
|
26
|
+
exportedFeatures: featureSet.features.length,
|
|
27
|
+
features: featureSet.features.map((feature) => ({
|
|
28
|
+
id: feature.id,
|
|
29
|
+
name: feature.name,
|
|
30
|
+
kind: feature.kind,
|
|
31
|
+
areaMeters: Math.round(feature.areaMeters),
|
|
32
|
+
boundsMeters: {
|
|
33
|
+
width: Number(feature.boundsMeters.width.toFixed(2)),
|
|
34
|
+
height: Number(feature.boundsMeters.height.toFixed(2))
|
|
35
|
+
}
|
|
36
|
+
}))
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const wadBuffer = createWadBuffer({
|
|
40
|
+
mapName: options.mapName,
|
|
41
|
+
mapInfo,
|
|
42
|
+
textMap,
|
|
43
|
+
metadataJson: JSON.stringify(metadata, null, 2)
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await ensureDir(options.outputDir);
|
|
47
|
+
await writeJson(path.join(options.outputDir, 'site.json'), {
|
|
48
|
+
...featureSet,
|
|
49
|
+
features: featureSet.features.map((feature) => ({
|
|
50
|
+
id: feature.id,
|
|
51
|
+
name: feature.name,
|
|
52
|
+
kind: feature.kind,
|
|
53
|
+
areaMeters: Number(feature.areaMeters.toFixed(2)),
|
|
54
|
+
boundsMeters: {
|
|
55
|
+
width: Number(feature.boundsMeters.width.toFixed(2)),
|
|
56
|
+
height: Number(feature.boundsMeters.height.toFixed(2))
|
|
57
|
+
},
|
|
58
|
+
sourceProperties: feature.sourceProperties,
|
|
59
|
+
sourceRing: feature.sourceRing
|
|
60
|
+
}))
|
|
61
|
+
});
|
|
62
|
+
await writeJson(path.join(options.outputDir, 'layout.json'), layout);
|
|
63
|
+
await writeText(path.join(options.outputDir, 'TEXTMAP.udmf'), textMap);
|
|
64
|
+
await writeText(path.join(options.outputDir, 'MAPINFO.txt'), mapInfo);
|
|
65
|
+
await fs.writeFile(path.join(options.outputDir, `${options.mapName}.wad`), wadBuffer);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
outputDir: options.outputDir,
|
|
69
|
+
mapPath: path.join(options.outputDir, `${options.mapName}.wad`),
|
|
70
|
+
featureCount: featureSet.featureCount,
|
|
71
|
+
exportedFeatures: featureSet.features.length
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function buildFromPlace(placeQuery, options = {}) {
|
|
76
|
+
const results = await searchPlace(placeQuery, 5);
|
|
77
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
78
|
+
throw new Error(`No geocoding result for "${placeQuery}".`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const best = results[0];
|
|
82
|
+
const radiusMeters = options.radiusMeters;
|
|
83
|
+
const bbox = radiusMeters
|
|
84
|
+
? bboxAroundPoint(Number(best.lat), Number(best.lon), radiusMeters)
|
|
85
|
+
: bboxFromNominatimResult(best);
|
|
86
|
+
|
|
87
|
+
const featureCollection = await fetchFeatureCollectionForBbox(bbox);
|
|
88
|
+
const title = options.title || best.display_name.split(',')[0];
|
|
89
|
+
const mapName = options.mapName || 'MAP01';
|
|
90
|
+
const outputDir = options.outputDir || path.resolve('output', slugify(title));
|
|
91
|
+
|
|
92
|
+
return buildArtifacts(featureCollection, {
|
|
93
|
+
title,
|
|
94
|
+
mapName,
|
|
95
|
+
maxRooms: options.maxRooms || 12,
|
|
96
|
+
outputDir,
|
|
97
|
+
source: {
|
|
98
|
+
type: 'place-search',
|
|
99
|
+
placeQuery,
|
|
100
|
+
chosenResult: {
|
|
101
|
+
displayName: best.display_name,
|
|
102
|
+
lat: Number(best.lat),
|
|
103
|
+
lon: Number(best.lon)
|
|
104
|
+
},
|
|
105
|
+
bbox
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function buildFromConfig(configPath) {
|
|
111
|
+
const absoluteConfigPath = path.resolve(configPath);
|
|
112
|
+
const configDir = path.dirname(absoluteConfigPath);
|
|
113
|
+
const config = await readJson(absoluteConfigPath);
|
|
114
|
+
|
|
115
|
+
if (config.inputGeoJson) {
|
|
116
|
+
const inputPath = path.resolve(configDir, config.inputGeoJson);
|
|
117
|
+
const featureCollection = await loadFeatureCollectionFromFile(inputPath);
|
|
118
|
+
return buildArtifacts(featureCollection, {
|
|
119
|
+
title: config.title || 'Imported Site',
|
|
120
|
+
mapName: config.mapName || 'MAP01',
|
|
121
|
+
maxRooms: config.maxRooms || 12,
|
|
122
|
+
outputDir: path.resolve(configDir, config.outputDir || path.join('output', slugify(config.title || 'site'))),
|
|
123
|
+
source: {
|
|
124
|
+
type: 'geojson',
|
|
125
|
+
inputGeoJson: inputPath
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (config.placeQuery) {
|
|
131
|
+
return buildFromPlace(config.placeQuery, {
|
|
132
|
+
title: config.title,
|
|
133
|
+
mapName: config.mapName,
|
|
134
|
+
maxRooms: config.maxRooms,
|
|
135
|
+
radiusMeters: config.radiusMeters,
|
|
136
|
+
outputDir: path.resolve(configDir, config.outputDir || path.join('output', slugify(config.title || config.placeQuery)))
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
throw new Error(`Config ${absoluteConfigPath} must include inputGeoJson or placeQuery.`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function buildDemo(outputDir) {
|
|
144
|
+
const configPath = path.resolve(__dirname, '..', 'examples', 'demo-site.json');
|
|
145
|
+
const result = await buildFromConfig(configPath);
|
|
146
|
+
|
|
147
|
+
if (!outputDir) {
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const targetDir = path.resolve(outputDir);
|
|
152
|
+
await ensureDir(targetDir);
|
|
153
|
+
|
|
154
|
+
const files = ['layout.json', 'MAP01.wad', 'MAPINFO.txt', 'site.json', 'TEXTMAP.udmf'];
|
|
155
|
+
for (const file of files) {
|
|
156
|
+
await fs.copyFile(path.join(result.outputDir, file), path.join(targetDir, file));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
...result,
|
|
161
|
+
outputDir: targetDir,
|
|
162
|
+
mapPath: path.join(targetDir, 'MAP01.wad')
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
buildDemo,
|
|
168
|
+
buildFromConfig,
|
|
169
|
+
buildFromPlace
|
|
170
|
+
};
|
|
171
|
+
|