Skip to main content
如何搭建城市活动地图:场馆、分类与实时发现
Tutorials

如何搭建城市活动地图:场馆、分类与实时发现

搭建城市活动地图的实战指南:对场馆做地理编码、绘制并聚合标记点、按分类和街区筛选,并让活动列表可被检索发现。

Brent van der Heiden9 min read
#events map#interactive map#geocoding#poi search#javascript map#local discovery

大多数本地发现类产品的成败都系于一块屏幕:地图。用户打开你的应用,是为了回答一个简单问题,即此刻我附近有什么活动,而地图就是他们找到答案的方式。把这张地图做对,包括正确的图钉、快速的筛选,以及搜索引擎真正能读懂的活动列表,是任何活动指南背后的核心工程工作。

本指南从零开始讲解如何搭建一张城市活动地图:建立活动数据模型、把场馆地理编码为坐标、绘制并聚合标记点、按分类和街区筛选,以及让活动列表可被发现。它假设你已经知道如何渲染一张底图。如果还不会,请先从我们的教程如何为你的网站添加交互式地图开始,再回到这里学习活动相关的部分。

围绕场馆建立活动模型

活动是发生在某个地点的、有时间限定的事物。地点才是要放上地图的东西,所以场馆是数据模型的锚点。一条最小化的活动记录如下:

const event = {
  id: 'evt_8471',
  title: 'Late Night Jazz at The Vortex',
  category: 'live-music',
  venue: 'The Vortex Jazz Club, 11 Gillett Square, London N16 8AZ',
  coords: null,        // 由地理编码填充,仅一次
  neighbourhood: null, // 由反向地理编码填充,仅一次
  startsAt: '2026-07-09T20:30:00Z',
  url: 'https://example.com/tickets/8471',
};

那两个 null 字段很关键。你不会想在每次加载地图时都对场馆做地理编码。只在活动创建或导入时做一次,然后把坐标存下来,当作永久数据对待。

第一步:将每个场馆地理编码为坐标

地理编码把人类可读的地址转换成可绘制的经纬度。把场馆字符串发给 Geocoding API,再从首条结果读取坐标:

async function geocodeVenue(address) {
  const url = new URL('https://api.mapatlas.eu/geocoding/v1/search');
  url.searchParams.set('text', address);
  url.searchParams.set('size', '1');

  const res = await fetch(url, { headers: { Authorization: `Bearer ${API_KEY}` } });
  const data = await res.json();
  const top = data.features?.[0];
  if (!top) return null;

  const [lon, lat] = top.geometry.coordinates;
  return { lat, lon, label: top.properties.label };
}

在写入时运行它,也就是当一个活动进入你的系统时,并把返回的坐标持久化到记录上。关于为什么精确的图钉位置很重要,以及楼顶坐标与人们实际走到的门口有何不同的完整说明,参见建筑入口地理编码

第二步:把活动绘制为标记点

每个活动都有坐标后,绘制就是一个循环。给每个分类分配各自的颜色,让地图一眼就能读懂:

const CATEGORY_COLORS = {
  'live-music': '#E75480',
  'theatre':    '#006BA6',
  'comedy':     '#EE7C0E',
  'food':       '#16A34A',
  'nightlife':  '#7C3AED',
};

function plotEvents(map, events) {
  events.forEach((evt) => {
    if (!evt.coords) return;
    const popup = new mapmetricsgl.Popup().setHTML(
      `<strong>${evt.title}</strong><br>${new Date(evt.startsAt).toLocaleString()}` +
      `<br><a href="${evt.url}">购票</a>`,
    );
    new mapmetricsgl.Marker({ color: CATEGORY_COLORS[evt.category] || '#0c3456' })
      .setLngLat([evt.coords.lon, evt.coords.lat])
      .setPopup(popup)
      .addTo(map);
  });
}

这正是真实产品背后的模式。一个在线示例是 OnlyHere,一份关于伦敦每日活动的指南,它对每个场馆做地理编码,并把当天的活动绘制到一张交互式 MapAtlas 地图上,让用户可以按场馆和街区浏览有什么活动,而不是滚动一份扁平列表。

第三步:城市变满时进行聚合

活动只有寥寥几个时,逐个标记点没有问题。而覆盖全城的信息流可不止寥寥几个。一旦超过大约 100 至 200 个图钉,就把活动移入一个 GeoJSON 数据源,让地图对它们聚合:

map.addSource('events', {
  type: 'geojson',
  cluster: true,
  clusterRadius: 50,
  data: {
    type: 'FeatureCollection',
    features: events.filter((e) => e.coords).map((e) => ({
      type: 'Feature',
      geometry: { type: 'Point', coordinates: [e.coords.lon, e.coords.lat] },
      properties: { title: e.title, category: e.category },
    })),
  },
});

现在市中心一个繁忙的夜晚只会显示一个带计数的气泡,而不是一百个相互重叠的图钉,随着用户放大,聚合也会拆分开来。

第四步:按分类和街区筛选

筛选是活动地图升级为活动产品的关键。把完整列表保存在内存中并对数组做筛选,然后把筛选后的集合交给地图数据源,而不是手工重建标记点:

function applyFilters(map, allEvents, { category, neighbourhood }) {
  const filtered = allEvents.filter((e) =>
    (!category || e.category === category) &&
    (!neighbourhood || e.neighbourhood === neighbourhood),
  );
  map.getSource('events').setData({
    type: 'FeatureCollection',
    features: filtered.filter((e) => e.coords).map((e) => ({
      type: 'Feature',
      geometry: { type: 'Point', coordinates: [e.coords.lon, e.coords.lat] },
      properties: { title: e.title },
    })),
  });
  return filtered;
}

要填充 neighbourhood 字段,只需对每个场馆的坐标做一次反向地理编码,并存下行政区名称。这一个值就能驱动街区下拉框、「本区域有什么活动」的视图,以及更清爽的列表页,全都无需在运行时再做一次查询。

第五步:让活动列表可被发现

地图为已经在你应用里的人回答了问题。但对于数量大得多、正在向搜索引擎或 AI 助手询问「今晚 Shoreditch 有什么活动」的受众,它什么也做不了。那些系统读取的是结构化数据,而不是像素。

用 schema.org 的 Event 字段标注每个活动,并复用你已经拥有的坐标:

{
  "@context": "https://schema.org",
  "@type": "Event",
  "name": "Late Night Jazz at The Vortex",
  "startDate": "2026-07-09T20:30:00Z",
  "location": {
    "@type": "Place",
    "name": "The Vortex Jazz Club",
    "geo": { "@type": "GeoCoordinates", "latitude": 51.5462, "longitude": -0.0753 }
  }
}

绘制图钉所用的坐标同时喂给结构化数据,于是展示与发现来自同一个可信来源。这正是一张好看的地图与一款能被找到的产品之间的差别。关于为什么机器可读的位置数据能驱动 AI 搜索可见性的更多内容,参见我们的指南面向 AI 搜索的位置专属常见问题

融会贯通

一张城市活动地图由五个活动部件组成:以场馆为锚点的数据模型、写入时的地理编码、彩色标记点、大规模下的聚合,以及基于数组的筛选,全都架设在让活动列表可被发现的结构化数据之上。把这些部件构建在同一个地理空间平台上,坐标就能从地理编码器顺畅地流向地图、再流向 schema,没有重复查询,也没有数据漂移。

MapAtlas 为你提供地理编码、反向地理编码、地点搜索和地图渲染,让你在欧盟托管、符合 GDPR 的基础设施上构建这一切。探索 Geocoding APISearch API,开始绘制属于你自己的城市。

常见问题

搭建城市活动地图需要哪些数据?

最低限度下,每个活动需要一个场馆地址、一个分类和一个开始时间。地址是把活动变成地图图钉的关键:只做一次地理编码得到经纬度,把坐标存下来,之后每次展示活动都复用它。其余内容,例如购票链接、图片和描述,都是挂在标记点弹窗上的元数据。

如何把场馆地址转换成地图坐标?

用场馆地址字符串调用地理编码 API,再从响应中读回经纬度。使用 MapAtlas Geocoding API 时,将地址作为文本查询发送到 https://api.mapatlas.eu/geocoding/v1/search,并取用首条结果的坐标。在活动创建时对每个场馆只做一次地理编码,而不是每次加载页面都做,这样地图保持流畅、API 用量也低。

一张地图能显示多少活动标记点才会变卡?

在缩小视图下,逐个渲染标记点大约到 100 至 200 个图钉仍能保持流畅。超过这个量就切换到 GeoJSON 数据源聚合(clustering),它在低缩放级别把附近活动合并为一个计数气泡,随着用户放大再拆分开来。一个覆盖全城、包含数百条活动的信息流应该从一开始就使用聚合。

如何让用户按分类或街区筛选活动?

把完整活动列表以数组形式保存在内存中,给每一项挂上分类和街区,再根据用户的选择对该数组做筛选。然后用筛选后的集合更新地图数据源,而不是销毁再重建标记点。按街区浏览的原理相同:筛选出坐标落在所选区域内的活动,或与该区域共享反向地理编码得到的行政区名称的活动。

活动地图为什么需要结构化数据?

可视化地图服务于人,但搜索引擎和 AI 助手读取的是结构化数据。用 schema.org 的 Event 字段(包含场馆名称、坐标和开始时间)标注每个活动,才能让某条活动出现在诸如「今晚 Shoreditch 有什么活动」这类问题的答案里。你在地图上绘制所用的坐标同时喂给结构化数据,于是发现与展示来自同一个可信来源。

觉得有用?分享给他人吧。

关于作者

Brent van der Heiden

作者

Brent van der Heiden

Co-Founder & CEO at MapAtlas

Brent built MapAtlas out of a conviction that developers deserve location APIs with fair pricing and genuine end-user privacy. He writes about geospatial infrastructure, AI search visibility, and how location data powers the products people rely on every day.

查看所有文章
返回博客