场景
需要展示覆盖北京的25万+个规则的网格,进行常规的点击显示数据、绘制勾选、着色等等操作。但是希望能直接使用geojson/topojson(原来构想的是随机森林和pagerank跑出结果,匹配到多边形,实时输出到geojson),不转格式,同时想尝试后端越少越好。。。
实现
- 首先想到的自然是类似于geojson-vt的实现,将json转换成切片之后显示。实际用了leaflet.vectorGrid.js,为了节省空间把geojson先转成topojson,体积大概小了30%,之后直接作为图层放到map里面。这样交互倒是很好实现,但是速度很慢。计时出来,下载70M的topojson需要几秒,而后计算切片显示又需要好几秒。。。最开始我还弱智地把topojson再在前端转成了geojson来放进slicer,就更慢了。另外,在放大到很大的时候自然性能还行,但是缩小到12级往下,缩放/拖动都需要重新计算,查看的延迟太大,不能正常使用。
- 首先就是要简化数据,使用了几种北京的建成区遥感分类结果,最后挑选了最新的一个结果来进行叠加,(具体就是先区域统计再筛选,不知道有没有更快的办法)直接缩小到6万+,geojson缩小到17M。这样的话直接用vectorGrid,除了缩小之外甚至都不会有延迟了,但是这里还是必须先把整个json下载下来。
- 为了不在一开始就下载所有的json,想到用一个简单的后端。方案是先用tippecannoe把geojson转成mapbox的mbtiles标准(等于把一个geojson转成一个sqlite的文件数据库)。leaflet.VectorGrid里可以把数据源选择成protobuf(protocal buffer),而mbtiles被发布成http://{host}:{port}/layer/{z}/{x}/{y}.pbf这样的buffer,流程上就ok了。github上面mbtiles有无数种通过自己来发布服务的办法,都是基于mapbox官方发布到github的几个基于node.js的插件来组合的,我选了一个比较无脑的tilehut.js。但是貌似mapbox的sqlite依赖给的有问题,我被卡住了一天,github issue里很多讨论,但是都没有用。。。最后记得是执行了重建所有packages依赖的一类操作才安装上。
这套方案和mapbox正常显示地图是一样的,所以响应上和底图一样基本没有延迟,缩小后也不会卡顿,定义样式和事件都可以。其实直接把mbtiles上传到mapbox也可以用,只不过不太好自动化进行而已。
总之,以上的操作证明了即便是大规模的数据,也可以只使用geojson来做地图展示,这样对于数据生成的步骤方便了不少。
一个protobuf例子
值得注意的几点:指定颜色的时候需要首先声明fill:true,另外由于样式化是针对protobuf里的所有图层分别设置的,所以要搞清图层的名字究竟是什么(尤其是和文件名不一致时)
// beijing grids color definition
function getColor_People(d) {
return d > 10000
? "#800026"
: d > 5000
? "#BD0026"
: d > 2500
? "#E31A1C"
: d > 1000
? "#FC4E2A"
: d > 500
? "#FD8D3C"
: d > 200
? "#FEB24C"
: d > 50
? "#FED976"
: "#FFEDA0";
}
var gridLayer;
var previousGridId = null;
var gridPopup = L.popup({ maxWidth: 1500, maxHeight: 400 });
// load grid with pbf tiles
var gridLayer = L.vectorGrid.protobuf(
"{host}:{port}/layer/{z}/{x}/{y}.pbf",
{
// pane: "gridPane",
rendererFactory: L.canvas.tile,
attribution: "© Your Organization",
interactive: true,
getFeatureId: function(feature) {
return feature.properties.id;
},
vectorTileLayerStyles: {
mylayername: function(feature) {
return {
// fill: true is needed
fill: true,
fillColor: getColor_People(feature.people),
fillOpacity: 0.7,
stroke: false,
color: "#595959",
weight: 0.6
};
}
}
}
);
gridLayer.on("click", function(e) {
if (map.getZoom() < 14) {
} else {
//console.log(e);
var prop = e.layer.properties;
//var latlng = [Number(parcel.y), Number(parcel.x)];
var latlng = e.latlng;
// settimeout otherwise when map click fires it will override this color change
// clear selection if slect on the same grid
var selectedGridId = prop["fnid"];
gridLayer.resetFeatureStyle(previousGridId);
if (selectedGridId === previousGridId && !gridPopup.isOpen()) {
gridPopup.remove();
selectedGridId = null;
} else {
gridPopup
.setLatLng(latlng)
.setContent(JSON.stringify(prop))
.openOn(map);
gridPopup.bringToFront();
map.panTo(latlng);
setTimeout(function() {
gridLayer.setFeatureStyle(
selectedGridId,
{
color: "red"
},
100
);
});
}
previousGridId = selectedGridId;
}
});
// gridLayer legends
var gridLayerLegend = L.control({ position: "bottomright" });
gridLayerLegend.onAdd = function(map) {
var div = L.DomUtil.create("div", "info legend"),
grades = [50, 200, 500, 1000, 2500, 5000, 10000],
labels = ['人流量'];
div.innerHTML+='<h4>人流量</h4>';
// loop through our density intervals and generate a label with a colored square for each interval
for (var i = 0; i < grades.length; i++) {
div.innerHTML +=
'<i style="background:' +
getColor_People(grades[i] + 1) +
'"></i> ' +
grades[i] +
(grades[i + 1] ? "–" + grades[i + 1] + "<br>" : "+");
}
return div;
};
// evnets to fire when adding and removing overlays
map.on("overlayadd", function(e) {
switch (e.name) {
case "Grid人流量":
gridLayerLegend.addTo(map);
break;
case "Grid2":
gridLayer2Legend.addTo(map);
break;
default:
break;
}
});
map.on("overlayremove", function(e) {
switch (e.name) {
case "Grid人流量":
gridLayerLegend.remove();
break;
case "Grid2":
gridLayer2Legend.remove();
break;
default:
break;
}
});
/**
* LAYER CONTROLS
*/
var baseMaps = {
BingSatellite: bingLayer,
OSM: osmTile,
GaoDe: GaoDeTile,
Grayscale: mapBoxTile
};
var overlayMaps = {
Grid人流量: gridLayer,
};
var layerControl = L.control.layers(baseMaps, overlayMaps).addTo(map);
效果: