feat: add factory sample machines and robot pricing conversations
This commit is contained in:
+119
-95
@@ -14,37 +14,41 @@
|
||||
// vertex count u8, height in 0.5 m u8, first vertex (x u16, z u16) in `step` metres from that origin,
|
||||
// then each further vertex as (dx i16, dz i16) from the one before. Buildings are sorted west to
|
||||
// east in 100 m strips, so neighbours sit next to each other and the small deltas gzip well.
|
||||
import {createReadStream, readFileSync, writeFileSync, mkdirSync} from 'node:fs';
|
||||
import {createGunzip} from 'node:zlib';
|
||||
import {createInterface} from 'node:readline';
|
||||
import {toLocal} from '../src/utils/geo.js';
|
||||
import {FACTORY} from '../src/constants/locations.js';
|
||||
import {createCollisionWorld, blocked} from '../src/game/world/collision.js';
|
||||
import { createReadStream, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { createGunzip } from 'node:zlib';
|
||||
import { createInterface } from 'node:readline';
|
||||
import { toLocal } from '../src/utils/geo.js';
|
||||
import { FACTORY } from '../src/constants/locations.js';
|
||||
import { createCollisionWorld, blocked } from '../src/game/world/collision.js';
|
||||
|
||||
const [source] = process.argv.slice(2);
|
||||
if (!source) {
|
||||
console.error('usage: node scripts/build-buildings.mjs <global-buildings .csv.gz>');
|
||||
process.exit(1);
|
||||
console.error('usage: node scripts/build-buildings.mjs <global-buildings .csv.gz>');
|
||||
process.exit(1);
|
||||
}
|
||||
const STEP = .5;
|
||||
const STEP = 0.5;
|
||||
const data = JSON.parse(readFileSync(new URL('../src/data/mellat.json', import.meta.url), 'utf8'));
|
||||
const {minX, minZ, maxX, maxZ} = data.bounds;
|
||||
const osm = createCollisionWorld(data.buildings.map(b => b.slice(1)));
|
||||
const areasOf = kind => data.areas.filter(a => data.areaKinds[a[0]] === kind).map(a => a.slice(1));
|
||||
const industrial = createCollisionWorld(areasOf('industrial')), parks = createCollisionWorld([...areasOf('park'), ...areasOf('forest')]);
|
||||
const { minX, minZ, maxX, maxZ } = data.bounds;
|
||||
const osm = createCollisionWorld(data.buildings.map((b) => b.slice(1)));
|
||||
const areasOf = (kind) =>
|
||||
data.areas.filter((a) => data.areaKinds[a[0]] === kind).map((a) => a.slice(1));
|
||||
const industrial = createCollisionWorld(areasOf('industrial')),
|
||||
parks = createCollisionWorld([...areasOf('park'), ...areasOf('forest')]);
|
||||
|
||||
// The factory compound in its own frame (u along the driveway, v across), with a margin.
|
||||
const normal = {x: -FACTORY.axis.z, z: FACTORY.axis.x};
|
||||
const normal = { x: -FACTORY.axis.z, z: FACTORY.axis.x };
|
||||
const inFactory = (x, z) => {
|
||||
const dx = x - FACTORY.center.x, dz = z - FACTORY.center.z;
|
||||
const u = dx * FACTORY.axis.x + dz * FACTORY.axis.z, v = dx * normal.x + dz * normal.z;
|
||||
return u > -115 && u < 85 && v > -55 && v < 55;
|
||||
const dx = x - FACTORY.center.x,
|
||||
dz = z - FACTORY.center.z;
|
||||
const u = dx * FACTORY.axis.x + dz * FACTORY.axis.z,
|
||||
v = dx * normal.x + dz * normal.z;
|
||||
return u > -115 && u < 85 && v > -55 && v < 55;
|
||||
};
|
||||
|
||||
const hash = n => {
|
||||
n = Math.imul(n ^ (n >>> 16), 0x45d9f3b);
|
||||
n = Math.imul(n ^ (n >>> 16), 0x45d9f3b);
|
||||
return ((n ^ (n >>> 16)) >>> 0) / 4294967296;
|
||||
const hash = (n) => {
|
||||
n = Math.imul(n ^ (n >>> 16), 0x45d9f3b);
|
||||
n = Math.imul(n ^ (n >>> 16), 0x45d9f3b);
|
||||
return ((n ^ (n >>> 16)) >>> 0) / 4294967296;
|
||||
};
|
||||
|
||||
// Outside the Payambar-e Azam ring (west of ~59.47°E or north of ~36.40°N) the region is industrial
|
||||
@@ -52,99 +56,119 @@ const hash = n => {
|
||||
const outskirts = (x, z) => x < -6000 || z < -9400;
|
||||
|
||||
// Storeys of 3.2 m, picked from the footprint area; warehouses are one tall storey.
|
||||
export function estimateHeight(area, {inIndustrial, inPark, inOutskirts}, seed) {
|
||||
const r = hash(seed), pick = (lo, hi) => lo + Math.floor(r * (hi - lo + 1));
|
||||
if (inPark) return 3.2;
|
||||
if ((inIndustrial || inOutskirts) && area >= 250) return 6 + r * 3;
|
||||
if (inOutskirts) return pick(1, 2) * 3.2;
|
||||
if (inIndustrial && area >= 150) return 7 + r * 3;
|
||||
if (area < 40) return 3.2;
|
||||
if (area < 120) return pick(2, 3) * 3.2;
|
||||
if (area < 300) return pick(3, 4) * 3.2;
|
||||
if (area < 800) return pick(4, 6) * 3.2;
|
||||
return pick(5, 8) * 3.2;
|
||||
export function estimateHeight(area, { inIndustrial, inPark, inOutskirts }, seed) {
|
||||
const r = hash(seed),
|
||||
pick = (lo, hi) => lo + Math.floor(r * (hi - lo + 1));
|
||||
if (inPark) return 3.2;
|
||||
if ((inIndustrial || inOutskirts) && area >= 250) return 6 + r * 3;
|
||||
if (inOutskirts) return pick(1, 2) * 3.2;
|
||||
if (inIndustrial && area >= 150) return 7 + r * 3;
|
||||
if (area < 40) return 3.2;
|
||||
if (area < 120) return pick(2, 3) * 3.2;
|
||||
if (area < 300) return pick(3, 4) * 3.2;
|
||||
if (area < 800) return pick(4, 6) * 3.2;
|
||||
return pick(5, 8) * 3.2;
|
||||
}
|
||||
|
||||
const polygonArea = pts => {
|
||||
let a = 0;
|
||||
for (let i = 0; i < pts.length; i += 2) {
|
||||
const j = (i + 2) % pts.length;
|
||||
a += pts[i] * pts[j + 1] - pts[j] * pts[i + 1];
|
||||
}
|
||||
return Math.abs(a / 2);
|
||||
const polygonArea = (pts) => {
|
||||
let a = 0;
|
||||
for (let i = 0; i < pts.length; i += 2) {
|
||||
const j = (i + 2) % pts.length;
|
||||
a += pts[i] * pts[j + 1] - pts[j] * pts[i + 1];
|
||||
}
|
||||
return Math.abs(a / 2);
|
||||
};
|
||||
|
||||
const out = [];
|
||||
let read = 0, outside = 0, duplicate = 0, factory = 0, tiny = 0;
|
||||
const lines = createInterface({input: createReadStream(source).pipe(createGunzip())});
|
||||
let read = 0,
|
||||
outside = 0,
|
||||
duplicate = 0,
|
||||
factory = 0,
|
||||
tiny = 0;
|
||||
const lines = createInterface({ input: createReadStream(source).pipe(createGunzip()) });
|
||||
for await (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
read++;
|
||||
const ring = JSON.parse(line).geometry.coordinates[0];
|
||||
const pts = [];
|
||||
for (const [lon, lat] of ring.slice(0, -1)) {
|
||||
const p = toLocal(lat, lon);
|
||||
pts.push(p.x, p.z);
|
||||
}
|
||||
let cx = 0, cz = 0;
|
||||
for (let i = 0; i < pts.length; i += 2) {
|
||||
cx += pts[i];
|
||||
cz += pts[i + 1];
|
||||
}
|
||||
cx /= pts.length / 2;
|
||||
cz /= pts.length / 2;
|
||||
if (cx < minX || cx > maxX || cz < minZ || cz > maxZ) {
|
||||
outside++;
|
||||
continue;
|
||||
}
|
||||
if (pts.length / 2 > 255 || pts.length < 6) continue;
|
||||
const area = polygonArea(pts);
|
||||
if (area < 12) {
|
||||
tiny++;
|
||||
continue;
|
||||
}
|
||||
if (blocked(osm, cx, cz)) {
|
||||
duplicate++;
|
||||
continue;
|
||||
}
|
||||
if (inFactory(cx, cz)) {
|
||||
factory++;
|
||||
continue;
|
||||
}
|
||||
const height = estimateHeight(area, {inIndustrial: blocked(industrial, cx, cz), inPark: blocked(parks, cx, cz), inOutskirts: outskirts(cx, cz)}, Math.round(cx * 7 + cz * 13));
|
||||
out.push({height, pts});
|
||||
if (!line.trim()) continue;
|
||||
read++;
|
||||
const ring = JSON.parse(line).geometry.coordinates[0];
|
||||
const pts = [];
|
||||
for (const [lon, lat] of ring.slice(0, -1)) {
|
||||
const p = toLocal(lat, lon);
|
||||
pts.push(p.x, p.z);
|
||||
}
|
||||
let cx = 0,
|
||||
cz = 0;
|
||||
for (let i = 0; i < pts.length; i += 2) {
|
||||
cx += pts[i];
|
||||
cz += pts[i + 1];
|
||||
}
|
||||
cx /= pts.length / 2;
|
||||
cz /= pts.length / 2;
|
||||
if (cx < minX || cx > maxX || cz < minZ || cz > maxZ) {
|
||||
outside++;
|
||||
continue;
|
||||
}
|
||||
if (pts.length / 2 > 255 || pts.length < 6) continue;
|
||||
const area = polygonArea(pts);
|
||||
if (area < 12) {
|
||||
tiny++;
|
||||
continue;
|
||||
}
|
||||
if (blocked(osm, cx, cz)) {
|
||||
duplicate++;
|
||||
continue;
|
||||
}
|
||||
if (inFactory(cx, cz)) {
|
||||
factory++;
|
||||
continue;
|
||||
}
|
||||
const height = estimateHeight(
|
||||
area,
|
||||
{
|
||||
inIndustrial: blocked(industrial, cx, cz),
|
||||
inPark: blocked(parks, cx, cz),
|
||||
inOutskirts: outskirts(cx, cz),
|
||||
},
|
||||
Math.round(cx * 7 + cz * 13),
|
||||
);
|
||||
out.push({ height, pts });
|
||||
}
|
||||
|
||||
let bytes = 20;
|
||||
for (const b of out) bytes += 2 + b.pts.length * 2;
|
||||
const buffer = new ArrayBuffer(bytes), view = new DataView(buffer);
|
||||
const buffer = new ArrayBuffer(bytes),
|
||||
view = new DataView(buffer);
|
||||
[0x52, 0x4d, 0x42, 0x32].forEach((c, i) => view.setUint8(i, c));
|
||||
out.sort((a, b) => Math.floor(a.pts[1] / 100) - Math.floor(b.pts[1] / 100) || a.pts[0] - b.pts[0]);
|
||||
view.setUint32(4, out.length, true);
|
||||
// Coordinates are stored from a point 200 m outside the region, so edge buildings never go negative.
|
||||
const originX = minX - 200, originZ = minZ - 200;
|
||||
const originX = minX - 200,
|
||||
originZ = minZ - 200;
|
||||
view.setFloat32(8, originX, true);
|
||||
view.setFloat32(12, originZ, true);
|
||||
view.setFloat32(16, STEP, true);
|
||||
let o = 20;
|
||||
for (const b of out) {
|
||||
view.setUint8(o++, b.pts.length / 2);
|
||||
view.setUint8(o++, Math.min(255, Math.round(b.height / .5)));
|
||||
let px = 0, pz = 0;
|
||||
for (let i = 0; i < b.pts.length; i += 2) {
|
||||
const qx = Math.round((b.pts[i] - originX) / STEP), qz = Math.round((b.pts[i + 1] - originZ) / STEP);
|
||||
if (i === 0) {
|
||||
view.setUint16(o, qx, true);
|
||||
view.setUint16(o + 2, qz, true);
|
||||
} else {
|
||||
view.setInt16(o, qx - px, true);
|
||||
view.setInt16(o + 2, qz - pz, true);
|
||||
}
|
||||
px = qx;
|
||||
pz = qz;
|
||||
o += 4;
|
||||
view.setUint8(o++, b.pts.length / 2);
|
||||
view.setUint8(o++, Math.min(255, Math.round(b.height / 0.5)));
|
||||
let px = 0,
|
||||
pz = 0;
|
||||
for (let i = 0; i < b.pts.length; i += 2) {
|
||||
const qx = Math.round((b.pts[i] - originX) / STEP),
|
||||
qz = Math.round((b.pts[i + 1] - originZ) / STEP);
|
||||
if (i === 0) {
|
||||
view.setUint16(o, qx, true);
|
||||
view.setUint16(o + 2, qz, true);
|
||||
} else {
|
||||
view.setInt16(o, qx - px, true);
|
||||
view.setInt16(o + 2, qz - pz, true);
|
||||
}
|
||||
px = qx;
|
||||
pz = qz;
|
||||
o += 4;
|
||||
}
|
||||
}
|
||||
mkdirSync(new URL('../public/data/', import.meta.url), {recursive: true});
|
||||
mkdirSync(new URL('../public/data/', import.meta.url), { recursive: true });
|
||||
writeFileSync(new URL('../public/data/buildings.bin', import.meta.url), new Uint8Array(buffer));
|
||||
console.log(`read ${read}, kept ${out.length}, outside ${outside}, OSM duplicates ${duplicate}, factory ${factory}, tiny ${tiny}, ${(bytes / 1e6).toFixed(1)} MB`);
|
||||
console.log(
|
||||
`read ${read}, kept ${out.length}, outside ${outside}, OSM duplicates ${duplicate}, factory ${factory}, tiny ${tiny}, ${(bytes / 1e6).toFixed(1)} MB`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user