Skip to content
目录

骑行轨迹缩略图

为啥要写这边文章?因为我平时喜欢骑行,常用的软件有'捷安特骑行'和'腾讯地图',然后我发现他们在生成骑行轨迹缩略图方面做得不够好

从上面的截图看都有各自的问题:

  • '捷安特骑行' 生成的缩略图轨迹明显不对
  • '腾讯地图' 地图生成的缩略图背景图都是同一张图片,不能做到根据骑行轨迹来自动生成背景图

这篇文章我们来探索下怎样生成更好的缩略图效果

方法探索

生成缩略图的方法主要有两种:

  • 利用地图引擎绘制轨迹和渲染,然后截图

  • 根据轨迹的数据计算需要哪些瓦片然后渲染瓦片和轨迹

方法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;
    }
}

在线例子

完整代码

maptalks教程 document auto generated by mdpress