Leaflet + MapLibre 双引擎,这个开源工具把任意城市变成装饰级地图海报

map-to-poster 是开发者 Dimar Tarmizi 开源的一个地图海报生成工具,670 Star。核心功能是把任意地理位置转成可打印的海报,但它的主题系统走的是偏艺术的方向——除了常规的 Minimal White 和 Midnight Dark,还有 Arctic Frost、Aurora Glow、Cyber Glitch 这类有明确视觉风格的主题。

在线 demo:https://maptoposter.tarmizi.id (opens in a new tab)

双引擎渲染与视口同步

项目同时集成了两套地图渲染引擎:Leaflet 和 MapLibre GL。

Leaflet 基于图片瓦片渲染,处理标准地图和卫星图——数据已经在服务端渲染好,以图片形式分块传输。MapLibre GL 基于矢量瓦片,把原始地理数据传到浏览器里,由 GPU 实时渲染,样式完全由代码控制,是艺术主题的技术基础。

两套引擎同时运行,需要保持视口同步——切换主题时,地图的位置和缩放级别不能变。项目用双向事件监听实现这一点,并用 isSyncing 标志防止两个引擎互相触发对方的 moveend 事件造成死循环:

map.on('moveend', () => {
  if (isSyncing) return;
  isSyncing = true;
  const center = map.getCenter();
  const zoom = map.getZoom();
  // Leaflet 坐标是 [lat, lng],MapLibre 是 [lng, lat],需要转换
  artisticMap.jumpTo({ center: [center.lng, center.lat], zoom: zoom - 1 });
  isSyncing = false;
});
 
artisticMap.on('moveend', () => {
  if (isSyncing) return;
  isSyncing = true;
  const center = artisticMap.getCenter();
  const zoom = artisticMap.getZoom();
  map.setView([center.lat, center.lng], zoom + 1);
  isSyncing = false;
});

注意缩放级别的 ±1 差值——两个引擎对同一区域的默认缩放比例不同,通过这个偏移量补偿对齐。

样式切换队列

MapLibre 切换主题是异步的,加载新样式需要时间。如果用户快速连续切换主题,前一个样式还没加载完就来了新请求,会产生冲突。项目用一个队列机制处理这个问题:

if (styleChangeInProgress) {
  // 缓存最新的待切换样式,丢弃中间状态
  pendingArtisticStyle = style;
  pendingArtisticThemeName = theme.name;
  return;
}
// 样式加载完成后,检查队列里是否还有待处理的样式
artisticMap.on('style.load', () => {
  if (pendingArtisticStyle) {
    const next = pendingArtisticStyle;
    pendingArtisticStyle = null;
    artisticMap.setStyle(next);
  }
});

主题与自定义

内置主题分两类:

标准主题:Minimal White(简洁白底)、Midnight Dark(深色)、Satellite View(卫星图),适合常规城市地图海报。

艺术主题:Arctic Frost(冷色系冰雪感)、Aurora Glow(渐变发光)、Cyber Glitch(故障艺术风格)等,视觉风格更强烈,适合做装饰性海报或创意设计素材。

如果内置主题不够用,可以直接编辑项目里的 artistic-themes.js 文件添加自定义主题,每个主题本质上是一套 MapLibre 样式配置。

排版方面,提供了多种相框样式和留白(Mat)选项,可以选择不同字体和文字内容,组合出画廊风格的海报版式。

路线与标记点

除了静态地图,项目支持在地图上添加可视化路线和拖拽标记点——比如标出一段旅行的轨迹,或者在特定地点放置标记,导出带有这些元素的海报。

这让海报从"某个地方的地图"变成"某段经历的记录",适合做旅行纪念、活动路线图等个性化内容。

高分辨率导出:多层 Canvas 合成

导出格式是 PNG,分辨率最高 50,000px,所有处理在浏览器本地完成。

导出不是简单地截一张屏幕图,而是把地图层和 UI 层分开处理再合成。地图层(Leaflet 或 MapLibre)直接从引擎的 canvas 读取像素,UI 层(相框、标题、坐标文字)用 html2canvas 单独渲染。两层合成到最终 canvas 上输出:

// 忽略地图元素,只捕获 UI 覆盖层
const overlayCanvas = await html2canvas(element, {
  useCORS: true,
  scale: scale,
  ignoreElements: (el) => el.id === 'map-preview' || el.id === 'artistic-map'
});
 
// MapLibre 地图层:等待渲染完成再读取 canvas
await new Promise(resolve => {
  const timer = setTimeout(() => {
    mapDataURL = artisticMap.getCanvas().toDataURL();
    resolve();
  }, 1500);
  // 优先监听 idle 事件,地图空闲时立即读取
  artisticMap.once('idle', () => {
    clearTimeout(timer);
    mapDataURL = artisticMap.getCanvas().toDataURL();
    resolve();
  });
});

高分辨率通过 scale 参数控制——scale 越大,canvas 尺寸越大,图片越清晰。同时针对 iOS 的 canvas 像素上限(16,777,216px)做了兜底处理,超出限制时等比缩小:

if (canvasWidth * canvasHeight > IOS_MAX_CANVAS_PIXELS) {
  const ratio = Math.sqrt(IOS_MAX_CANVAS_PIXELS / (canvasWidth * canvasHeight));
  canvasWidth = Math.floor(canvasWidth * ratio);
  canvasHeight = Math.floor(canvasHeight * ratio);
}

状态管理:观察者模式

项目没有用 Redux 或 Pinia,而是自己实现了一个轻量的观察者模式。subscribe 订阅状态变化,updateState 触发所有订阅者更新,同时把需要持久化的字段写入 localStorage:

export function subscribe(callback) {
  observers.push(callback);
  callback(state); // 立即执行一次,同步初始状态
}
 
export function updateState(partialState) {
  Object.assign(state, partialState);
  saveSettings();       // 持久化到 localStorage
  notifyObservers();    // 通知所有订阅者
}

这套机制让地图、主题、导出三个模块各自订阅自己关心的状态变化,互不耦合。

技术栈

技术版本用途
Vanilla JavaScript-核心逻辑,无框架依赖
Vite5构建工具
Leaflet-光栅瓦片地图渲染(标准/卫星图)
MapLibre GL-矢量瓦片地图渲染(艺术主题)
Tailwind CSS3样式
html2canvas-将页面渲染结果导出为 PNG
Nominatim-OpenStreetMap 地理编码,支持地名搜索

几个值得注意的选型:

Vanilla JS 而非框架:整个项目没有使用 React 或 Vue,状态管理用观察者模式自行实现,设置通过 localStorage 持久化。对于想学习不依赖框架构建复杂 Web 应用的开发者,这份代码值得参考。

html2canvas 导出:导出 PNG 的方式是把当前 DOM 渲染结果转成 canvas,再导出图片。这种方案的优点是实现简单,缺点是导出结果受浏览器渲染影响,与 PDF 矢量导出的思路不同。50,000px 的超高分辨率通过缩放 canvas 尺寸实现。

双引擎同步:Leaflet 和 MapLibre GL 分别维护自己的渲染上下文,项目在两者之间同步视口状态(中心坐标、缩放级别),确保切换引擎时画面位置不跳变。

安装与运行

需要提前安装 Node.js 18 及以上版本。

git clone https://github.com/dimartarmizi/map-to-poster.git
cd map-to-poster
npm install
npm run dev

生产构建:

npm run build

写在最后

map-to-poster 和同类工具的主要差异在于两点:一是双引擎架构让卫星图和矢量艺术主题在同一套界面里共存;二是主题系统覆盖范围更广,Cyber Glitch 这类风格在同类工具里比较少见。

项目是 MIT 协议,允许商业使用和二次开发。如果你想做一个有特定视觉风格的地图海报应用,它的双引擎架构和主题系统是值得参考的起点。

GitHub 地址:https://github.com/dimartarmizi/map-to-poster (opens in a new tab)