骑行轨迹缩略图
为啥要写这边文章?因为我平时喜欢骑行,常用的软件有'捷安特骑行'和'腾讯地图',然后我发现他们在生成骑行轨迹缩略图方面做得不够好
从上面的截图看都有各自的问题:
- '捷安特骑行' 生成的缩略图轨迹明显不对
- '腾讯地图' 地图生成的缩略图背景图都是同一张图片,不能做到根据骑行轨迹来自动生成背景图
这篇文章我们来探索下怎样生成更好的缩略图效果
方法探索
生成缩略图的方法主要有两种:
利用地图引擎绘制轨迹和渲染,然后截图
根据轨迹的数据计算需要哪些瓦片然后渲染瓦片和轨迹
方法1明显大材小用,这里我们主要探索下方法2
方法细节分析
- 因为要生成的图片是么有坐标系统的,纯像素坐标,所以需要我们把轨迹数据转成像素坐标
我们只需要把经纬度转换成墨卡托坐标,然后转成像素坐标即可,因为墨卡托是平面坐标是可以直接进行简单的计算的,而经纬度不是 平面坐标
js
function coordinatesToMercator(feature) {
const coordinates = feature.geometry.coordinates;
const transform = (coords) => {
if (Array.isArray(coords[0])) {
return coords.map((coord) => {
return transform(coord);
});
} else {
return gcoord.transform(coords, gcoord.WGS84, gcoord.WebMercator);
}
};
return transform(coordinates);
}
//计算坐标到像素的转换
function coordinatesToPixel(feature, imageMBBOX) {
const [ax, ay] = getImageAxAy(imageMBBOX);
const [minx, miny, maxx, maxy] = imageMBBOX;
const transform = (coords) => {
if (Array.isArray(coords[0])) {
return coords.map((coord) => {
return transform(coord);
});
} else {
const [x, y] = coords;
const px = (x - minx) / ax;
const py = (maxy - y) / ay;
return [px, py];
}
};
return transform(feature.mcoordinates);
}
这里使用了开源库gcoord
- 要根据轨迹的数据动态计算轨迹覆盖了哪些瓦片集合,并计算瓦片的像素坐标位置
js
//计算瓦片集合
function calTiles(imageMBBOX) {
let [xmin, ymin, xmax, ymax] = imageMBBOX;
const p1 = gcoord.transform([xmin, ymin], gcoord.WebMercator, gcoord.WGS84);
const p2 = gcoord.transform([xmax, ymax], gcoord.WebMercator, gcoord.WGS84);
const [minx, miny] = p1;
const [maxx, maxy] = p2;
const coordinates = [
[minx, miny],
[maxx, miny],
[maxx, maxy],
[minx, maxy],
[minx, miny],
];
const polygon = {
type: "Polygon",
coordinates: [coordinates],
};
let zoom = 18;
let tiles;
while (zoom > 0) {
tiles = study.TileCover.tiles(polygon, {
min_zoom: zoom,
max_zoom: zoom,
});
const len = tiles.length;
if (!len || len <= 8) {
break;
} else {
zoom--;
}
}
const list = [];
tiles.forEach((tile) => {
const [x, y, z] = tile;
const mbbox = merc.bbox(x, y, z, false, "900913");
list.push({
x,
y,
z,
mbbox,
});
});
return list;
}
瓦片集合的计算使用了开源库 tile-cover
瓦片包围盒计算使用了开源库 sphericalmercator
生成的图片要考虑padding效果,在计算整个图片的包围盒时要注意:
因为缩略图是长宽相等的,但是轨迹的包围盒不是长宽相等,所以要找出哪个长,然后短的要进行对应的offset的
整个图片的包围盒要考虑padding的
从上面的代码里我们看到有个非常重要的变量imageMBBOX即图片整个的墨卡托bbox.
以这张图为例,显然轨迹的包围盒高度要大于宽度,所以宽度要进行一定的offset来保证轨迹不形变
js
//计算整个图片的包围盒
function calImageMBBOX(mbbox) {
let [minx, miny, maxx, maxy] = mbbox;
const dx = maxx - minx,
dy = maxy - miny;
const maxOffset = Math.max(dx, dy);
let xoffset = 0,
yoffset = 0;
if (maxOffset === dx) {
yoffset = (dx - dy) / 2;
} else {
xoffset = (dy - dx) / 2;
}
minx -= xoffset;
maxx += xoffset;
miny -= yoffset;
maxy += yoffset;
let ax = (maxx - minx) / imageSize;
let ay = (maxy - miny) / imageSize;
const [paddingX, paddingY] = padding;
minx -= ax * paddingX;
maxx += ax * paddingX;
miny -= ay * paddingY;
maxy += ay * paddingY;
return [minx, miny, maxx, maxy];
}
整体流程
- 加载轨迹数据(geojson),然后将轨迹数据转成墨卡托坐标
- 计算轨迹数据的包围盒
- 计算整个图片的包围盒
- 利用图片的包围盒计算需要哪些瓦片集合
- 利用图片的包围盒将轨迹转成像素坐标
- 绘制瓦片集合
js
//绘制瓦片
function drawTiles(ctx, tiles, imageMBBOX) {
const [ax, ay] = getImageAxAy(imageMBBOX);
const [minx, miny, maxx, maxy] = imageMBBOX;
tiles.forEach((tile) => {
const { image, mbbox } = tile;
const [tileminx, tileminy, tilemaxx, tilemaxy] = mbbox;
const px1 = (tileminx - minx) / ax;
const py1 = (maxy - tilemaxy) / ay;
const px2 = (tilemaxx - minx) / ax;
const py2 = (maxy - tileminy) / ay;
ctx.drawImage(image, px1, py1, px2 - px1 + 1, py2 - py1 + 1);
});
}
- 绘制轨迹的路径
js
//绘制路线
function drawLineString(ctx, feature) {
const type = feature.geometry.type;
let paths = feature.pixels;
if (type === "LineString") {
paths = [paths];
}
let minx = Infinity,
miny = Infinity,
maxx = -Infinity,
maxy = -Infinity;
ctx.beginPath();
paths.forEach((path) => {
for (let i = 0, len = path.length; i < len; i++) {
const [x, y] = path[i];
minx = Math.min(minx, x);
miny = Math.min(miny, y);
maxx = Math.max(maxx, x);
maxy = Math.max(maxy, y);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
});
ctx.stroke();
if (debug) {
const strokeStyle = ctx.strokeStyle;
const [paddingX, paddingY] = padding;
ctx.beginPath();
ctx.strokeStyle = "blue";
ctx.rect(paddingX, paddingY, imageSize, imageSize);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = "green";
ctx.rect(minx, miny, maxx - minx, maxy - miny);
ctx.stroke();
ctx.strokeStyle = strokeStyle;
}
}