大多数本地发现类产品的成败都系于一块屏幕:地图。用户打开你的应用,是为了回答一个简单问题,即此刻我附近有什么活动,而地图就是他们找到答案的方式。把这张地图做对,包括正确的图钉、快速的筛选,以及搜索引擎真正能读懂的活动列表,是任何活动指南背后的核心工程工作。
本指南从零开始讲解如何搭建一张城市活动地图:建立活动数据模型、把场馆地理编码为坐标、绘制并聚合标记点、按分类和街区筛选,以及让活动列表可被发现。它假设你已经知道如何渲染一张底图。如果还不会,请先从我们的教程如何为你的网站添加交互式地图开始,再回到这里学习活动相关的部分。
围绕场馆建立活动模型
活动是发生在某个地点的、有时间限定的事物。地点才是要放上地图的东西,所以场馆是数据模型的锚点。一条最小化的活动记录如下:
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 API 和 Search API,开始绘制属于你自己的城市。
常见问题
搭建城市活动地图需要哪些数据?
最低限度下,每个活动需要一个场馆地址、一个分类和一个开始时间。地址是把活动变成地图图钉的关键:只做一次地理编码得到经纬度,把坐标存下来,之后每次展示活动都复用它。其余内容,例如购票链接、图片和描述,都是挂在标记点弹窗上的元数据。
如何把场馆地址转换成地图坐标?
用场馆地址字符串调用地理编码 API,再从响应中读回经纬度。使用 MapAtlas Geocoding API 时,将地址作为文本查询发送到 https://api.mapatlas.eu/geocoding/v1/search,并取用首条结果的坐标。在活动创建时对每个场馆只做一次地理编码,而不是每次加载页面都做,这样地图保持流畅、API 用量也低。
一张地图能显示多少活动标记点才会变卡?
在缩小视图下,逐个渲染标记点大约到 100 至 200 个图钉仍能保持流畅。超过这个量就切换到 GeoJSON 数据源聚合(clustering),它在低缩放级别把附近活动合并为一个计数气泡,随着用户放大再拆分开来。一个覆盖全城、包含数百条活动的信息流应该从一开始就使用聚合。
如何让用户按分类或街区筛选活动?
把完整活动列表以数组形式保存在内存中,给每一项挂上分类和街区,再根据用户的选择对该数组做筛选。然后用筛选后的集合更新地图数据源,而不是销毁再重建标记点。按街区浏览的原理相同:筛选出坐标落在所选区域内的活动,或与该区域共享反向地理编码得到的行政区名称的活动。
活动地图为什么需要结构化数据?
可视化地图服务于人,但搜索引擎和 AI 助手读取的是结构化数据。用 schema.org 的 Event 字段(包含场馆名称、坐标和开始时间)标注每个活动,才能让某条活动出现在诸如「今晚 Shoreditch 有什么活动」这类问题的答案里。你在地图上绘制所用的坐标同时喂给结构化数据,于是发现与展示来自同一个可信来源。

