PageSourceSearch

https://www.geoboundaries.org/assets/js/internalSpatial.js

js geoboundaries.org collected 2026-10-01 11:30:33 UTC 11,182 bytes, 333 lines download raw bytes

1
2// create global reference to worker performing source matching
3var matchingWorker = null; 
4
5// spatial stats stuff
6function calcSpatialStats(features) {
7    var stats = {};
8    var circumf = 0;
9    var vertices = 0;
10    var area = 0;
11    var segLengths = [];
12    for (feat of features) {
13        var geom = feat.getGeometry();
14        circumf += ol.sphere.getLength(geom);
15        area += ol.sphere.getArea(geom);
16        if (geom.getType() == 'Polygon') {
17            // wrap so can treat at multipolygon
18            var polys = [geom.getCoordinates()];
19        } else {
20            var polys = geom.getCoordinates();
21        };
22        for (poly of polys) {
23            for (ring of poly) {
24                vertices += ring.length;
25            };
26            /*
27            // poly level aggregate stats
28            var ext = poly[0]; // polygon exterior
29            vertices += ext.length;
30            var line = turf.lineString(ext);
31            circumf += turf.length(line);
32            var polygon = turf.polygon([ext]);
33            area += turf.area(polygon);
34            // calc detailed segment lengths
35            //for (var i = 1; i < (ext.length-1); i++) {
36            //	v1 = ext[i];
37            //	v2 = ext[i+1];
38            //	var len = turf.length(turf.lineString([v1,v2]));
39            //	segLengths.push(len);
40            //};
41            */
42        };
43    };
44    // store final stats
45    stats.adminCount = features.length;
46    stats.area = area / 1000000.0; // convert from m2 to km2
47    circumf = circumf / 1000.0; // convert from m to km
48    stats.circumf = circumf;
49    stats.vertices = vertices;
50    stats.avgLineResolution = circumf / vertices * 1000; // ie avg vertext-to-vertex segment length ie resolution in meters
51    stats.avgLineDensity = vertices / circumf; // ie avg number of vertices per 1 km distance ie density (inverse of line resolution)
52    //segLengths.sort(function(a, b) {return a - b});
53    //stats.medLineResolution = segLengths[Math.round(segLengths.length/2)]  / 1000.0; // convert from m to km, even though supposed to be km
54    return stats;
55};
56
57//-----------------------------------
58// this chunk defines boundary comparisons
59
60// helpers
61function geoj2turf(geoj) {
62    if (geoj.type == 'Polygon') {
63        geom = turf.polygon(geoj.coordinates)
64    } else if (geoj.type == 'MultiPolygon') {
65        geom = turf.multiPolygon(geoj.coordinates)
66    };
67    return geom;
68};
69
70// feature to feature similarity
71function robustTurfArea(geom) {
72    // WARNING: turf calculates wildly incorrect area estimates, see instead 'olArea'
73    // make sure isec rings are correctly sorted (otherwise will get negative area)
74    // see https://github.com/Turfjs/turf/issues/1482
75    // https://github.com/w8r/martinez/issues/91
76    var geomType = turf.getType(geom);
77    var coords = turf.getCoords(geom);
78    alert('area calc for '+geomType+' '+coords.length+' '+JSON.stringify(coords));
79    if (geomType == 'Polygon') {
80        // treat as a multipolygon w one polygon
81        coords = [coords];
82    };
83    // reorganize the polygon rings to make sure it's correct
84    var allRings = [];
85    for (poly of coords) {
86        for (ring of poly) {
87            /*if (ring[0] != ring[ring.length-1]) {
88                ring.push(ring[ring.length-1]);
89            };*/
90            allRings.push(ring);
91        };
92    };
93    var multiLines = turf.multiLineString(allRings);
94    var featureColl = turf.polygonize(multiLines);
95    // cumulate the area of each resulting polygon
96    var area = 0;
97    var pi = 0;
98    turf.featureEach(featureColl, function (currentFeature, featureIndex) {
99        area = area + turf.area(currentFeature);
100        pi++;
101    });
102    alert('old '+turf.area(geom)+' new '+area);
103
104    /*
105    // calc area one ring at a time
106    area = 0;
107    let ip = 0;
108    for (poly of coords) {
109        let ir = 0;
110        var ringAreas = [];
111        for (ring of poly) {
112            var ringArea = turf.area(turf.polygon([ring]));
113            alert(ip+'-'+ir+', verts '+ring.length+', area '+ringArea);
114            //ringArea = Math.abs(ringArea);
115            //if (ir > 0) {
116            //	ringArea = -ringArea;
117            //};
118            ringAreas.push(ringArea);
119            //area = area + ringArea;
120            ir++;
121        };
122        var extArea = Math.max.apply(Math, ringAreas);
123        var sumArea = ringAreas.reduce((a, b) => a + b, 0);
124        var holeArea = sumArea - extArea;
125        area = area + extArea - holeArea;
126        ip++;
127    };
128    */
129
130    return area;
131};
132
133function olArea(geom) {
134    var feat = {'type':'Feature', 'properties':{}, 'geometry':{'type':turf.getType(geom), 'coordinates':turf.getCoords(geom)}};
135    var olFeat = new ol.format.GeoJSON().readFeature(feat);
136    var area = ol.sphere.getArea(olFeat.getGeometry());
137    //alert(turf.area(geom)+'-->'+area);
138    return area;
139};
140
141function olPerimeter(geom) {
142    fdsfs;
143};
144
145function similarity(feat1, feat2) {
146    try {
147        // create turf objects
148        //alert('creating turf geoms');
149        geom1 = geoj2turf(turf.simplify(feat1.geometry, {tolerance:0.01}));
150        geom2 = geoj2turf(turf.simplify(feat2.geometry, {tolerance:0.01}));
151
152        // exit early if no overlap
153        /*
154        var [xmin1,ymin1,xmax1,ymax1] = turf.bbox(geom1);
155        var [xmin2,ymin2,xmax2,ymax2] = turf.bbox(geom2);
156        var boxoverlap = (xmin1 <= xmax2 & xmax1 >= xmin2 & ymin1 <= ymax2 & ymax1 >= ymin2)
157        if (!boxoverlap) {
158            return {'equality':0, 'within':0, 'contains':0}
159        };
160        */
161
162        // calc intersection
163        //alert('calc intersection');
164        var isec = turf.intersect(geom1, geom2);
165        if (isec == null) {
166            // exit early if no intersection
167            return {'equality':0, 'within':0, 'contains':0}
168        };
169
170        // calc union
171        //alert('calc union');
172        var union = turf.union(geom1, geom2);
173
174        // calc metrics
175        //alert('calc areas');
176        var geom1Area = turf.convertArea(olArea(geom1),'meters','kilometers');
177        var geom2Area = turf.convertArea(olArea(geom2),'meters','kilometers');
178        var unionArea = turf.convertArea(olArea(union), 'meters', 'kilometers');
179        var isecArea = turf.convertArea(olArea(isec), 'meters', 'kilometers');
180        var areas = {'geom1Area':geom1Area, 'geom2Area':geom2Area, 'unionArea':unionArea, 'isecArea':isecArea}
180;
181        //alert(JSON.stringify(areas));
182        
183        var results = {};
184        results.equality = isecArea / unionArea;
185        results.within = isecArea / geom1Area;
186        results.contains = isecArea / geom2Area;
187    } catch (error) {
188        console.warn('similarity could not be calculated: ' + error);
189        results = {equality:NaN, within:NaN, contains:NaN};
190    };
191    return results;
192};
193
194function calcSpatialRelations(feat, features) {
195    //feat = feat.getGeometry().simplify(0.01);
196    var geojWriter = new ol.format.GeoJSON();
197    var geoj1 = geojWriter.writeFeatureObject(feat);
198    var matches = [];
199    for (feat2 of features) {
200        if (!ol.extent.intersects(feat.getGeometry().getExtent(), feat2.getGeometry().getExtent())) {
201            continue;
202        };
203        //feat2 = feat2.getGeometry().simplify(0.01);
204        geoj2 = geojWriter.writeFeatureObject(feat2);
205        simil = similarity(geoj1, geoj2);
206        if (simil.equality > 0.0) {
207            matches.push([feat2,simil]);
208        };
209        i++;
210    };
211    return matches;
212};
213
214function calcAllSpatialRelations(data1, data2, onSuccess, onProgress=null) {
215    // calc relations from 1 to 2
216    // calculate everything in background and receive results at end
217    // to avoid locking up the entire gui
218
219    // terminate any previous worker
220    if (matchingWorker !== null) {
221        matchingWorker.terminate();
222    };
223
224    // create worker
225    matchingWorker = new Worker('assets/js/internalMatcherWorker.js');
226    console.log(matchingWorker);
227    
228    // define how to process messages
229    function processResults(results) {
230        console.log('received results:');
231        console.log(results);
232        onSuccess(results);
233    };
234    function processMessage(event) {
235        let [status,data] = event.data;
236        if (status == 'processing') {
237            let [i,total] = data;
238            onProgress(i, total);
239        } else if (status == 'finished') {
240            let results = data;
241            processResults(results);
242        };
243    };
244    matchingWorker.onmessage = processMessage;
245
246    // tell worker to start processing
247    matchingWorker.postMessage([data1, data2]);
248};
249
250function sortSpatialRelations(matches, sort_by, thresh, reverse=true) {
251    // sort
252    function sortFunc(a, b) {
253        if (reverse == false) {
254            var trueVal = 1;
255        } else {
256            var trueVal = -1;
257        };
258        if (a[1][sort_by] < b[1][sort_by]) {
259            // a is less than b by some ordering criterion
260            return -trueVal;
261        };
262        if (a[1][sort_by] > b[1][sort_by]) {
263            // a is greater than b by the ordering criterion
264            return trueVal;
265        };
266        // a must be equal to b
267        return 0;
268    };
269    matches.sort(sortFunc);
270
271    // filter by threshold
272    newMatches = [];
273    for (m of matches) {
274        if (m[1][sort_by] >= thresh) {
275            newMatches.push(m);
276        };
277    };
278
279    return newMatches;
280};
281
282function calcBestMatches(matches) {
283    // this should output a simpler match list
284    // with one row for every feat1
285    // in the format feat,bestmatchfeat,stats
286    // where multiple feats can't match to the same feat
287
288    // helper to find features that match another
289    function findFeaturesThatMatch(matchID) {
290        result = [];
291        for (x of matches) {
292            var [feature,related] = x;
293            related = sortSpatialRelations(related, 'equality', 0.01);
294            if (related.length==0) {continue};
295            for (y of related) {
296                var [matchFeat,stats] = y;
297                if (matchFeat.id == matchID) {
298                    result.push([feature,stats]);
299                };
300            };
301        };
302        return result;
303    };
304
305    // create best match list
306    var finalMatches = [];
307    for (x of matches) {
308        var [feature,related] = x;
309        // match with highest equality
310        related = sortSpatialRelations(related, 'equality', 0.01);
311        if (related.length==0) {
312            finalMatches.push([feature,null,null]);
313            continue;
314        };
315        [bestMatchFeat,bestStats] = related[0];
316        // make sure this match is the highest among all others
317        // ie only the feature with the best match to another is allowed
318        // ie multiple feats can't match another
319        var othersThatMatch = findFeaturesThatMatch(bestMatchFeat.id);
320        othersThatMatch = sortSpatialRelations(othersThatMatch, 'equality', 0.01);
321        [bestOtherThatMatches,bestOtherThatMatchesStats] = othersThatMatch[0];
322        if (feature.id == bestOtherThatMatches.id) {
323            finalMatches.push([feature,bestMatchFeat,bestStats]);
324        } else {
325            finalMatches.push([feature,null,null]);
326        };
327    };
328
329    // return
330    return finalMatches;
331};
332
333

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.