mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
534 字
1 分钟
基于 Web Canvas 与 IndexedDB 的市政道路设施平面图编辑器

市政道路改造、地下管线协调和临时交通组织经常需要在一张平面图上维护道路中心线、路口、施工区与交通设施。这篇文章记录一个浏览器平面图编辑器的核心实现。

为了让现场网络不稳定时仍能继续编辑,方案采用 HTML5 Canvas 负责高频绘制,使用 IndexedDB 保存本地草稿和版本快照。


一、为什么选择 Canvas?#

当画布包含大量道路节点、管线交点、标志牌和施工区域时,使用大量 DOM 节点会带来明显的布局与重绘开销。Canvas 更适合这类连续缩放、平移和拖拽场景:

  • 集中绘制:全部图元由统一渲染循环管理,避免频繁触发布局计算。
  • 坐标一致:屏幕坐标可以稳定映射到工程坐标系。
  • 数据解耦:业务实体与绘制样式分离,便于独立调整数据模型和校验规则。

二、编辑器核心模型#

先把道路节点、连接关系和校验规则拆成独立模型:

export type RoadNodeType = 'SIGNAL' | 'JUNCTION' | 'WORK_ZONE';
export interface RoadNode {
id: string;
name: string;
type: RoadNodeType;
x: number;
y: number;
chainage: number;
}
export interface RoadLink {
id: string;
fromNodeId: string;
toNodeId: string;
laneClearance: number;
}
export interface SafetyRules {
minimumClearance: number;
}
export class MunicipalMapEditor {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private nodes: RoadNode[] = [];
private links: RoadLink[] = [];
private selectedNode: RoadNode | null = null;
private scale = 1;
private offsetX = 0;
private offsetY = 0;
constructor(canvas: HTMLCanvasElement, private rules: SafetyRules) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
this.bindEvents();
}
private bindEvents() {
this.canvas.addEventListener('pointerdown', (event) => {
const point = this.screenToWorld(event.clientX, event.clientY);
this.selectedNode = this.findNodeAt(point.x, point.y);
});
this.canvas.addEventListener('pointermove', (event) => {
if (!this.selectedNode) return;
const point = this.screenToWorld(event.clientX, event.clientY);
this.selectedNode.x = point.x;
this.selectedNode.y = point.y;
this.render();
});
this.canvas.addEventListener('pointerup', async () => {
this.selectedNode = null;
await this.saveLocalDraft();
});
}
private screenToWorld(screenX: number, screenY: number) {
const rect = this.canvas.getBoundingClientRect();
return {
x: (screenX - rect.left - this.offsetX) / this.scale,
y: (screenY - rect.top - this.offsetY) / this.scale,
};
}
private findNodeAt(x: number, y: number): RoadNode | null {
const radius = 15;
return this.nodes.find((node) => Math.hypot(node.x - x, node.y - y) < radius) ?? null;
}
public render() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.save();
this.ctx.translate(this.offsetX, this.offsetY);
this.ctx.scale(this.scale, this.scale);
for (const link of this.links) {
const from = this.nodes.find((node) => node.id === link.fromNodeId);
const to = this.nodes.find((node) => node.id === link.toNodeId);
if (!from || !to) continue;
this.ctx.beginPath();
this.ctx.moveTo(from.x, from.y);
this.ctx.lineTo(to.x, to.y);
this.ctx.strokeStyle =
link.laneClearance < this.rules.minimumClearance ? '#ff4d4d' : '#4dabf7';
this.ctx.stroke();
}
for (const node of this.nodes) {
this.ctx.beginPath();
this.ctx.arc(node.x, node.y, 8, 0, Math.PI * 2);
this.ctx.fillStyle = node.type === 'WORK_ZONE' ? '#e67700' : '#37b24d';
this.ctx.fill();
this.ctx.fillText(node.name, node.x + 12, node.y + 4);
}
this.ctx.restore();
}
private async saveLocalDraft() {
await localDraftDB.saveDraft('municipal_map_draft', {
nodes: this.nodes,
links: this.links,
updatedAt: Date.now(),
});
}
}

净距阈值不应硬编码,而应由当前项目采用的规范和审批配置提供,这样可以减少业务耦合,也方便按场景切换规则。


三、IndexedDB 本地草稿#

export class LocalDraftDB {
private db: IDBDatabase | null = null;
async open() {
this.db = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open('MunicipalMapDemo', 1);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains('drafts')) {
db.createObjectStore('drafts', { keyPath: 'key' });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async saveDraft(key: string, data: unknown) {
if (!this.db) throw new Error('Database is not ready');
const transaction = this.db.transaction('drafts', 'readwrite');
transaction.objectStore('drafts').put({ key, data });
}
}
export const localDraftDB = new LocalDraftDB();
await localDraftDB.open();

生产环境还应补充数据版本号、异常恢复、容量限制、主动清理和草稿加密策略。


四、实现要点#

  1. 将道路、施工区和交通设施抽象为通用节点,降低绘制层与业务字段的耦合。
  2. 将净距、桩号格式和告警规则全部配置化,由受控数据源注入。
  3. 本地草稿仅存储编辑数据,敏感图纸应加密并设置生命周期。
  4. 导出文件和截图按用途保留必要图层,避免把无关业务字段一起带出。
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

基于 Web Canvas 与 IndexedDB 的市政道路设施平面图编辑器
https://blog.luozili.work/posts/municipal-road-canvas-indexeddb-editor/
作者
llbzow
发布于
2026-05-28
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录

💬
🎀