六边形的一次webassembly实践

141 阅读3分钟

最近有需求要求计算六边形,我研究了一下有几种方式

  • h3.js
  • turf.js

实现的思路就是先使用h3的函数或者turf.hexgrid来计算出来相关的多边形,然后按照多边形和六边形的关系做一次filter

使用过后,发现以下问题

1.h3.js实现的效率比较高,但是生成的多边形是斜的,因此表现效果上不佳

2.使用turf生成的hexgrid实现的效率很低,很多多边形的时候会很卡,>4s以上,但是生成的多边形方方正正

因此便有了疑问h3.js为何效率比较高,我看了一下h3.js的源码,libh3.js明显是webasembly实现的方式,所以我决定把turf的hexgrid函数用c实现一遍,

1.turf hexgrid源码的翻译

//turf.cc
#ifndef EM_PORT_API
#    if defined(__EMSCRIPTEN__)
#        include<emscripten.h>
#        if defined(__cplusplus)
#           define EM_PORT_API(rettype) extern "C" rettype EMSCRIPTEN_KEEPALIVE   
#        else
#           define EM_PORT_API(rettype) rettype EMSCRIPTEN_KEEPALIVE
#        endif
#     else
#        if defined(__cplusplus)
#           define EM_PORT_API(rettype) extern "C" rettype    
#        else
#           define EM_PORT_API(rettype) rettype
#        endif
#     endif
#endif
#include<math.h>
#include<stdio.h>
#include<stdlib.h>
double* hexes;//存储多边形
double min(double x, double y) {
    return x < y ? x : y;
}

EM_PORT_API(void) free_space(){
    if(hexes){
        free(hexes);
    }
    hexes=nullptr;
}

double _abs(double x) {
    return x > 0 ? x : -1 * x;
}
double max(double x, double y) {
    return x > y ? x : y;
}
const double DEGREE2RADIANS = 0.017453292519943295;
const double EARTH_RADIUS = 6371008.8;
EM_PORT_API(double) distance(double lon1, double lat1, double lon2, double lat2) {
    double radLat1 = lat1 * DEGREE2RADIANS;
    double radLat2 = lat2 * DEGREE2RADIANS;
    double a = radLat1 - radLat2;
    double b = lon1 * DEGREE2RADIANS - lon2 * DEGREE2RADIANS;
    double s = 2 * asin(sqrt(pow(sin(a / 2), 2) +
        cos(radLat1) * cos(radLat2) * pow(sin(b / 2), 2)));
    s = s * EARTH_RADIUS;
    return s;
}
struct Point {
    double x;
    double y;
    Point(double x, double y) :x(x), y(y) {}
    Point() :x(0), y(0) {}
};

EM_PORT_API(bool) pointinpolygon(double x1, double y1, double* polygonarr, int count) {
    //A point is in a polygon if a line from the point to infinity crosses the polygon an odd number of times
    bool odd = false;
    for (int i = 0, j = count - 1; i < count; i++) {
        if (((polygonarr[2*i+1] > y1) != (polygonarr[2*j+1] > y1)) // One point needs to be above, one below our y coordinate
                  // ...and the edge doesn't cross our Y corrdinate before our x coordinate (but between our x coordinate and infinity)
            && (x1 < (polygonarr[2*j] - polygonarr[2*i]) * (y1 - polygonarr[2*i+1]) / (polygonarr[2*j+1] - polygonarr[2*i+1]) + polygonarr[2*i])) {
            // Invert odd
            // System.out.println("Point crosses edge " + (j + 1));
            // totalCrosses++;
            odd = !odd;
        }
        //else {System.out.println("Point does not cross edge " + (j + 1));}
        j = i;
    }
    return odd;
}
const double consines[6] = { 1,
  0.5000000000000001,
  -0.4999999999999998,
  -1,
  -0.5000000000000004,
  0.49999999999999933 };
const double sines[6] = {
     0,
  0.8660254037844386,
  0.8660254037844387,
  1.2246467991473532e-16,
  -0.8660254037844385,
  -0.866025403784439
};
double arr[14];
EM_PORT_API(double*) hexagon(double lon, double lat, double rx, double ry) {
    for (int i = 0; i < 6; i++) {
        double x = lon + rx * consines[i];
        double y = lat + ry * sines[i];
        arr[i * 2] = x;
        arr[i * 2 + 1] = y;
    }
    arr[12] = arr[0];
    arr[13] = arr[1];
    return arr;
}
//六边形的算法
EM_PORT_API(double*) prehexgrid(double* bbox, double cellSide,double* mask,int maskCount,int* cc) {
    double west = bbox[0];
    double south = bbox[1];
    double east = bbox[2];
    double north = bbox[3];
    double centerY = (south + north) / 2.0;
    double centerX = (west + east) / 2.0;
    double xFraction = (cellSide * 2) / distance(west, centerY, east, centerY);
    double cellWidth = xFraction * (east - west);
    double yFraction = (cellSide * 2) / distance(centerX, south, centerX, north);
    double cellHeight = yFraction * (north - south);
    double radius = cellWidth / 2;
    double hex_width = radius * 2;
    double hex_height = sqrt(3) / 2 * cellHeight;
    double box_width = east - west;
    double box_height = north - south;
    double x_interval = (3.0 / 4) * hex_width;
    double y_interval = hex_height;

    double x_span = (box_width - hex_width) / (hex_width - radius / 2);
    int x_count = int(floor(x_span));
    double x_adjust = (x_count * x_interval - radius / 2 - box_width) / 2 -
        radius / 2 +
        x_interval / 2;
    // adjust box_height so all hexagons will be inside the bbox
    int y_count = int(floor((box_height - hex_height) / hex_height));
    double y_adjust = (box_height - y_count * hex_height) / 2;
    bool hasOffsetY = y_count * hex_height - box_height > hex_height / 2;

    if (hasOffsetY) {
        y_adjust -= hex_height / 4;
    }
    printf("%d,%d,%ld", x_count, y_count, (x_count + 1) * (y_count + 1) * 14 * sizeof(double));

    if (hexes) free(hexes);

    hexes = (double*)malloc((x_count + 1) * (y_count + 1) * 14 * sizeof(double));

    int index = 0;

    *cc = 0;
    for (int x = 0; x <= x_count; x++) {
        for (int y = 0; y <= y_count; y++) {
            bool isOdd = (x % 2 == 1);
            if (y == 0 && isOdd) continue;
            if (y == 0 && hasOffsetY) continue;

            double center_x = x * x_interval + west - x_adjust;
            double center_y = y * y_interval + south + y_adjust;

            if (isOdd) {
                center_y -= hex_height / 2;
            }

            double* hex = hexagon(center_x, center_y, cellWidth / 2, cellHeight / 2);

            bool inPoly = false;
            for (int i = 0; i < 6; i++) {
                if (pointinpolygon(hex[i * 2], hex[i * 2 + 1], mask, maskCount)) {
                    inPoly = true;
                    break;
                }
            }

            if (inPoly) {
                for (int i = 0; i < 7; i++) {
                    hexes[index] = hex[i * 2];
                    hexes[index+1] = hex[i * 2 + 1];
                    index += 2;
                }
                (*cc)++;
            }
        }
    }

    return hexes;


}

int main() {
    double arr[4] = {
        120.124391,
        30.136342,
        120.236837,
        30.2376661 };


    double mask[606] = {
      120.221058,30.237666,120.218373,30.235087,120.213307,30.230547,120.211297,30.228716,120.208612,30.226514,120.203348,30.222039,120.196561,30.216567,120.187584,30.211086,120.182867,30.207929,120.177622,30.205625,120.165029,30.20131,120.148694,30.195716,120.142721,30.19364,120.140885,30.193007,120.140174,30.192838,120.136944,30.19197,120.134121,30.190587,120.131764,30.188743,120.129668,30.18685,120.12804,30.184778,120.126671,30.18283,120.125951,30.18111,120.125111,30.179444,120.124626,30.177352,120.124391,30.172199,120.12459,30.167261,120.125128,30.163251,120.126001,30.159457,120.127474,30.156009,120.130377,30.151164,120.133044,30.147956,120.138594,30.142077,120.146144,30.137174,120.148451,30.138966,120.151484,30.141541,120.151779,30.142047,120.152336,30.142541,120.152773,30.142747,120.153497,30.142937,120.154209,30.143183,120.154693,30.143925,120.154942,30.144073,120.155892,30.144333,120.155305,30.145231,120.154992,30.145199,120.154744,30.145958,120.155311,30.146005,120.155667,30.146189,120.156763,30.147137,120.157946,30.147951,120.158722,30.14868,120.159077,30.148889,120.159903,30.148223,120.160536,30.147474,120.160276,30.147127,120.159646,30.147089,120.159665,30.146401,120.159794,30.146044,120.160135,30.146121,120.161411,30.146764,120.162004,30.147104,120.162314,30.147387,120.162887,30.148108,120.163804,30.148916,120.164114,30.149848,120.164218,30.150019,120.164363,30.150089,120.164815,30.150059,120.165645,30.149592,120.165798,30.149784,120.166304,30.149541,120.166459,30.149609,120.167039,30.149247,120.167748,30.149061,120.168275,30.148647,120.169313,30.149234,120.169687,30.14953,120.16992,30.1499,120.170471,30.151121,120.171031,30.151651,120.171291,30.151832,120.172924,30.152478,120.173428,30.15277,120.174529,30.153646,120.175204,30.153887,120.176357,30.152938,120.1767,30.152381,120.177282,30.151913,120.177653,30.151412,120.178158,30.150979,120.178569,30.150268,120.178862,30.149422,120.180332,30.149818,120.180668,30.149326,120.18116,30.148164,120.181388,30.147826,120.181723,30.147243,120.18314,30.146317,120.184041,30.145388,120.184139,30.145363,120.186651,30.146597,120.186758,30.14656,120.187069,30.145964,120.186185,30.144433,120.186265,30.144335,120.186562,30.144447,120.187224,30.144965,120.188097,30.145145,120.188179,30.145304,120.188479,30.145566,120.188746,30.145586,120.190779,30.146556,120.191541,30.146825,120.191791,30.14716,120.19188,30.147118,120.192554,30.145647,120.19322,30.144793,120.193958,30.144826,120.195352,30.142458,120.196023,30.143079,120.196262,30.143133,120.196385,30.142888,120.196015,30.142564,120.196031,30.142421,120.196501,30.142238,120.196781,30.141628,120.196842,30.141218,120.19699,30.140893,120.197165,30.140764,120.198019,30.140516,120.199551,30.13969,120.200471,30.139436,120.201378,30.13956,120.202359,30.139871,120.202959,30.140163,120.204085,30.141057,120.204503,30.142364,120.204752,30.142711,120.205039,30.142921,120.205262,30.143025,120.205641,30.142611,120.205885,30.142215,120.207219,30.140855,120.207884,30.139947,120.208046,30.139301,120.209109,30.138965,120.209555,30.138712,120.20952,30.138274,120.208739,30.13756,120.208691,30.137251,120.208742,30.137131,120.210281,30.136504,120.210951,30.136342,120.211569,30.136376,120.213295,30.137893,120.214533,30.136915,120.214714,30.136913,120.215921,30.137828,120.217398,30.138647,120.218973,30.139728,120.21947,30.139948,120.219436,30.140051,120.218801,30.140584,120.218286,30.141561,120.216933,30.143075,120.216922,30.143299,120.21706,30.143445,120.217618,30.143524,120.217699,30.143706,120.217644,30.144917,120.21833,30.145888,120.218789,30.147601,120.218904,30.14778,120.219234,30.148027,120.220174,30.148424,120.220334,30.148576,120.220309,30.148731,120.21986,30.149268,120.219853,30.149489,120.220016,30.150049,120.219899,30.150514,120.220105,30.150769,120.220585,30.151225,120.22057,30.151896,120.221061,30.152526,120.221659,30.153759,120.223046,30.153813,120.223807,30.153504,120.224188,30.153495,120.22443,30.153591,120.224592,30.153866,120.224576,30.154195,120.224379,30.155129,120.224175,30.155523,120.223138,30.15669,120.223169,30.157257,120.223239,30.157441,120.223479,30.157708,120.225774,30.159177,120.225853,30.159582,120.226174,30.160155,120.226082,30.160374,120.225331,30.161289,120.225099,30.161799,120.225024,30.162066,120.225043,30.162656,120.224975,30.162712,120.22377,30.162598,120.223136,30.162777,120.222762,30.162807,120.2219,30.16272,120.221269,30.162432,120.220538,30.162581,120.220299,30.162559,120.220059,30.162396,120.219817,30.162012,120.219629,30.161868,120.219116,30.161748,120.218652,30.161799,120.218535,30.161576,120.217946,30.161303,120.217869,30.161315,120.217727,30.161521,120.217591,30.161542,120.216859,30.16132,120.216749,30.161433,120.216719,30.162347,120.216568,30.162382,120.21655,30.162712,120.218127,30.163232,120.218035,30.163641,120.218222,30.163788,120.218224,30.164162,120.217985,30.164506,120.217946,30.164753,120.218469,30.165197,120.218689,30.165493,120.219519,30.166093,120.21948,30.166831,120.219966,30.167029,120.219943,30.1672,120.219362,30.168161,120.219357,30.168367,120.219582,30.168605,120.219455,30.168706,120.218892,30.1688,120.218863,30.168879,120.219003,30.16894,120.219655,30.168925,120.2203,30.169686,120.220315,30.169952,120.219936,30.169766,120.219997,30.170025,120.22055,30.170523,120.220884,30.17057,120.221182,30.170495,120.221388,30.170606,120.221718,30.170632,120.22204,30.17084,120.22223,30.171062,120.224395,30.17125,120.224375,30.171648,120.224058,30.172268,120.224013,30.172775,120.223553,30.173695,120.223417,30.173849,120.223236,30.173891,120.222285,30.173643,120.221875,30.17442,120.221398,30.175512,120.222014,30.175496,120.222031,30.176295,120.222045,30.178883,120.223863,30.178899,120.224656,30.179167,120.224682,30.179393,120.224447,30.181476,120.224576,30.181885,120.233498,30.18204,120.233677,30.183505,120.23531,30.1831,120.236733,30.182979,120.236837,30.18458,120.233982,30.185901,120.234065,30.187528,120.234198,30.188784,120.234249,30.195259,120.234317,30.201283,120.234399,30.208242,120.234512,30.209543,120.23442,30.20999,120.234459,30.213544,120.234952,30.213663,120.235111,30.225426,120.235232,30.228138,120.235041,30.229126,120.234435,30.229798,120.233707,30.229988,120.231887,30.230181,120.227398,30.232514,120.226516,30.232896,120.22396,30.2363,120.221058,30.237666    };
    int count = 0;
    prehexgrid(arr, 50,mask,606/2,&count);
    printf("\n%d", count);
    
    bool c=pointinpolygon(120.2316,30.2130, mask, 606/2);
    printf("%d", c);//in
    
    return 0;
}

这里记一个小插曲,本来我用chatgpt来生成一个点在多边形的模板,它给我一个java模板,我一看注释写的很全,但是采用后发现还是有很多的问题

public class PolygonUtils {

    /**
     * 判断点是否在多边形内
     *
     * @param point 检测点
     * @param pts   多边形的顶点
     * @return 点在多边形内返回true, 否则返回false
     */
    public static boolean isPointInPoly(Point2D.Double point, List<Point2D.Double> pts) {

        int N = pts.size();
        boolean boundOrVertex = true; //如果点位于多边形的顶点或边上,也算做点在多边形内,直接返回true
        int intersectCount = 0;//cross points count of x 
        double precision = 2e-10; //浮点类型计算时候与0比较时候的容差
        Point2D.Double p1, p2;//neighbour bound vertices
        Point2D.Double p = point; //当前点

        p1 = pts.get(0);//left vertex        
        for (int i = 1; i <= N; ++i) {//check all rays            
            if (p.equals(p1)) {
                return boundOrVertex;//p is an vertex
            }

            p2 = pts.get(i % N);//right vertex            
            if (p.x < Math.min(p1.x, p2.x) || p.x > Math.max(p1.x, p2.x)) {//ray is outside of our interests                
                p1 = p2;
                continue;//next ray left point
            }

            if (p.x > Math.min(p1.x, p2.x) && p.x < Math.max(p1.x, p2.x)) {//ray is crossing over by the algorithm (common part of)
                if (p.y <= Math.max(p1.y, p2.y)) {//x is before of ray                    
                    if (p1.x == p2.x && p.y >= Math.min(p1.y, p2.y)) {//overlies on a horizontal ray
                        return boundOrVertex;
                    }

                    if (p1.y == p2.y) {//ray is vertical                        
                        if (p1.y == p.y) {//overlies on a vertical ray
                            return boundOrVertex;
                        } else {//before ray
                            ++intersectCount;
                        }
                    } else {//cross point on the left side                        
                        double xinters = (p.x - p1.x) * (p2.y - p1.y) / (p2.x - p1.x) + p1.y;//cross point of y                        
                        if (Math.abs(p.y - xinters) < precision) {//overlies on a ray
                            return boundOrVertex;
                        }

                        if (p.y < xinters) {//before ray
                            ++intersectCount;
                        }
                    }
                }
            } else {//special case when ray is crossing through the vertex                
                if (p.x == p2.x && p.y <= p2.y) {//p crossing over p2                    
                    Point2D.Double p3 = pts.get((i + 1) % N); //next vertex                    
                    if (p.x >= Math.min(p1.x, p3.x) && p.x <= Math.max(p1.x, p3.x)) {//p.x lies between p1.x & p3.x
                        ++intersectCount;
                    } else {
                        intersectCount += 2;
                    }
                }
            }
            p1 = p2;//next ray left point
        }

        if (intersectCount % 2 == 0) {//偶数在多边形外
            return false;
        } else { //奇数在多边形内
            return true;
        }

    }

}

总之,这段代码翻译成c后有点问题,后面有时间再看

//计算点是否在多边形内
EM_PORT_API(bool) pointinpolygon2(double x1, double y1, double* polygonarr, int count) {
    bool boundOrVertex = true; //如果点位于多边形的顶点或边上,也算做点在多边形内,直接返回true
    int intersectCount = 0;//cross points count of x 
    double precision = 2e-11; //浮点类型计算时候与0比较时候的容差
    struct Point p1, p2;
    struct Point p(x1, y1);//当前点

    p1.x = polygonarr[0];
    p1.y = polygonarr[1];//leftVertex

    for (int i = 1; i <= count; i++) {
        if (_abs(p.x - p1.x) < precision && _abs(p.y - p1.y) < precision) {
            return boundOrVertex;// p is an vertex
        }

        p2.x = polygonarr[i * 2];
        p2.y = polygonarr[i * 2 + 1];//right vertex

        if (p.x<min(p1.x, p2.x) || p.x>max(p1.x, p2.x)) {
            //ray is outside of our intersets
            p1.x = p2.x;
            p1.y = p2.y;
            continue;
        }

        if (p.x > min(p1.x, p2.x) && p.x < max(p1.x, p2.x)) {
            if (p.y <= max(p1.y, p2.y)) {
                if (p1.x == p2.x && p.y >= min(p1.y, p2.y)) {
                    return boundOrVertex;
                }

                if (p1.y == p2.y) {//ray is vertical                        
                    if (p1.y == p.y) {//overlies on a vertical ray
                        return boundOrVertex;
                    }
                    else {//before ray
                        ++intersectCount;
                    }
                }
                else {//cross point on the left side 
                    double xinters = (p.x - p1.x) * (p2.y - p1.y) / (p2.x - p1.x) + p1.y;
                    if (_abs(p.y - xinters) < precision) {
                        return boundOrVertex;
                    }

                    if (p.y < xinters) {
                        ++intersectCount;
                    }
                }
            }
        }
        else {//special case when ray is crossing through the vertex 
            if (p.x == p2.x && p.y <= p2.y) {//p crossing over p2                    
                Point p3(polygonarr[(i + 1) * 2], polygonarr[(i + 1) * 2 + 1]);
                if (p.x >= min(p1.x, p3.x) && p.x <= max(p1.x, p3.x)) {//p.x lies between p1.x & p3.x
                    ++intersectCount;
                }
                else {
                    intersectCount += 2;
                }
            }
        }
        p1.x = p2.x;
        p1.y = p2.y;//next ray left point
    }


    if (intersectCount % 2 == 0) {//偶数在多边形外
        return false;
    }
    else { //奇数在多边形内
        return true;
    }
}

编译

使用docker apiaryio/emcc 1.38.11镜像,之所以用这个镜像,是因为我需要导出malloc,free函数,因为传入参数中有数组,便于开辟heap空间,传递参数

docker pull apiaryio/emcc 1.38.11

docker run --rm -it -v /root/cpp/:/src apiaryio/emcc:1.38.11

这里我们设置allow_memory_growth用于在动态调整内存大小, wasm参数为0时生成wasm文件,wasm=1时生成js文件【asm.js】文件,

emcc turf.cpp -s ALLOW_MEMORY_GROWTH=1 -s WASM=0 -o libhexgrid.html

这里我们为何要生成asm.js格式文件呢?

1.经过实践,我发现放到vue项目中,使用wasm文件,需要wasmModule.onRuntimeInitialized回调后才能完备使用,实际上就是要等一段时间,在vue中嵌入起来打包等过程中困扰了我好多天,我最终放弃这个wasm的若干好处,还是参考h3.js使用方式

// @ts-nocheck libh3-browser.js

var libh3 = (
function(libh3) {
  libh3 = libh3 || {};

var Module=typeof libh3!==...//asm.js代码

  return libh3
}
)(typeof libh3 === 'object' ? libh3 : {});
export default libh3;

我们也这样搞个对象导出

var libhexgrid=(
	libhexgrid=libhexgrid||{}
	\\生成代码
	return libhexgrid
)(typeof libhexgrid === 'object' ? libhexgrid : {});
export default libhexgrid;

然后直接import使用即可

这里有个优化就是这个生成代码有NODE,SHELL环境判断代码,可以删除掉,但是也不是必须

经过了上诉的总总折腾,我们提升了hexgrid的效率在5-10倍左右,实现了高效率和图形美观

最后贴一段如何使用传递参数,主要是将数据塞进数组,然后计算出结果后再组装=这个不是c语言强项,样例中使用的是高德坐标系,地图是84的,所以有点偏差

 cal_data() {
            let data = wasmModule._distance(120, 30, 120, 31);
            console.log(data)
            console.log(Mockdata)
            let bbox =turf.bbox(Mockdata)
            // let bbox = [119, 30, 120, 31]
            let bboxPtr = wasmModule._malloc(8 * bbox.length);
            for (let i = 0; i < bbox.length; i++) {
                wasmModule.HEAPF64[bboxPtr / 8 + i] = bbox[i];
            }


            let mask=Mockdata.features[0].geometry.coordinates.flat(4)
            // let mask = [
            //     119, 30,
            //     120, 30,
            //     120, 31,
            //     119, 31,
            //     119, 30
            // ];
            console.log(JSON.stringify(mask))

            let maskPtr = wasmModule._malloc(8 * mask.length);
            for (let i = 0; i < mask.length; i++) {
                wasmModule.HEAPF64[maskPtr / 8 + i] = mask[i];
            }
            let inPtr = wasmModule._malloc(4);
            wasmModule.HEAP32[inPtr / 4] = 0;
            let resultPtr = wasmModule._prehexgrid(bboxPtr, 50, maskPtr, mask.length/2, inPtr);

            let count = wasmModule.HEAP32[inPtr >> 2];
            console.log(count)
            let hexes = []

            for (let i = 0; i < count; i++) {
                let hex = []
                for (let j = 0; j < 7; j += 1) {
                    let x = wasmModule.HEAPF64[resultPtr / 8 + 14 * i + j * 2]
                    let y = wasmModule.HEAPF64[resultPtr / 8 + 14 * i + j * 2 + 1]
                    hex.push([x, y])
                }
                // hexes.push(hex)
                let poly = turf.polygon([hex], { cnt: 0 })
                hexes.push(poly);
                // console.log(wasmModule.HEAPF64[resultPtr>>3+i])
            }
            console.log(JSON.stringify(turf.featureCollection(hexes)))
            this.map.getSource('maine').setData(turf.featureCollection(hexes))
            wasmModule._free(maskPtr);
             wasmModule._free(inPtr);
              wasmModule._free(bboxPtr);
              wasmModule._free_space();
        }

image-20230311120446285.png