feat: persist direct edits with revision rebase
This commit is contained in:
@@ -15,9 +15,14 @@ const { generate, validateDocument, runtime } = require('../src/native-traffic-s
|
||||
const { convertGeoJson } = require('../src/reference/gaode');
|
||||
const { exportNativeRoadPackage } = require('../src/export/native-road-package');
|
||||
const { compileInput } = require('../src/compile/compiler');
|
||||
const { ensureRevisionStore, setActiveAreaConfig } = require('../src/compile/road-revisions');
|
||||
const {
|
||||
createCheckpoint,
|
||||
ensureRevisionStore,
|
||||
readRevision,
|
||||
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');
|
||||
const { loadEditDocument, saveEditDocument, validateEditDocument } = require('../src/compile/native-road-edits');
|
||||
|
||||
function startWorkbench({
|
||||
area = null,
|
||||
@@ -90,8 +95,44 @@ function handle(request, response, session) {
|
||||
})
|
||||
.then((value) => sendJson(response, 200, value))
|
||||
.catch((error) => sendJson(response, 400, { ok: false, error: error.message }));
|
||||
if (request.method === 'POST' && url.pathname === '/api/edits')
|
||||
return readBody(request)
|
||||
.then((body) => {
|
||||
if (!area) throw new Error('请先导入 OSM 文件。');
|
||||
return saveEdits(area, body);
|
||||
})
|
||||
.then((value) => sendJson(response, 200, value))
|
||||
.catch((error) =>
|
||||
sendJson(response, error.statusCode || 400, {
|
||||
ok: false,
|
||||
error: error.message,
|
||||
...(error.current ? { current: error.current } : {}),
|
||||
}),
|
||||
);
|
||||
if (request.method === 'POST' && url.pathname === '/api/revisions')
|
||||
return readBody(request)
|
||||
.then((body) => {
|
||||
if (!area) throw new Error('请先导入 OSM 文件。');
|
||||
const revision = createCheckpoint(path.dirname(area.input), body.label);
|
||||
return { ok: true, revision: revision.manifest, ...editState(area) };
|
||||
})
|
||||
.then((value) => sendJson(response, 200, value))
|
||||
.catch((error) => sendJson(response, 400, { ok: false, error: error.message }));
|
||||
const rebase = /^\/api\/revisions\/(rev-\d{4})\/rebase$/.exec(url.pathname);
|
||||
if (request.method === 'POST' && rebase)
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
if (!area) throw new Error('请先导入 OSM 文件。');
|
||||
return rebaseEdits(area, rebase[1]);
|
||||
})
|
||||
.then((value) => sendJson(response, 200, value))
|
||||
.catch((error) => sendJson(response, 400, { ok: false, error: error.message }));
|
||||
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,
|
||||
...(session.area ? activeRevisionFields(session.area) : {}),
|
||||
});
|
||||
if (request.method === 'POST' && url.pathname === '/api/import')
|
||||
return readUpload(request, session)
|
||||
.then((result) =>
|
||||
@@ -190,7 +231,7 @@ function handle(request, response, session) {
|
||||
function state(area, junctionReference = null, debug = false) {
|
||||
// This is the read entry point for both a fresh import and a reopened legacy
|
||||
// workspace. Initialization only adds the v2 layout; it never rewrites v1 files.
|
||||
ensureRevisionStore(path.dirname(area.input), area.nativeRoad);
|
||||
const revisionStore = ensureRevisionStore(path.dirname(area.input), area.nativeRoad);
|
||||
const nativeDir = area.outputs.nativeRoadDir;
|
||||
const osm2streetsRoadSurface = area.outputs.geojsonDir
|
||||
? path.join(area.outputs.geojsonDir, 'road_surface.geojson')
|
||||
@@ -206,6 +247,8 @@ function state(area, junctionReference = null, debug = false) {
|
||||
const compiled = readCompiled(area);
|
||||
return {
|
||||
areaId: area.id,
|
||||
activeRevisionId: revisionStore.active.activeRevisionId,
|
||||
documentVersion: revisionStore.active.documentVersion,
|
||||
debug: debug ? { junctionCandidates: junctionCandidates(compiled) } : null,
|
||||
compiled,
|
||||
overrides: loadOverrides(area.outputs.nativeRoadOverrides),
|
||||
@@ -232,6 +275,11 @@ function state(area, junctionReference = null, debug = false) {
|
||||
};
|
||||
}
|
||||
|
||||
function activeRevisionFields(area) {
|
||||
const store = ensureRevisionStore(path.dirname(area.input), area.nativeRoad);
|
||||
return { activeRevisionId: store.active.activeRevisionId, documentVersion: store.active.documentVersion };
|
||||
}
|
||||
|
||||
function editState(area) {
|
||||
const workspace = path.dirname(area.input);
|
||||
const initialized = ensureRevisionStore(workspace, area.nativeRoad);
|
||||
@@ -308,6 +356,59 @@ function editPreview(area, body = {}) {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function saveEdits(area, body = {}) {
|
||||
const workspace = path.dirname(area.input);
|
||||
const initialized = ensureRevisionStore(workspace, area.nativeRoad);
|
||||
const current = loadEditDocument(initialized.paths.activeEdits);
|
||||
if (!Number.isInteger(body.expectedDocumentVersion)) throw new Error('expectedDocumentVersion must be an integer.');
|
||||
if (body.expectedDocumentVersion !== current.documentVersion) {
|
||||
const error = new Error('编辑文档版本已变化,请刷新后重试。');
|
||||
error.statusCode = 409;
|
||||
error.current = { documentVersion: current.documentVersion };
|
||||
throw error;
|
||||
}
|
||||
const candidate = body.document
|
||||
? { ...body.document, documentVersion: current.documentVersion }
|
||||
: {
|
||||
...current,
|
||||
...(Array.isArray(body.constraints) ? { constraints: body.constraints } : {}),
|
||||
...(Array.isArray(body.operations) ? { operations: body.operations } : {}),
|
||||
documentVersion: current.documentVersion,
|
||||
};
|
||||
const saved = saveEditDocument(initialized.paths.activeEdits, candidate);
|
||||
writeJsonAtomic(initialized.paths.activeState, {
|
||||
...initialized.active,
|
||||
documentVersion: saved.documentVersion,
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
document: saved,
|
||||
documentVersion: saved.documentVersion,
|
||||
activeRevisionId: initialized.active.activeRevisionId,
|
||||
};
|
||||
}
|
||||
|
||||
function rebaseEdits(area, revisionId) {
|
||||
const workspace = path.dirname(area.input);
|
||||
const revision = readRevision(workspace, revisionId);
|
||||
const model = compileRoadModel(fs.readFileSync(area.input, 'utf8'), loadOverrides(area.outputs.nativeRoadOverrides));
|
||||
const resolution = resolveDirectEditConstraints(model, revision.directEdits, {
|
||||
revisionId,
|
||||
compilerGeometryVersion: GEOMETRY_VERSION,
|
||||
});
|
||||
const counts = Object.fromEntries(
|
||||
['exact', 'recheck', 'pending', 'conflicted', 'stale'].map((status) => [status, 0]),
|
||||
);
|
||||
for (const state of resolution.constraintStates) counts[state.status] = (counts[state.status] || 0) + 1;
|
||||
return {
|
||||
ok: true,
|
||||
revisionId,
|
||||
counts,
|
||||
constraints: resolution.constraintStates,
|
||||
diagnostics: resolution.diagnostics,
|
||||
};
|
||||
}
|
||||
// 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
|
||||
// the inspector can offer a ready-to-paste cluster配置.
|
||||
@@ -525,4 +626,4 @@ function sendJson(response, status, value) {
|
||||
response.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
||||
response.end(`${JSON.stringify(value)}\n`);
|
||||
}
|
||||
module.exports = { startWorkbench, readMultipart, editState, editPreview };
|
||||
module.exports = { startWorkbench, readMultipart, editState, editPreview, saveEdits, rebaseEdits };
|
||||
|
||||
Reference in New Issue
Block a user