feat: add direct edit preview APIs
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
"road-compiler": "bin/road-compiler.js"
|
"road-compiler": "bin/road-compiler.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node test/index.js && node test/native-road-edits.js && node test/road-revisions.js && node test/direct-edit-solver.js && node test/fixtures.js",
|
"test": "node test/index.js && node test/native-road-edits.js && node test/road-revisions.js && node test/direct-edit-solver.js && node test/workbench-edit-api.js && node test/fixtures.js",
|
||||||
"test:fixtures:update-baseline": "node test/update-fixture-baselines.js",
|
"test:fixtures:update-baseline": "node test/update-fixture-baselines.js",
|
||||||
"road:workbench": "node bin/road-workbench.js",
|
"road:workbench": "node bin/road-workbench.js",
|
||||||
"road:export": "node bin/road-compiler.js",
|
"road:export": "node bin/road-compiler.js",
|
||||||
|
|||||||
61
test/workbench-edit-api.js
Normal file
61
test/workbench-edit-api.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('assert/strict');
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
const { compileInput } = require('../src/compile/compiler');
|
||||||
|
const { ensureRevisionStore } = require('../src/compile/road-revisions');
|
||||||
|
const { editState, editPreview } = require('../workbench/server');
|
||||||
|
|
||||||
|
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'workbench-edit-api-'));
|
||||||
|
const input = {
|
||||||
|
areaId: 'edit-api-test',
|
||||||
|
osmFile: path.join(workspace, 'source.osm'),
|
||||||
|
outDir: path.join(workspace, 'outputs', 'native-road'),
|
||||||
|
stagingDir: path.join(workspace, 'outputs', '_pipeline'),
|
||||||
|
overridesFile: path.join(workspace, 'native-road-overrides.json'),
|
||||||
|
trafficSignalsFile: path.join(workspace, 'native-traffic-signals.json'),
|
||||||
|
options: { edgeLines: false, junctionTemplates: { enabled: false, references: [] } },
|
||||||
|
};
|
||||||
|
fs.copyFileSync(path.join(__dirname, 'fixtures', 'fengshu-er-road.osm'), input.osmFile);
|
||||||
|
fs.writeFileSync(input.overridesFile, `${JSON.stringify({ schema: 'native-road-overrides/v1', overrides: [] })}\n`);
|
||||||
|
fs.writeFileSync(
|
||||||
|
input.trafficSignalsFile,
|
||||||
|
`${JSON.stringify({ schema: 'native-traffic-signals/v1', provenance: 'empty', assemblies: { type: 'FeatureCollection', features: [] } })}\n`,
|
||||||
|
);
|
||||||
|
const initialized = ensureRevisionStore(workspace, input.options);
|
||||||
|
const area = compileInput({
|
||||||
|
...input,
|
||||||
|
areaConfigSnapshotFile: initialized.paths.activeAreaConfig,
|
||||||
|
editsFile: initialized.paths.activeEdits,
|
||||||
|
}).area;
|
||||||
|
|
||||||
|
const state = editState(area);
|
||||||
|
assert.equal(state.active, true);
|
||||||
|
assert.equal(state.documentVersion, 0);
|
||||||
|
assert.equal(state.handles.schema, 'road-edit-handles/v1');
|
||||||
|
assert.equal(state.activeRevisionId, 'rev-0001');
|
||||||
|
|
||||||
|
const trackedFiles = [
|
||||||
|
initialized.paths.activeEdits,
|
||||||
|
initialized.paths.activeState,
|
||||||
|
initialized.paths.activeAreaConfig,
|
||||||
|
].map((file) => ({
|
||||||
|
file,
|
||||||
|
bytes: fs.readFileSync(file),
|
||||||
|
mtimeMs: fs.statSync(file).mtimeMs,
|
||||||
|
}));
|
||||||
|
const preview = editPreview(area, { previewSeq: 17, document: state.document });
|
||||||
|
assert.equal(preview.ok, true);
|
||||||
|
assert.equal(preview.previewSeq, 17);
|
||||||
|
assert.equal(preview.degraded, false);
|
||||||
|
assert.equal(preview.handles.schema, 'road-edit-handles/v1');
|
||||||
|
assert.ok(preview.layers.roadSurface);
|
||||||
|
for (const tracked of trackedFiles) {
|
||||||
|
assert.deepEqual(fs.readFileSync(tracked.file), tracked.bytes, `${tracked.file} bytes unchanged by preview`);
|
||||||
|
assert.equal(fs.statSync(tracked.file).mtimeMs, tracked.mtimeMs, `${tracked.file} mtime unchanged by preview`);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.rmSync(workspace, { recursive: true, force: true });
|
||||||
|
console.log('workbench edit API tests passed');
|
||||||
@@ -4,12 +4,20 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { loadOverrides, validateOverrides, writeJsonAtomic } = require('../src/compile/native-road');
|
const {
|
||||||
|
compileRoadModel,
|
||||||
|
compileGeometry,
|
||||||
|
loadOverrides,
|
||||||
|
validateOverrides,
|
||||||
|
writeJsonAtomic,
|
||||||
|
} = require('../src/compile/native-road');
|
||||||
const { generate, validateDocument, runtime } = require('../src/native-traffic-signals');
|
const { generate, validateDocument, runtime } = require('../src/native-traffic-signals');
|
||||||
const { convertGeoJson } = require('../src/reference/gaode');
|
const { convertGeoJson } = require('../src/reference/gaode');
|
||||||
const { exportNativeRoadPackage } = require('../src/export/native-road-package');
|
const { exportNativeRoadPackage } = require('../src/export/native-road-package');
|
||||||
const { compileInput } = require('../src/compile/compiler');
|
const { compileInput } = require('../src/compile/compiler');
|
||||||
const { ensureRevisionStore, setActiveAreaConfig } = require('../src/compile/road-revisions');
|
const { ensureRevisionStore, setActiveAreaConfig } = require('../src/compile/road-revisions');
|
||||||
|
const { GEOMETRY_VERSION, resolveDirectEditConstraints } = require('../src/compile/direct-edit-solver');
|
||||||
|
const { loadEditDocument, validateEditDocument } = require('../src/compile/native-road-edits');
|
||||||
|
|
||||||
function startWorkbench({
|
function startWorkbench({
|
||||||
area = null,
|
area = null,
|
||||||
@@ -69,6 +77,19 @@ function handle(request, response, session) {
|
|||||||
return area
|
return area
|
||||||
? sendJson(response, 200, state(area, junctionReference, debug))
|
? sendJson(response, 200, state(area, junctionReference, debug))
|
||||||
: sendJson(response, 200, { active: false });
|
: sendJson(response, 200, { active: false });
|
||||||
|
if (request.method === 'GET' && url.pathname === '/api/edit-state')
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => (area ? editState(area) : { active: false }))
|
||||||
|
.then((value) => sendJson(response, 200, value))
|
||||||
|
.catch((error) => sendJson(response, 400, { ok: false, error: error.message }));
|
||||||
|
if (request.method === 'POST' && url.pathname === '/api/edit-preview')
|
||||||
|
return readBody(request)
|
||||||
|
.then((body) => {
|
||||||
|
if (!area) throw new Error('请先导入 OSM 文件。');
|
||||||
|
return editPreview(area, body);
|
||||||
|
})
|
||||||
|
.then((value) => sendJson(response, 200, value))
|
||||||
|
.catch((error) => sendJson(response, 400, { ok: false, error: error.message }));
|
||||||
if (request.method === 'GET' && url.pathname === '/api/session')
|
if (request.method === 'GET' && url.pathname === '/api/session')
|
||||||
return sendJson(response, 200, { active: Boolean(session.area), areaId: session.area?.id || null });
|
return sendJson(response, 200, { active: Boolean(session.area), areaId: session.area?.id || null });
|
||||||
if (request.method === 'POST' && url.pathname === '/api/import')
|
if (request.method === 'POST' && url.pathname === '/api/import')
|
||||||
@@ -210,6 +231,83 @@ function state(area, junctionReference = null, debug = false) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function editState(area) {
|
||||||
|
const workspace = path.dirname(area.input);
|
||||||
|
const initialized = ensureRevisionStore(workspace, area.nativeRoad);
|
||||||
|
const directEdits = loadEditDocument(initialized.paths.activeEdits);
|
||||||
|
const model = compileRoadModel(fs.readFileSync(area.input, 'utf8'), loadOverrides(area.outputs.nativeRoadOverrides));
|
||||||
|
const resolution = resolveDirectEditConstraints(model, directEdits, {
|
||||||
|
revisionId: initialized.active.activeRevisionId,
|
||||||
|
compilerGeometryVersion: GEOMETRY_VERSION,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
active: true,
|
||||||
|
areaId: area.id,
|
||||||
|
activeRevisionId: initialized.active.activeRevisionId,
|
||||||
|
documentVersion: directEdits.documentVersion,
|
||||||
|
document: directEdits,
|
||||||
|
constraintStates: resolution.constraintStates,
|
||||||
|
diagnostics: resolution.diagnostics,
|
||||||
|
handles: resolution.handles,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function editPreview(area, body = {}) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const workspace = path.dirname(area.input);
|
||||||
|
const activeEditsFile = path.join(workspace, 'active', 'native-road-edits.json');
|
||||||
|
const activeStateFile = path.join(workspace, 'active', 'state.json');
|
||||||
|
if (!fs.existsSync(activeEditsFile) || !fs.existsSync(activeStateFile))
|
||||||
|
throw new Error('编辑工作区尚未初始化,请先重新导入 OSM 文件。');
|
||||||
|
const activeEdits = loadEditDocument(activeEditsFile);
|
||||||
|
const draft = body.document
|
||||||
|
? validateEditDocument(body.document)
|
||||||
|
: validateEditDocument({
|
||||||
|
...activeEdits,
|
||||||
|
...(Array.isArray(body.constraints) ? { constraints: body.constraints } : {}),
|
||||||
|
...(Array.isArray(body.operations) ? { operations: body.operations } : {}),
|
||||||
|
});
|
||||||
|
const activeState = readJson(activeStateFile);
|
||||||
|
const model = compileRoadModel(fs.readFileSync(area.input, 'utf8'), loadOverrides(area.outputs.nativeRoadOverrides));
|
||||||
|
const previewSeq = Number.isInteger(body.previewSeq) ? body.previewSeq : 0;
|
||||||
|
const resolution = resolveDirectEditConstraints(model, draft, {
|
||||||
|
revisionId: activeState.activeRevisionId,
|
||||||
|
previewSeq,
|
||||||
|
compilerGeometryVersion: GEOMETRY_VERSION,
|
||||||
|
});
|
||||||
|
const compiled = compileGeometry(model, loadOverrides(area.outputs.nativeRoadOverrides), {
|
||||||
|
edgeLines: area.nativeRoad.edgeLines,
|
||||||
|
junctionTemplates: area.nativeRoad.junctionTemplates,
|
||||||
|
directEdit: resolution,
|
||||||
|
});
|
||||||
|
const diagnostics = [...compiled.diagnostics, ...resolution.diagnostics];
|
||||||
|
const degraded = Date.now() - startedAt > 300;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
previewSeq,
|
||||||
|
degraded,
|
||||||
|
revisionId: activeState.activeRevisionId,
|
||||||
|
documentVersion: activeEdits.documentVersion,
|
||||||
|
constraintStates: resolution.constraintStates,
|
||||||
|
diagnostics,
|
||||||
|
handles: resolution.handles,
|
||||||
|
layers: {
|
||||||
|
roadSurface: compiled.roadSurface,
|
||||||
|
edgeLines: compiled.edgeLines,
|
||||||
|
sidewalkSurface: compiled.sidewalkSurface,
|
||||||
|
intersectionSurface: compiled.intersectionSurface,
|
||||||
|
laneCenterlines: compiled.laneCenterlines,
|
||||||
|
laneSeparators: compiled.laneSeparators,
|
||||||
|
centerLines: compiled.centerLines,
|
||||||
|
directionArrows: compiled.directionArrows,
|
||||||
|
turnArrows: compiled.turnArrows,
|
||||||
|
crosswalks: compiled.crosswalks,
|
||||||
|
vehicleStopLines: compiled.vehicleStopLines,
|
||||||
|
connectors: compiled.connectors,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
// The compiler reports candidates as advisory diagnostics. Lift them into their
|
// The compiler reports candidates as advisory diagnostics. Lift them into their
|
||||||
// own payload with a stable index so the map can label them "#1, #2, ..." and
|
// own payload with a stable index so the map can label them "#1, #2, ..." and
|
||||||
// the inspector can offer a ready-to-paste cluster配置.
|
// the inspector can offer a ready-to-paste cluster配置.
|
||||||
@@ -427,4 +525,4 @@ function sendJson(response, status, value) {
|
|||||||
response.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
response.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
||||||
response.end(`${JSON.stringify(value)}\n`);
|
response.end(`${JSON.stringify(value)}\n`);
|
||||||
}
|
}
|
||||||
module.exports = { startWorkbench, readMultipart };
|
module.exports = { startWorkbench, readMultipart, editState, editPreview };
|
||||||
|
|||||||
Reference in New Issue
Block a user