地图可视化实践录:空间分析库Turf.js的学习

本文介绍空间分析库Turf.js的接口,并给出实例。

知识点

Turf .js是空间分析的 JavaScript 库。它包含传统的空间操作、用于创建 GeoJSON 数据的辅助函数以及数据分类和统计工具。其涵盖空间几何图形的测量、关系判断、坐标变换和偏移、辅助方法等具体场景,功能强大。

因为Turf.js使用GeoJSON格式处理所有地理数,因此其坐标顺序为经度,纬度,与leaflet刚好相反。

Turf.js 不仅能在后端的Node.js环境中使用,也可在浏览器环境中使用,比如与 Leaflet、MapBox GL JS 等地图库配合。本文基于原来的综合示例,学习一些入门级的接口,由于是在html中调用接口,所以给出实例和对应展示效果。详细接口说明建议参考官网。

实践

下载和使用

参考https://turf.nodejs.cn/docs/getting-started文档下载,编写本文时(2025年11月),最新版本为7.2.0,具体地址:https://unpkg.com/@turf/turf@7.2.0/turf.min.js。

在html文件引入:

复制代码
<script src="mymap/maptool/turf-7.2.0.min.js"></script>

点、线、面

turf.js的点、线、面,与leaflet有较多相似之处。

点:turf.point([坐标], {可选属性});

线:turf.lineString([坐标], {可选属性})

面:turf.polygon([坐标], {可选属性})

核心代码:

复制代码
    const points = [
        [108.397293,22.738823],
        [108.481064,22.724890]
    ];
    // 点
    // 注:turf.point 返回点,第二个参数可指定properties参数,drawOneLayer 函数会自动判断name,故加之
    const point1 = turf.point(points[0], {name: "五象湖公园"});
    var pointLayer1 = drawOneLayer(point1);
    addToLayers(pointLayer1);

    const point2 = turf.point(points[1], {name: "南宁园博园"});
    var pointLayer2 = drawOneLayer(point2);
    addToLayers(pointLayer2);
    
    // 线
    const linePoints = [
        [108.316269, 22.838212],
        [108.326569, 22.807200],
        [108.347168, 22.779347],
        [108.352661, 22.759720],
        [108.379440, 22.738190]
    ];

    var line = turf.lineString(
        // [
        //     [108.316269, 22.838212],
        //     [108.326569, 22.807200],
        //     [108.347168, 22.779347],
        //     [108.352661, 22.759720],
        //     [108.379440, 22.738190]
        // ],
        linePoints,
        {color: "#FF00FF", name: "折线"}
    );

    // 可指定为miles或kilometers,默认后者,可不写
    var totalDistance = turf.length(line, { units: "kilometers"});
    var lineLayer = drawOneLayer(line);
    addToLayers(lineLayer);

    // 面
    const polygon = turf.polygon(
        [
            [
                [108.3, 22.86], 
                [108.4, 22.86], 
                [108.4, 22.9], 
                [108.3, 22.9], 
                [108.3, 22.86]
            ]
        ], {color: "#0000FF"}
    );

    var polygonLayer = drawOneLayer(polygon);
    addToLayers(polygonLayer);

注意,drawOneLayeraddToLayers是为了显示绘制效果而调用的。效果如下图所示。

里程、面积计算

计算长度:turf.length(line),默认返回长度单位为千米。

计算里程:turf.distance(point1, point2),默认返回长度单位为千米。

计算面积:turf.area(polygon),返回面积单位为平方米。

核心代码:

复制代码
    // 计算两点间距离
    const disTurf = turf.distance(point1, point2, { units: 'kilometers' });
    info +=`2点间距离(turfs): ${disTurf.toFixed(3)} 公里\r\n`;

    const disLeaflet = calcDistance(points, "km")
    info +=`2点间距离(leaflet): ${disLeaflet.toFixed(3)} 公里\r\n`;

    // 可指定为miles或kilometers,默认后者,可不写
    var totalDistance = turf.length(line, { units: "kilometers"});

    info += `线段长度: ${totalDistance.toFixed(3)} 公里\r\n`
    
    const disLeaflet2 = calcDistance(linePoints, "km")
    info +=`线段长度(leaflet): ${disLeaflet2.toFixed(3)} 公里\r\n`;

    // 计算面积
    const area = turf.area(polygon); // 平方米
    info += `面积: ${area.toFixed(3)} 平方米`;

turf使用distance计算2个经纬度的里程,用length即可计算经纬度数组的里程。leaflet也有类似的接口,其使用distanceTo计算2个经纬度里程,但没有length功能接口,下面实现之:

复制代码
// 计算经纬度数组points的总里程
function calcDistance(points, units = 'km') {
    let totalDistance = 0;

    for (let i = 0; i < points.length - 1; i++) {
        const [lng1, lat1] = points[i];
        const [lng2, lat2] = points[i + 1];
        
        const distance = L.latLng(lat1, lng1).distanceTo(L.latLng(lat2, lng2));
        totalDistance += distance;
    }

    // 单位转换
    const unitConvert = {
        'm': (d) => d,
        'km': (d) => d / 1000,
        'mile': (d) => d / 1609.34
    };
    const convert = unitConvert[units] || unitConvert['km'];

    return convert(totalDistance);
}

效果图如下:

可以看到,使用turfs和leaflet计算相同经纬度里程的结果是相同的。

外接矩形、中心点

计算最小外接矩形:turf.bbox

计算中心点:turf.center

核心代码:

复制代码
function turf_demo2() {
    var info = ""

    // 最小外接矩形
    var bbox = turf.bbox(gxGeoJson); // gxGeoJson 在 450000-广西壮族自治区.js 中定义
    var polygon = turf.bboxPolygon(bbox, {}, {color: "#0000FF"});
    var polygonLayer = drawOneLayer(polygon);
    addToLayers(polygonLayer);

    info += `广西最小外接矩形为 ${bbox}\r\n`

    // 计算中心点
    const center = turf.center(gxGeoJson);
    const latlng = center.geometry.coordinates;
    const point = turf.point(latlng, {name: "中心点"});
    var pointLayer = drawOneLayer(point);
    addToLayers(pointLayer);
    
    info += `中心点为 ${latlng}\r\n`

    // 计算总面积
    var totalArea = turf.area(gxGeoJson);
    totalArea = totalArea / 10000000000;

    info += `广西总面积 ${totalArea.toFixed(3)} 万平方千米`

    return info
}

效果图:

根据GeoJSON使用turf.area计算总面积,为23.81万平方千米,与官方部门提供的数据(23.76万平方千米)非常接近。

贝塞尔曲线Bezier

调用turf.bezierSpline可以将一条线段变成平滑的塞尔曲线。

核心代码:

复制代码
    // 坐标点
    var points = [
        [108.390427,22.922982],
        [108.476257,22.929306],
        [108.524323,22.895786],
        [108.500290,22.841376]
    ];
    // 原始线
    var line = turf.lineString(
        points,
        {color: "#0000FF", name: "折线"}
    );
    addToLayers(drawOneLayer(line));

    var line1 = turf.lineString(
        points,
    );
    var curved = turf.bezierSpline(line1, 
        {
            properties: {color: "#FF0000", name: "折线"}
        }
    );
    addToLayers(drawOneLayer(curved));

效果图如下,其中蓝色为原始线段,红色为贝塞尔曲线。

根据官方手册,可带resolutionsharpness参数。值越高,效果越好,下面代码将参数调低数值。

复制代码
    var curved = turf.bezierSpline(line1, 
        {
            resolution: 1000, 
            sharpness: 0.5,
            properties: {color: "#FF0000", name: "折线"}
        }
    );
    addToLayers(drawOneLayer(curved));

效果比默认的差一些,如图:

空间关系判断

点与线关系的判断:turf.booleanPointOnLine,点在线上,返回true。

点与面关系的判断:booleanPointInPolygon,点在面内,返回true。

面与面关系的判断:booleanOverlap,如有重叠,返回true。

为减少篇幅,只列出核心代码,完整代码参考文后工程仓库代码。

点线关系判断代码片段:

复制代码
    const points = [
        [108.481064,22.724890],
        [108.397293,22.738823]
    ];
    
    const point1 = turf.point(points[0], {name: "南宁园博园"});
    addToLayers(drawOneLayer(point1));

    const point2 = turf.point(points[1], {name: "五象湖公园"});
    addToLayers(drawOneLayer(point2));

    var line1 = turf.lineString(
        [
            [108.481064,22.724890],
            [108.397293,22.738823],
            [108.369827,22.774599]
        ], {color: "#0000FF"}
    );
    addToLayers(drawOneLayer(line1));

    // 判断点与线的关系
    const isPointOnLine1 = turf.booleanPointOnLine(point1, line1);
    // 不在线上的点
    const outPoint = turf.point([108.45,22.77], {name: "其它点"});
    addToLayers(drawOneLayer(outPoint));
    const isPointOnLine2 = turf.booleanPointOnLine(outPoint, line1);

    info += `点1与线1关系:${isPointOnLine1}, 其它与线1关系: ${isPointOnLine2}`;

效果:

点面关系判断代码:

复制代码
    polygon1 = turf.polygon(
    [
        [
            [108.3, 22.86], 
            [108.4, 22.86], 
            [108.4, 22.9], 
            [108.3, 22.9], 
            [108.3, 22.86]
        ]
    ], {color: "#0000FF"}
    );

    polygon2 = turf.polygon(
    [
        [
            [108.349228,22.918555],
            [108.378067,22.963451],
            [108.476257,22.950806],
            [108.479004,22.893888],
            [108.440552,22.855929],
            //[108.417892,22.909701], // 删除此坐标,两多边形相交
            [108.349228,22.918555]
        ]
    ], {color: "#0000FF"}
    );

    addToLayers(drawOneLayer(polygon1));
    addToLayers(drawOneLayer(polygon2));

    inPoint = turf.point([108.304596,22.895786], {name: "内点"})
    addToLayers(drawOneLayer(inPoint));

    const isPointInPoly = turf.booleanPointInPolygon(inPoint, polygon1);
    const isOverlap = turf.booleanOverlap(polygon1, polygon2);

    info += `点与面关系:${isPointInPoly}, 面与面的关系: ${isOverlap}\r\n`;

效果:

几何运算

交集:turf.intersect

并集:turf.union

差集:turf.difference

上述几个函数均返回GeoJson格式,可直接用之显示在地图上。

核心代码:

复制代码
    polygon1 = turf.polygon(
    [
        [
            [108.3, 22.86], 
            [108.4, 22.86], 
            [108.4, 22.9], 
            [108.3, 22.9], 
            [108.3, 22.86]
        ]
    ], {color: "#0000FF"}
    );

    polygon2 = turf.polygon(
    [
        [
            [108.325195,22.887562],
            [108.327942,22.872379],
            [108.422012,22.864155],
            [108.446732,22.879971],
            [108.417892,22.891990],
            [108.325195,22.887562]
        ]
    ], {color: "#0000FF"}
    );

    // 交集
    const intersection = turf.intersect(turf.featureCollection([polygon1, polygon2]), {properties: {color: "#FF0000"}});
    // 并集
    const union = turf.union(turf.featureCollection([polygon1, polygon2]), {properties: {color: "#008000"}});
    // 差集
    const difference = turf.difference(turf.featureCollection([polygon1, polygon2]), {properties: {color: "#FF0000"}});

效果如下所示,从上而下依次是原始2个多边形,交集、并集、差集。

需要说明的是,笔者之前使用的是turf6版本,当前使用turf7版本,两版本之间部分接口参数是不同的。以下交集接口的对比:

复制代码
v6版本:
const intersection = turf.intersect(polygon1, polygon2);
v7版本:
const intersection = turf.intersect(turf.featureCollection([polygon1, polygon2]));

发现这些变化,也是经历了一些曲折的过程。由于笔者旧的工程有使用计算2个多边形交集的接口,更新turf版本后抛出异常,查了很久才发现是接口参数变化了。

小结

turf还有很多其它高阶功能,但GIS于笔者而言,只是工作的一小部分而已,目前使用到的函数,只限于其中一小部分,比如,计算外接矩形、多边形交集,等。其它接口,留待后续有需要时再研究了。

代码

文中列出了主要的代码片段,另有相关的工程demo,已上传到github仓库。因不定时更新,代码不一定与文中严格对应,以代码仓库为准。如使用,请自行根据实际情况修改。

仓库地址:https://github.com/latelee/mapdemo

本文涉及文件:100.综合示例.html,函数为turf_demo1turf_demo2

相关推荐
李迟6 天前
地图可视化实践录:leaflet学习之右键菜单
gis地图实践
李迟3 个月前
地图可视化实践录:显示地理区域图
gis地图实践
李迟3 个月前
地图可视化实践录:显示高德地图和百度地图
gis地图实践