PageSourceSearch

https://www.geoboundaries.org/assets/js/geoContrast/Toolbox.js

js geoboundaries.org collected 2026-09-24 09:25:29 UTC 59,106 bytes, 1,643 lines download raw bytes

1
2//-----------------------------------
3// this chunk initiates the main map
4
5// layer definitions
6
7var mainStyle = new ol.style.Style({
8    fill: new ol.style.Fill({
9        color: 'rgba(220, 220, 255, 0.3)',
10    }),
11    stroke: new ol.style.Stroke({
12        color: 'rgb(29,107,191)', //'rgb(49, 127, 211)',
13        width: 2.5,
14    }),
15});
16var mainLayer = new ol.layer.Vector({
17    title: "Main boundary",
18    style: mainStyle,
19});
20
21var comparisonStyle = new ol.style.Style({
22    fill: new ol.style.Fill({
23        color: 'rgba(255, 255, 255, 0)', // fully transparent
24    }),
25    stroke: new ol.style.Stroke({
26        color: 'rgba(255, 0, 0, 0.8)',
27        width: 1.5,
28        lineDash: [10,10]
29    }),
30});
31var comparisonLayer = new ol.layer.Vector({
32    title: "Comparison boundary",
33    style: comparisonStyle,
34});
35
36// map
37
38var baseMaps = {
39    'google': new ol.source.XYZ({
40        attributions: 'Satellite Imagery <a href="https://www.google.com" target="_blank">&copy; Google</a> ',
41        url:
42        'https://www.google.com/maps/vt?lyrs=s&x={x}&y={y}&z={z}',
43        maxZoom: 20,
44        crossOrigin: 'anonymous' // necessary for converting map to img during pdf generation: https://stackoverflow.com/questions/66671183/how-to-export-map-image-in-openlayer-6-without-cors-problems-tainted-canvas-iss
45    }),
46    'esri': new ol.source.XYZ({
47        attributions: 'ESRI World Street Map',
48        url:
49        'http://services.arcgisonline.com/arcgis/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}',
50        maxZoom: 20,
51        crossOrigin: 'anonymous' // necessary for converting map to img during pdf generation: https://stackoverflow.com/questions/66671183/how-to-export-map-image-in-openlayer-6-without-cors-problems-tainted-canvas-iss
52    }),
53    'osm': new ol.source.OSM({crossOrigin: 'anonymous'}),
54};
55var baseMapLayer = new ol.layer.Tile({source: baseMaps.google});
56
57function setBaseMap(name) {
58    baseMapLayer.setSource(baseMaps[name]);
59};
60
61var map = new ol.Map({
62    target: 'map',
63    controls: ol.control.defaults({attribution:false}).extend([new ol.control.FullScreen(),
64                                            new ol.control.ScaleLine({units: 'metric'}),
65                                            ]),
66    layers: [
67        baseMapLayer,
68        mainLayer,
69        comparisonLayer
70    ],
71    view: new ol.View({
72        center: ol.proj.fromLonLat([37.41, 8.82]),
73        zoom: 4
74    })
75});
76
77map.on('singleclick', function(evt) {
78    // get feats
79    let mainFeat = null;
80    let comparisonFeat = null;
81    map.forEachFeatureAtPixel(evt.pixel, function(feature, layer) {
82        if (layer === mainLayer) {
83            mainFeat = feature
84        } else if (layer === comparisonLayer) {
85            comparisonFeat = feature
86        };
87    });
88    // init and open popup for the found features
89    if (mainFeat != null | comparisonFeat != null) {
90        openFeatureComparePopup(mainFeat, comparisonFeat);
91    };
92});
93
94function toggleMainLayer() {
95    var check = document.getElementById('toggle-layer-main');
96    mainLayer.setVisible(check.checked);
97};
98
99function toggleComparisonLayer() {
100    var check = document.getElementById('toggle-layer-comparison');
101    comparisonLayer.setVisible(check.checked);
102};
103
104var switcher = document.getElementById('layer-switcher');
105var control = new ol.control.Control({
106    element: switcher
107});
108map.addControl(control);
109
110
111
112
113
114
115// ------------------------------
116// this loads and stores the geocontrast metadata csv
117
118var geoContrastMetadata = null;
119
120function onSuccess (data) {
121    geoContrastMetadata = data;
122    // update countries
123    updateCountryDropdown();
124    // update main sources
125    updateMainSourceDropdown();
126    // update comparison sources
127    updateComparisonSourceDropdown();
128};
129loadGeoContrastMetadata(onSuccess);
130
131
132
133
134
135///////////////////////
136// file upload stuff
137
138/*
139<!--for details on local file reading, see https://www.html5rocks.com/en/tutorials/file/dndfiles/ -->
140<!--https://web.dev/read-files/ -->
141<!--https://gis.stackexchange.com/questions/138155/allow-client-to-upload-layers-to-openlayers-3 -->
142<!--https://gis.stackexchange.com/questions/368033/how-to-display-shapefiles-on-an-openlayers-web-mapping-application-that-are-prov
143-->
144*/
145
146function handleMainFileUpload(evt) {
147    var files = evt.target.files;
148    
149    // get file contents as a base64 encoded url string
150    const file = files[0];
151    fileExtension = file.name.split('.').pop();
152    //alert('local file selected: '+file.name+' - '+fileExtension);
153    
154    if (fileExtension == 'geojson' | fileExtension == 'json') {
155        reader = new FileReader();
156        reader.onload = function(e) {
157            // use reader results to create new source
158            var geojson = reader.result;
159            source = new ol.source.Vector({
160                format: new ol.format.GeoJSON({}),
161                overlaps: false,
162            });
163            // update the layer
164            updateMainLayerFromGeoJSON(source, geojson, zoomToExtent=true);
165        };
166        // read as data url
167        reader.readAsText(file);
168    } else if (fileExtension == 'zip') {
169        // experiment with zipfile reading
170        // https://stuk.github.io/jszip/documentation/examples/read-local-file-api.html
171        reader = new FileReader();
172        reader.onload = function(e) {
173            // use reader results to create new source
174            var raw = reader.result;
175            var zip = new JSZip(raw);
176            var paths = [];
177            for (filename in zip.files) {
178                if (filename.endsWith('.shp')) {
179                    var path = file.name + '/' + filename;
180                    var displayName = filename;
181                    paths.push([path,displayName]);
182                };
183            };
184            updateMainFileDropdown(paths);
185        };
186        reader.readAsBinaryString(file);
187    };
188};
189
190function handleComparisonFileUpload(evt) {
191    var files = evt.target.files;
192    
193    // get file contents as a base64 encoded url string
194    const file = files[0];
195    fileExtension = file.name.split('.').pop();
196    //alert('local file selected: '+file.name+' - '+fileExtension);
197    
198    if (fileExtension == 'geojson' | fileExtension == 'json') {
199        reader = new FileReader();
200        reader.onload = function(e) {
201            // use reader results to create new source
202            var geojson = reader.result;
203            source = new ol.source.Vector({
204                format: new ol.format.GeoJSON({}),
205                overlaps: false,
206            });
207            // update the layer
208            updateComparisonLayerFromGeoJSON(source, geojson, zoomToExtent=true);
209        };
210        // read as data url
211        reader.readAsText(file);
212    } else if (fileExtension == 'zip') {
213        // experiment with zipfile reading
214        // https://stuk.github.io/jszip/documentation/examples/read-local-file-api.html
215        reader = new FileReader();
216        reader.onload = function(e) {
217            // use reader results to create new source
218            var raw = reader.result;
219            var zip = new JSZip(raw);
220            var paths = [];
221            for (filename in zip.files) {
222                if (filename.endsWith('.shp')) {
223                    var path = file.name + '/' + filename;
224                    var displayName = filename;
225                    paths.push([path,displayName]);
226                };
227            };
228            updateComparisonFileDropdown(paths);
229        };
230        reader.readAsBinaryString(file);
231    };
232};
233
234
235
236
237
238
239
240////////////////////////
241// update file dropdown stuff
242
243function updateMainFileDropdown(paths) {
244    // activate and clear the dropdown
245    var select = document.getElementById('main-file-select');
246    select.disabled = false;
247    select.innerHTML = '';
248    // populate the dropdown
249    for ([path,displayName] of paths) {
250        var opt = document.createElement('option');
251        opt.value = path;
252        opt.innerText = displayName;
253        select.appendChild(opt);
254    };
255    // force change
256    mainFileDropdownChanged();
257};
258
259function updateComparisonFileDropdown(paths) {
260    // activate and clear the dropdown
261    var select = document.getElementById('comparison-file-select');
262    select.disabled = false;
263    select.innerHTML = '';
264    // populate the dropdown
265    for ([path,displayName] of paths) {
266        var opt = document.createElement('option');
267        opt.value = path;
268        opt.innerText = displayName;
269        select.appendChild(opt);
270    };
271    // force change
272    comparisonFileDropdownChanged();
273};
274
275
276
277
278
279
280
281//////////////////////
282// file dropdown changed
283function mainFileDropdownChanged() {
284    // first clear previous info
285    clearMainInfo();
286    clearMainStats();
287    clearMatchTable(); //clearMainNames();
288    clearTotalEquality();
289    // get file info
290    var file = document.getElementById('main-file-input').files[0];
291    var path = document.getElementById('main-file-select').value;
292    var subPath = path.split('.zip/')[1]; // only the relative path inside the zipfile
293    // read
294    reader = new FileReader();
295    reader.onload = function(e) {
296        // open zipfile
297        var raw = reader.result;
298        var zip = new JSZip(raw);
299        // prep args
300        var shpString = subPath;
301        var dbfString = subPath.replace('.shp', '.dbf');
302        var prjString = subPath.replace('.shp', '.prj');
303        // try to read projection
304        let prjData = null;
305        try {
306            prjRaw = zip.file(prjString).asText();
307            //console.log(prjRaw);
308            prjData = proj4(prjRaw);
309        } catch (err) {
310            console.log('could not read prj file, assuming WGS84');
311            //console.log(err);
312        };
313        
314        function processData(geojson) {
315            // geojson returned
316            source = new ol.source.Vector({
317                format: new ol.format.GeoJSON({}),
318                overlaps: false,
319            });
320            // update the layer
321            updateMainLayerFromGeoJSON(source, geojson, zoomToExtent=true);
322        };
323
324        // load using shapefile-js
325        // https://github.com/calvinmetcalf/shapefile-js
326        var waiting = Promise.all([shp.parseShp(zip.file(shpString).asArrayBuffer(), prjData), 
327                                    shp.parseDbf(zip.file(dbfString).asArrayBuffer())
328                                    ])
329        waiting.then(function(result){
330                        var geoj = shp.combine(result);
331                        processData(geoj);
332                    });
333    };
334    reader.readAsBinaryString(file);
335};
336
337function comparisonFileDropdownChanged() {
338    // first clear previous info
339    clearComparisonInfo();
340    clearComparisonStats();
341    clearMatchTable(); //clearComparisonNames();
342    clearTotalEquality();
343    // get file info
344    var file = document.getElementById('comparison-file-input').files[0];
345    var path = document.getElementById('comparison-file-select').value;
346    var subPath = path.split('.zip/')[1]; // only the relative path inside the zipfile
347    // read
348    reader = new FileReader();
349    reader.onload = function(e) {
350        // open zipfile
351        var raw = reader.result;
352        var zip = new JSZip(raw);
353        // prep args
354        var shpString = subPath;
355        var dbfString = subPath.replace('.shp', '.dbf');
356        var prjString = subPath.replace('.shp', '.prj');
357        // try to read projection
358        let prjData = null;
359        try {
360            prjRaw = zip.file(prjString).asText();
361            //console.log(prjRaw);
362            prjData = proj4(prjRaw);
363        } catch (err) {
364            console.log('could not read prj file, assuming WGS84');
365            //console.log(err);
366        };
367        
368        function processData(geojson) {
369            // geojson returned
370            source = new ol.source.Vector({
371                format: new ol.format.GeoJSON({}),
372                overlaps: false,
373            });
374            // update the layer
375            updateComparisonLayerFromGeoJSON(source, geojson, zoomToExtent=true);
376        };
377
378        // read using shapefile-js
379        // https://github.com/calvinmetcalf/shapefile-js
380        var waiting = Promise.all([shp.parseShp(zip.file(shpString).asArrayBuffer(), prjData), 
381                                    shp.parseDbf(zip.file(dbfString).asArrayBuffer())
382                                    ])
383        waiting.then(function(result){
384            var geoj = shp.combine(result);
385            processData(geoj);
386        });
387    };
388    reader.readAsBinaryString(file);
389};
390
391
392
393
394
395////////////////////////
396// update map layer stuff
397
398function zoomMainFeature(ID) {
399    var features = mainLayer.getSource().getFeatures();
400    // find feature
401    for (feature of features) {
402        if (feature.getId() == ID) {
403            break;
404        };
405    };
406    // zoom to extent
407    map.getView().fit(feature.getGeometry().getExtent());
408    // zoom out a little
409    //map.getView().setZoom(map.getView().getZoom()-1);
410};
411
412function zoomComparisonFeature(ID) {
413    var features = comparisonLayer.getSource().getFeatures();
414    // find feature
415    for (feature of features) {
416        if (feature.getId() == ID) {
417            break;
418        };
419    };
420    // zoom to extent
421    map.getView().fit(feature.getGeometry().getExtent());
422    // zoom out a little
423    //map.getView().setZoom(map.getView().getZoom()-1);
424};
425
426function updateMainLayer(zoomToExtent=false) {
427    // get main params
428    iso = document.getElementById('country-select').value;
429    // get comparison params
430    level = document.getElementById('main-admin-level-select').value;
431    sourceName = document.getElementById('main-boundary-select').value;
432    if (sourceName == null) {
433        return
434    };
435    // create and set new source
436    var source = new ol.source.Vector({
437        overlaps: false,
438    });
439    mainLayer.setSource(source);
440    // zoom after source has finished loading
441    if (zoomToExtent) {
442        source.on('change', function() {
443            //alert('main loaded, zoom to bbox: '+source.getExtent());
444            // zoom to extent
445            map.getView().fit(source.getExtent());
446            // zoom out a little
447            map.getView().setZoom(map.getView().getZoom()-1);
448        });
449    };
450    // update various divs after source has finished loading
451    source.on('change', function() {
452        //alert('main loaded, update info');
453        features = source.getFeatures();
454        updateMainStats(features);
455        updateMainFieldsDropdown(features);
456        //updateMainNames(features);
457        updateMainInfo(features);
458        calcMatchTable();
459        //calcBoundaryMakeupTables();
460    });
461    // notify if failed to load source
462    source.on(['error','featuresloaderror'], function() {
463        alert('Failed to load features for '+sourceName+' at '+level+'. Please choose another sourceor level.');
464    });
465    // load the source
466    loadGeoContrastSource(source, iso, level, sourceName);
467};
468
469function updateComparisonLayer(zoomToExtent=false) {
470    // get main params
471    iso = document.getElementById('country-select').value;
472    // get comparison params
473    level = document.getElementById('comparison-admin-level-select').value;
474    sourceName = document.getElementById('comparison-boundary-select').value;
475    if (sourceName == null) {
476        return
477    };
478    // create and set new source
479    var source = new ol.source.Vector({
480        overlaps: false,
481    });
482    comparisonLayer.setSource(source);
483    // zoom after source has finished loading
484    if (zoomToExtent) {
485        source.on('change', function() {
486            //alert('comparison loaded, zoom to bbox: '+source.getExtent());
487            // zoom to extent
488            map.getView().fit(source.getExtent());
489            // zoom out a little
490            map.getView().setZoom(map.getView().getZoom()-1);
491        });
492    };
493    // update various divs after source has finished loading
494    source.on('change', function() {
495        //alert('comparison loaded, update info');
496        features = source.getFeatures();
497        updateComparisonStats(features);
498        updateComparisonFieldsDropdown(features);
499        //updateComparisonNames(features);
500        updateComparisonInfo(features);
501        calcMatchTable();
502        //calcBoundaryMakeupTables();
503    });
504    // notify if failed to load source
505    source.on(['error','featuresloaderror'], function() {
506        alert('Failed to load features for '+sourceName+' at '+level+'. Please choose another source or level.');
507    });
508    // load the source
509    loadGeoContrastSource(source, iso, level, sourceName);
510};
511
512function clearMainLayer() {
513    source = new ol.source.Vector({
514        url: null,
515        format: new ol.format.TopoJSON({}),
516        overlaps: false,
517    });
518    mainLayer.setSource(source);
519};
520
521function clearComparisonLayer() {
522    source = new ol.source.Vector({
523        url: null,
524        format: new ol.format.TopoJSON({}),
525        overlaps: false,
526    });
527    comparisonLayer.setSource(source);
528};
529
530function updateMainLayerFromGeoJSON(source, geojson, zoomToExtent=false) {
531    // set the new source
532    mainLayer.setSource(source);
533    // zoom to new source after source has finished loading
534    if (zoomToExtent) {
535        source.on('change', function() {
536            //alert('new bbox: '+source.getExtent());
537            // get combined extent of main and uploaded file
538            extent = ol.extent.createEmpty();
539            ol.extent.extend(extent, source.getExtent());
540            ol.extent.extend(extent, comparisonLayer.getSource().getExtent());
541            // zoom to extent
542            map.getView().fit(extent);
543            // zoom out a little
544            map.getView().setZoom(map.getView().getZoom()-1);
545        });
546    };
547    // update various divs after source has finished loading
548    source.on('change', function() {
549        //alert('main loaded, update info');
550        features = source.getFeatures();
551        updateMainStats(features);
552        updateMainFieldsDropdown(features);
553        //updateMainNames(features);
554        updateMainInfo(features);
555        calcMatchTable();
556    });
557    // notify if failed to load source
558    source.on(['error','featuresloaderror'], function() {
559        alert('Failed to load uploaded file.');
560    });
561    // load the geojson data
562    loadFromGeoJSON(source, geojson);
563};
564
565function updateComparisonLayerFromGeoJSON(source, geojson, zoomToExtent=false) {
566    // set the new source
567    comparisonLayer.setSource(source);
568    // zoom to new source after source has finished loading
569    if (zoomToExtent) {
570        source.on('change', function() {
571            //alert('new bbox: '+source.getExtent());
572            // get combined extent of main and uploaded file
573            extent = ol.extent.createEmpty();
574            ol.extent.extend(extent, source.getExtent());
575            ol.extent.extend(extent, mainLayer.getSource().getExtent());
576            // zoom to extent
577            map.getView().fit(extent);
578            // zoom out a little
579            map.getView().setZoom(map.getView().getZoom()-1);
580        });
581    };
582    // update various divs after source has finished loading
583    source.on('change', function() {
584        //alert('main loaded, update info');
585        features = source.getFeatures();
586        updateComparisonStats(features);
587        updateComparisonFieldsDropdown(features);
588        //updateComparisonNames(features);
589        updateComparisonInfo(features);
590        calcMatchTable();
591    });
592    // notify if failed to load source
593    source.on(['error','featuresloaderror'], function() {
594        alert('Failed to load uploaded file.');
595    });
596    // load the geojson data
597    loadFromGeoJSON(source, geojson);
598};
599
600
601
602
603
604
605
606
607//////////////////////////////
608// populating the dropdowns
609
610function updateCountryDropdown() {
611    // NOTE: requires that geoContrastMetadata has already been populated
612    // get country dropdown
613    var select = document.getElementById('country-select');
614    // clear all existing dropdown options
615    select.innerHTML = '';
616    // get list of unique countries
617    var countriesSeen = [];
618    var countries = [];
619    for (row of geoContrastMetadata) {
620        if (row.length <= 1) {
621            // ignore empty rows
622            continue;
623        };
624        var name = row.boundaryName;
625        var iso = row.boundaryISO;
626        if (iso === undefined) {
627            // must be other empty rows? 
628            continue;
629        };
630        var country = {'name':name, 'iso':iso};
631        if (!(countriesSeen.includes(country.iso))) {
632            // only add if hasn't already been added
633            countries.push(country);
634            countriesSeen.push(country.iso);
635        };
636    };
637    // sort
638    countries.sort(function( a,b ){ if (a.name > b.name) {return 1} else {return -1} })
639    // add new options from geoContrastMetadata
640    for (country of countries) {
641        var opt = document.createElement("option");
642        opt.value = country.iso;
643        opt.textContent = country.name;
644        select.appendChild(opt);
645    };
646    // set the country to get-param if specified
647    const urlParams = new URLSearchParams(window.location.search);
648    var iso = urlParams.get('country');
649    if ((iso != null) & (iso != select.value)) {
650        select.value = iso;
651    } else {
652        // default country
653        select.value = 'NIC';
654    };
655    // force dropdown change
656    countryChanged();
657};
658
659// admin level
660
661function updateMainAdminLevelDropdown() {
662    // NOTE: requires that geoContrastMetadata has already been populated
663    // get admin-level dropdown
664    var select = document.getElementById('main-admin-level-select');
665    var selectVal = select.value;
666    // clear all existing dropdown options
667    select.innerHTML = '';
668    // get geoContrast metadata
669    var metadata = geoContrastMetadata;
670    // get current country and comparison source
671    var currentIso = document.getElementById('country-select').value;
672    var currentSource = document.getElementById('main-boundary-select').value;
673    // find available admin-levels for country
674    var adminLevelsSeen = [];
675    var adminLevels = [];
676    for (var i = 1; i < metadata.length; i++) {
677        var row = metadata[i];
678        if (row.length <= 1) {
679            // ignore empty rows
680            i++;
681            continue;
682        };
683        var iso = row.boundaryISO;
684        var lvl = row.boundaryType;
685        if (iso == currentIso) {
686            if (!(adminLevelsSeen.includes(lvl))) {
687                // only add if hasn't already been added
688                adminLevels.push(lvl);
689                adminLevelsSeen.push(lvl);
690            };
691        };
692    };
693    // sort
694    adminLevels.sort();
695    // add new options from geoContrastMetadata
696    for (lvl of adminLevels) {
697        var opt = document.createElement("option");
698        opt.value = lvl;
699        opt.textContent = lvl;
700        select.appendChild(opt);
701    };
702    // set the adm level to get-param if specified
703    const urlParams = new URLSearchParams(window.location.search);
704    var lvl = urlParams.get('mainLevel');
705    if ((lvl != null) & (lvl != select.value[3])) {
706        select.value = 'ADM'+lvl;
707    } else {
708        // default main level
709        select.value = 'ADM1';
710    };
711    // force dropdown change
712    mainAdminLevelChanged();
713};
714
715function updateComparisonAdminLevelDropdown() {
716    // NOTE: requires that geoContrastMetadata has already been populated
717    // get admin-level dropdown
718    var select = document.getElementById('comparison-admin-level-select');
719    var selectVal = select.value;
720    // clear all existing dropdown options
721    select.innerHTML = '';
722    // get geoContrast metadata
723    var metadata = geoContrastMetadata;
724    // get current country and comparison source
725    var currentIso = document.getElementById('country-select').value;
726    var currentSource = document.getElementById('comparison-boundary-select').value;
727    // find available admin-levels for country
728    var adminLevelsSeen = [];
729    var adminLevels = [];
730    for (var i = 1; i < metadata.length; i++) {
731        var row = metadata[i];
732        if (row.length <= 1) {
733            // ignore empty rows
734            i++;
735            continue;
736        };
737        var iso = row.boundaryISO;
738        var lvl = row.boundaryType;
739        if (iso == currentIso) {
740            if (!(adminLevelsSeen.includes(lvl))) {
741                // only add if hasn't already been added
742                adminLevels.push(lvl);
743                adminLevelsSeen.push(lvl);
744            };
745        };
746    };
747    // sort
748    adminLevels.sort();
749    // add new options from geoContrastMetadata
750    for (lvl of adminLevels) {
751        var opt = document.createElement("option");
752        opt.value = lvl;
753        opt.textContent = lvl;
754        select.appendChild(opt);
755    };
756    // set the adm level to get-param if specified
757    const urlParams = new URLSearchParams(window.location.search);
758    var lvl = urlParams.get('comparisonLevel');
759    if ((lvl != null) & (lvl != select.value[3])) {
760        select.value = 'ADM'+lvl;
761    } else {
762        // default comparison level
763        select.value = 'ADM1';
764    };
765    // force dropdown change
766    comparisonAdminLevelChanged();
767};
768
769// sources
770
771function openMainSourcePopup() {
772    popup = document.getElementById('main-source-popup');
773    popup.className = "popup";
774    tableDiv = popup.querySelector('#main-sources-table').parentNode;
775    tableDiv.scrollTop = 0;
776};
777
778function closeMainSourcePopup() {
779    popup = document.getElementById('main-source-popup');
780    popup.className = "popup is-hidden is-visually-hidden";
781};
782
783function openComparisonSourcePopup() {
784    popup = document.getElementById('comparison-source-popup');
785    popup.className = "popup";
786    tableDiv = popup.querySelector('#comparison-sources-table').parentNode;
787    tableDiv.scrollTop = 0;
788};
789
790function closeComparisonSourcePopup() {
791    popup = document.getElementById('comparison-source-popup');
792    popup.className = "popup is-hidden is-visually-hidden";
793};
794
795function updateMainSourceTable() {
796    //console.log('update main source table');
797    var currentIso = document.getElementById('country-select').value;
798    var currentLevel = document.getElementById('main-admin-level-select').value;
799    // clear sources table
800    var table = document.querySelector('#main-sources-table tbody');
801    table.innerHTML = '';
802    // get geoContrast metadata
803    var metadata = geoContrastMetadata;
804    // get list of sources that match the specified country
805    var sourceRows = [];
806    for (var i = 1; i < metadata.length; i++) {
807        var sourceRow = metadata[i];
808        if (sourceRow.length <= 1) {
809            // ignore empty rows
810            i++;
811            continue;
812        };
813        // skip any rows that don't match the country and level
814        if (!(sourceRow['boundaryISO']==currentIso & sourceRow['boundaryType']==currentLevel)) {
815            continue;
816        };
817        sourceRows.push(sourceRow);
818    };
819    
820    // sort alphabetically
821    sortBy(sourceRows, 'boundarySource-1');
822    var currentSource = document.getElementById("main-boundary-select").value;
823
824    // begin adding data to sources table
825    for (sourceRow of sourceRows) {
826        var tr = document.createElement('tr');
827        if (sourceRow['boundarySource-1']==currentSource) {
828            tr.style.backgroundColor = '#F0B323'; //'rgba(255,213,128,0.4)';
829            tr.style.color = 'white';
830        };
831        // select button
832        var td = document.createElement('td');
833        var but = document.createElement('button');
834        but.innerHTML = '&#x2714';
835        but.data = sourceRow['boundarySource-1'];
836        if (sourceRow['boundarySource-1']==currentSource) {
837            but.style.filter = 'brightness(1000)';
838        };
839        function onclick() {
840            var sel = document.getElementById("main-boundary-select");
841            sel.value = this.data;
842            // force dropdown change
843            mainSourceChanged();
844            // close
845            closeMainSourcePopup();
846        };
847        but.onclick = onclick;
848        //but.setAttribute('onclick', onclick);
849        //but.style.padding = '5px'
850        //but.style.margin = 0;
851        td.appendChild(but);
852        tr.appendChild(td);
853        // source name
854        var td = document.createElement('td');
855        if (sourceRow['boundarySource-1']=='upload') {
856            td.innerText = 'Upload Your Own Boundary';
857        } else {
858            td.innerText = sourceRow['boundarySource-1'];
859        }
860        tr.appendChild(td);
861        // license
862        var td = document.createElement('td');
863        td.innerText = sourceRow.boundaryLicense;
864        tr.appendChild(td);
865        // year
866        var td = document.createElement('td');
867        td.innerText = sourceRow.boundaryYearRepresented;
868        tr.appendChild(td);
869        // updated
870        var td = document.createElement('td');
871        td.innerText = sourceRow.sourceDataUpdateDate;
872        tr.appendChild(td);
873        // unit count
874        var td = document.createElement('td');
875        td.innerText = sourceRow.boundaryCount;
876        tr.appendChild(td);
877        // min line res
878        var td = document.createElement('td');
879        td.innerText = parseFloat(sourceRow.statsLineResolution).toFixed(1) + ' m';
880        tr.appendChild(td);
881        // max vert dens
882        var td = document.createElement('td');
883        td.innerText = parseFloat(sourceRow.statsVertexDensity).toFixed(1) + ' / km';
884        tr.appendChild(td);
885        //
886        table.appendChild(tr);
887    };
888};
889
890function updateMainSourceDropdown() {
891    //alert('update main boundary dropdown');
892    // update source table
893    updateMainSourceTable();
894    // get current country and level
895    var currentIso = document.getElementById('country-select').value;
896    var currentLevel = document.getElementById('main-admin-level-select').value;
897    // get source dropdown
898    var select = document.getElementById('main-boundary-select');
899    var selectVal = select.value;
900    // clear all existing dropdown options
901    select.innerHTML = '';
902    // get geoContrast metadata
903    var metadata = geoContrastMetadata;
904    // get list of sources that match the specified country
905    var sources = [];
906    var sourcesSeen = [];
907    for (var i = 1; i < metadata.length; i++) {
908        var row = metadata[i];
909        if (row.length <= 1) {
910            // ignore empty rows
911            i++;
912            continue;
913        };
914        var source = row['boundarySource-1'];
915        var iso = row.boundaryISO;
916        var level = row.boundaryType;
917        if ((iso==currentIso) & (level==currentLevel)) {
918            if (!(sourcesSeen.includes(source))) {
919                // only add if hasn't already been added
920                sourcesSeen.push(source);
921                sources.push(source);
922            };
923        };
924    };
925    // sort
926    sources.sort()
927    // add new options from geoContrastMetadata
928    for (source of sources) {
929        var opt = document.createElement("option");
930        opt.value = source;
931        opt.textContent = source;
932        select.appendChild(opt);
933    };
934    // finally add custom upload boundary choice
935    opt = document.createElement('option');
936    opt.value = 'upload';
937    opt.textContent = 'Upload Your Own Boundary';
938    select.appendChild(opt);
939    sources.push('upload');
940    // set the source to get-param if specified
941    const urlParams = new URLSearchParams(window.location.search);
942    var source = urlParams.get('mainSource');
943    var defaultSource = 'geoBoundaries (Open)';
944    if (source != null & sources.includes(source)) {
945        select.value = source;
946    } else if (sources.includes(defaultSource)) {
947        // default source
948        select.value = defaultSource;
949    } else {
950        // default not available, use first available source
951        select.value = sources[0];
952    };
953    // force dropdown change
954    mainSourceChanged();
955};
956
957function updateComparisonSourceTable() {
958    //console.log('update comparison source table');
959    var currentIso = document.getElementById('country-select').value;
960    var currentLevel = document.getElementById('comparison-admin-level-select').value;
961    // clear sources table
962    var table = document.querySelector('#comparison-sources-table tbody');
963    table.innerHTML = '';
964    // get geoContrast metadata
965    var metadata = geoContrastMetadata;
966    // get list of sources that match the specified country
967    var sourceRows = [];
968    for (var i = 1; i < metadata.length; i++) {
969        var sourceRow = metadata[i];
970        if (sourceRow.length <= 1) {
971            // ignore empty rows
972            i++;
973            continue;
974        };
975        // skip any rows that don't match the country and level
976        if (!(sourceRow['boundaryISO']==currentIso & sourceRow['boundaryType']==currentLevel)) {
977            continue;
978        };
979        sourceRows.push(sourceRow);
980    };
981    
982    // sort alphabetically
983    sortBy(sourceRows, 'boundarySource-1');
984    var currentSource = document.getElementById("comparison-boundary-select").value;
985
986    // begin adding data to sources table
987    for (sourceRow of sourceRows) {
988        var tr = document.createElement('tr');
989        if (sourceRow['boundarySource-1']==currentSource) {
990            tr.style.backgroundColor = '#F0B323'; //'rgba(255,213,128,0.4)';
991            tr.style.color = 'white';
992        };
993        // select button
994        var td = document.createElement('td');
995        var but = document.createElement('button');
996        but.innerHTML = '&#x2714';
997        but.data = sourceRow['boundarySource-1'];
998        if (sourceRow['boundarySource-1']==currentSource) {
999            but.style.filter = 'brightness(1000)';
1000        };
1001        function onclick() {
1002            var sel = document.getElementById("comparison-boundary-select");
1003            sel.value = this.data;
1004            // force dropdown change
1005            comparisonSourceChanged();
1006            // close
1007            closeComparisonSourcePopup();
1008        };
1009        but.onclick = onclick;
1010        //but.setAttribute('onclick', onclick);
1011        //but.style.padding = '5px'
1012        //but.style.margin = 0;
1013        td.appendChild(but);
1014        tr.appendChild(td);
1015        // source name
1016        var td = document.createElement('td');
1017        if (sourceRow['boundarySource-1']=='upload') {
1018            td.innerText = 'Upload Your Own Boundary';
1019        } else {
1020            td.innerText = sourceRow['boundarySource-1'];
1021        }
1022        tr.appendChild(td);
1023        // license
1024        var td = document.createElement('td');
1025        td.innerText = sourceRow.boundaryLicense;
1026        tr.appendChild(td);
1027        // year
1028        var td = document.createElement('td');
1029        td.innerText = sourceRow.boundaryYearRepresented;
1030        tr.appendChild(td);
1031        // updated
1032        var td = document.createElement('td');
1033        td.innerText = sourceRow.sourceDataUpdateDate;
1034        tr.appendChild(td);
1035        // unit count
1036        var td = document.createElement('td');
1037        td.innerText = sourceRow.boundaryCount;
1038        tr.appendChild(td);
1039        // min line res
1040        var td = document.createElement('td');
1041        td.innerText = parseFloat(sourceRow.statsLineResolution).toFixed(1) + ' m';
1042        tr.appendChild(td);
1043        // max vert dens
1044        var td = document.createElement('td');
1045        td.innerText = parseFloat(sourceRow.statsVertexDensity).toFixed(1) + ' / km';
1046        tr.appendChild(td);
1047        //
1048        table.appendChild(tr);
1049    };
1050};
1051
1052function updateComparisonSourceDropdown() {
1053    //alert('update comparison boundary dropdown');
1054    // update source table
1055    updateComparisonSourceTable();
1056    // get current country and level
1057    var currentIso = document.getElementById('country-select').value;
1058    var currentLevel = document.getElementById('comparison-admin-level-select').value;
1059    // get source dropdown
1060    var select = document.getElementById('comparison-boundary-select');
1061    var selectVal = select.value;
1062    // clear all existing dropdown options
1063    select.innerHTML = '';
1064    // get geoContrast metadata
1065    var metadata = geoContrastMetadata;
1066    // get list of sources that match the specified country
1067    var sources = [];
1068    var sourcesSeen = [];
1069    for (var i = 1; i < metadata.length; i++) {
1070        var row = metadata[i];
1071        if (row.length <= 1) {
1072            // ignore empty rows
1073            i++;
1074            continue;
1075        };
1076        var source = row['boundarySource-1'];
1077        var iso = row.boundaryISO;
1078        var level = row.boundaryType;
1079        if ((iso==currentIso) & (level==currentLevel)) {
1080            if (!(sourcesSeen.includes(source))) {
1081                // only add if hasn't already been added
1082                sourcesSeen.push(source);
1083                sources.push(source);
1084            };
1085        };
1086    };
1087    // sort
1088    sources.sort()
1089    // add new options from geoContrastMetadata
1090    for (source of sources) {
1091        var opt = document.createElement("option");
1092        opt.value = source;
1093        opt.textContent = source;
1094        select.appendChild(opt);
1095    };
1096    // finally add custom upload boundary choice
1097    opt = document.createElement('option');
1098    opt.value = 'upload';
1099    opt.textContent = 'Upload Your Own Boundary';
1100    select.appendChild(opt);
1101    sources.push('upload');
1102    // set the source to get-param if specified
1103    const urlParams = new URLSearchParams(window.location.search);
1104    var source = urlParams.get('comparisonSource');
1105    var defaultSource = 'GADM v3.6';
1106    if (source != null & sources.includes(source)) {
1107        select.value = source;
1108    } else if (sources.includes(defaultSource)) {
1109        // default source
1110        select.value = defaultSource;
1111    } else {
1112        // default not available, use first available source
1113        select.value = sources[0];
1114    };
1115    // force dropdown change
1116    comparisonSourceChanged();
1117};
1118
1119
1120
1121
1122
1123
1124
1125
1126/////////////////////////////
1127// dropdown change behavior
1128
1129function updateGetParams() {
1130    const urlParams = new URLSearchParams(window.location.search);
1131    // set country
1132    var select = document.getElementById('country-select');
1133    if (select.value == '') {return}; // to avoid errors at startup when not all selects have been populated
1134    urlParams.set('country', select.value);
1135    // set main source
1136    var select = document.getElementById('main-boundary-select');
1137    if (select.value == '') {return}; // to avoid errors at startup when not all selects have been populated
1138    urlParams.set('mainSource', select.value);
1139    // set comparison source
1140    var select = document.getElementById('comparison-boundary-select');
1141    if (select.value == '') {return}; // to avoid errors at startup when not all selects have been populated
1142    urlParams.set('comparisonSource', select.value);
1143    // set main adm level
1144    var select = document.getElementById('main-admin-level-select');
1145    if (select.value == '') {return}; // to avoid errors at startup when not all selects have been populated
1146    urlParams.set('mainLevel', select.value[select.value.length-1]); // only the adm number
1147    // set comparison adm level
1148    var select = document.getElementById('comparison-admin-level-select');
1149    if (select.value == '') {return}; // to avoid errors at startup when not all selects have been populated
1150    urlParams.set('comparisonLevel', select.value[select.value.length-1]); // only the adm number
1151    // update url
1152    //window.location.search = urlParams;
1153    var newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + urlParams.toString();
1154    //alert(newUrl);
1155    window.history.replaceState({path:newUrl}, '', newUrl);
1156};
1157
1158// country
1159
1160function countryChanged() {
1161    //alert('country changed');
1162    updateMainAdminLevelDropdown();
1163    updateComparisonAdminLevelDropdown();
1164    //updateGetParams();
1165};
1166
1167// admin levels
1168
1169function mainAdminLevelChanged() {
1170    //alert('comparison admin-level changed');
1171    updateMainSourceDropdown();
1172    //updateGetParams();
1173};
1174
1175function comparisonAdminLevelChanged() {
1176    //alert('comparison admin-level changed');
1177    updateComparisonSourceDropdown();
1178    //updateGetParams();
1179};
1180
1181// sources
1182
1183function mainUploadButtonPushed() {
1184    val = document.getElementById('main-boundary-select').value;
1185    if (val == 'upload') {
1186        // already in upload mode
1187        // get available sources
1188        select = document.getElementById('main-boundary-select');
1189        sourceVals = [];
1190        for (opt of select.options) {
1191            sourceVals.push(opt.value);
1192        };
1193        // set dropdown value to first available source
1194        newVal = 'none';
1195        if (sourceVals.length) {
1196            newVal = sourceVals[0];
1197        };
1198        document.getElementById('main-boundary-select').value = newVal;
1199    } else {
1200        // switch to upload mode
1201        // set dropdown value to upload
1202        document.getElementById('main-boundary-select').value = 'upload';
1203    };
1204    // trigger source changed
1205    mainSourceChanged();
1206};
1207
1208function comparisonUploadButtonPushed() {
1209    val = document.getElementById('comparison-boundary-select').value;
1210    if (val == 'upload') {
1211        // already in upload mode
1212        // get available sources
1213        select = document.getElementById('comparison-boundary-select');
1214        sourceVals = [];
1215        for (opt of select.options) {
1216            sourceVals.push(opt.value);
1217        };
1218        // set dropdown value to first available source
1219        newVal = 'none';
1220        if (sourceVals.length) {
1221            newVal = sourceVals[0];
1222        };
1223        document.getElementById('comparison-boundary-select').value = newVal;
1224    } else {
1225        // switch to upload mode
1226        // set dropdown value to upload
1227        document.getElementById('comparison-boundary-select').value = 'upload';
1228    };
1229    // trigger source changed
1230    comparisonSourceChanged();
1231};
1232
1233function mainSourceChanged() {
1234    //alert('main source changed');
1235    // update source table
1236    updateMainSourceTable();
1237    // empty misc info
1238    clearMainInfo();
1239    clearMainStats();
1240    clearMatchTable(); //clearMainNames();
1241    clearTotalEquality();
1242    // clear comparison layer
1243    clearMainLayer();
1244    // check which comparison source was selected
1245    source = document.getElementById('main-boundary-select').value;
1246    uploadOnSymbol = '&#x00d7;';
1247    uploadOffSymbol = '<i class="fa fa-upload" aria-hidden="true"></i>';
1248    if (source == 'none' | source == '') {
1249        // activate admin level button
1250        document.getElementById('main-admin-level-select').style.display = '';
1251        document.getElementById('main-boundary-select').style.display = '';
1252        // reset upload button style
1253        document.getElementById('main-boundary-upload-button').className = "upload-button";
1254        document.getElementById('main-boundary-upload-button').innerHTML = uploadOffSymbol;
1255        // hide file button elements
1256        document.getElementById('main-boundary-upload-title').style.display = 'none';
1257        document.getElementById('main-file-div').style.display = 'none';
1258    } else if (source == 'upload') {
1259        // disable admin level button
1260        document.getElementById('main-admin-level-select').style.display = 'none';
1261        document.getElementById('main-boundary-select').style.display = 'none';
1262        // reset
1263        document.getElementById('main-file-input').value = null;
1264        document.getElementById('main-file-select').innerHTML = '<option value="" disabled selected hidden>(Please select a boundary)</option>';
1265        document.getElementById('main-file-select').disabled = true;
1266        // change upload button style to active
1267        document.getElementById('main-boundary-upload-button').className = "upload-button upload-button-active";
1268        document.getElementById('main-boundary-upload-button').innerHTML = uploadOnSymbol;
1269        // show file button elements
1270        document.getElementById('main-boundary-upload-title').style.display = '';
1271        document.getElementById('main-file-div').style.display = 'block';
1272    } else {
1273        // activate admin level button
1274        document.getElementById('main-admin-level-select').style.display = '';
1275        document.getElementById('main-boundary-select').style.display = '';
1276        // reset upload button style
1277        document.getElementById('main-boundary-upload-button').className = "upload-button";
1278        document.getElementById('main-boundary-upload-button').innerHTML = uploadOffSymbol;
1279        // hide file button elements
1280        document.getElementById('main-boundary-upload-title').style.display = 'none';
1281        document.getElementById('main-file-div').style.display = 'none';
1282        // update main layer with external geoContrast topojson
1283        updateMainLayer(zoomToExtent=true);
1284    };
1285    updateGetParams();
1286};
1287
1288function comparisonSourceChanged() {
1289    //alert('comparison source changed');
1290    // update source table
1291    updateComparisonSourceTable();
1292    // clear misc info
1293    clearComparisonInfo();
1294    clearComparisonStats();
1295    clearMatchTable(); //clearComparisonNames();
1296    clearTotalEquality();
1297    // clear comparison layer
1298    clearComparisonLayer();
1299    // check which comparison source was selected
1300    source = document.getElementById('comparison-boundary-select').value;
1301    uploadOnSymbol = '&#x00d7;';
1302    uploadOffSymbol = '<i class="fa fa-upload" aria-hidden="true"></i>';
1303    if (source == 'none' | source == '') {
1304        // activate admin level button
1305        document.getElementById('comparison-admin-level-select').style.display = '';
1306        document.getElementById('comparison-boundary-select').style.display = '';
1307        // reset upload button style
1308        document.getElementById('comparison-boundary-upload-button').className = "upload-button";
1309        document.getElementById('comparison-boundary-upload-button').innerHTML = uploadOffSymbol;
1310        // hide file button elements
1311        document.getElementById('comparison-boundary-upload-title').style.display = 'none';
1312        document.getElementById('comparison-file-div').style.display = 'none';
1313    } else if (source == 'upload') {
1314        // disable admin level button
1315        document.getElementById('comparison-admin-level-select').style.display = 'none';
1316        document.getElementById('comparison-boundary-select').style.display = 'none';
1317        // reset
1318        document.getElementById('comparison-file-input').value = null;
1319        document.getElementById('comparison-file-select').innerHTML = '<option value="" disabled selected hidden>(Please select a boundary)</option>';
1320        document.getElementById('comparison-file-select').disabled = true;
1321        // change upload button style to active
1322        document.getElementById('comparison-boundary-upload-button').className = "upload-button upload-button-active";
1323        document.getElementById('comparison-boundary-upload-button').innerHTML = uploadOnSymbol;
1324        // show file button elements
1325        document.getElementById('comparison-boundary-upload-title').style.display = '';
1326        document.getElementById('comparison-file-div').style.display = 'block';
1327    } else {
1328        // activate admin level button
1329        document.getElementById('comparison-admin-level-select').style.display = '';
1330        document.getElementById('comparison-boundary-select').style.display = '';
1331        // reset upload button style
1332        document.getElementById('comparison-boundary-upload-button').className = "upload-button";
1333        document.getElementById('comparison-boundary-upload-button').innerHTML = uploadOffSymbol;
1334        // hide file button elements
1335        document.getElementById('comparison-boundary-upload-title').style.display = 'none';
1336        document.getElementById('comparison-file-div').style.display = 'none';
1337        // update main layer with external geoContrast topojson
1338        updateComparisonLayer(zoomToExtent=true);
1339    };
1340    updateGetParams();
1341};
1342
1343
1344// pdf report
1345
1346function getPageWrapper() {
1347    // create page wrapper with padding
1348    var wrapper = document.createElement('div');
1349    wrapper.style.padding = '50px';
1350    // add top header
1351    var header = document.createElement('header'); //document.getElementById('header').cloneNode(true);
1352    header.className = "logo";
1353    header.style = "width:100%; margin-bottom:40px";
1354    header.innerHTML = `
1355        <span style="float:left">
1356            <strong>Boundary comparison report: <span class="countryName"></span></strong>
1357        </span>
1358        <span style="float:right">
1359            William &amp; Mary geoLab
1360        </span>
1361    `
1362    // set country name
1363    var countrySelect = document.getElementById('country-select');
1364    var country = countrySelect.options[countrySelect.selectedIndex].text;
1365    header.querySelector('.countryName').innerText = country;
1366    wrapper.appendChild(header);
1367    // add header line
1368    var hr = document.createElement('hr');
1369    hr.style = "border-color:#F0B323; background-color:#F0B323; margin:3px; height:6px; margin-bottom:30px"
1370    wrapper.appendChild(hr);
1371    // custom css to display links without color
1372    var style = document.createElement('style');
1373    style.innerHTML = `
1374        a {
1375            color: inherit;
1376        }
1377    `;
1378    wrapper.appendChild(style);
1379    return wrapper;
1380}
1381
1382async function renderFrontPage(mapImgData) {
1383    // get page wrapper
1384    var wrapper = getPageWrapper();
1385    // add top title incl country
1386    var titleDiv = document.createElement('div');
1387    titleDiv.innerHTML = `
1388        <h1 style="margin:10px 0px">Boundary comparison report</h1>
1389        <h2 id="country-header-for-printing" style="margin:10px 0px"></h2>
1390        <br>
1391    `
1392    var countrySelect = document.getElementById('country-select');
1393    var country = countrySelect.options[countrySelect.selectedIndex].text;
1394    titleDiv.querySelector('#country-header-for-printing').innerText = country + ', administrative boundaries';
1395    wrapper.appendChild(titleDiv);
1396    // add map
1397    var mapDiv = document.createElement('div');
1398    mapDiv.innerHTML = `
1399        <style>
1400            #map-image-legend {
1401                position:absolute;
1402                top:10px;
1403                left:10px;
1404                background-color:white; 
1405                border-radius:5px; 
1406                opacity:0.8;
1407                color:black;
1408            }
1409
1410            #map-image-legend #layer-list {
1411                list-style-type: none;
1412                margin: 0;
1413            }
1414            
1415            #map-image-legend #layer-list li {
1416                display: flex;
1417                flex-direction: row;
1418                align-items: center;
1419                margin:5px;
1420            }
1421            
1422            #legend-symbol-main {
1423                width: 20px;
1424                border-top: 4px solid #319FD3;
1425                border-bottom: unset;
1426                margin: 5px;
1427            }
1428            
1429            #legend-symbol-comparison {
1430                width: 20px;
1431                border-top: 4px dashed rgb(255,0,0);
1432                border-bottom: unset;
1433                margin: 5px;
1434            }
1435        </style>
1436        <div style="position:relative; width:100%; height:75%; margin:0; padding:0; text-align:center">
1437            <img id="map-image-for-printing" style="width:100%; height:100%; object-fit:cover" crossorigin="anonymous"/>
1438            <div id="map-image-legend">
1439                <ul id="layer-list">
1440                    <li>
1441                        <hr id="legend-symbol-main">
1442                        <span class="main-source-title">Main boundary</span>
1443                    </li>
1444                    <li>
1445                        <hr id="legend-symbol-comparison">
1446                        <span class="comp-source-title">Comparison boundary</span>
1447                    </li>
1448                </ul>
1449            </div>
1450        </div>
1451    `
1452    wrapper.appendChild(mapDiv);
1453    // populate legend
1454    var mainSource = document.querySelector('.main-source-title').innerText;
1455    var comparisonSource = document.querySelector('.comp-source-title').innerText;
1456    mapDiv.querySelector('.main-source-title').innerText = mainSource;
1457    mapDiv.querySelector('.comp-source-title').innerText = comparisonSource;
1458    // set map img src
1459    var mapImg = wrapper.querySelector('#map-image-for-printing');
1460    mapImg.src = mapImgData;
1461    // render to image
1462    var config = {};
1463    document.body.appendChild(wrapper);
1464    var canvas = await html2canvas(wrapper, config);
1465    wrapper.remove();
1466    return canvas;
1467}
1468
1469async function renderMetaStatsPage() {
1470    // get page wrapper
1471    var wrapper = getPageWrapper();
1472    // add meta
1473    var banner = document.getElementById('source-overview-banner').cloneNode(true);
1474    wrapper.appendChild(banner);
1475    metaBox = document.getElementById('source-overview').cloneNode(true);
1476    wrapper.appendChild(metaBox);
1477    var br = document.createElement('br');
1478    wrapper.appendChild(br);
1479    var br = document.createElement('br');
1480    wrapper.appendChild(br);
1481    // add stats
1482    var banner = document.getElementById('source-stats-banner').cloneNode(true);
1483    wrapper.appendChild(banner);
1484    statsBox = document.getElementById('source-stats').cloneNode(true);
1485    wrapper.appendChild(statsBox);
1486    var br = document.createElement('br');
1487    wrapper.appendChild(br);
1488    // render to image
1489    var config = {};
1490    document.body.appendChild(wrapper);
1491    var canvas = await html2canvas(wrapper, config);
1492    wrapper.remove();
1493    return canvas;
1494}
1495
1496async function renderAgreementPage() {
1497    // get table row info
1498    var matchTable = document.getElementById('match-table-div');
1499    var rows = matchTable.querySelectorAll('tr');
1500    var rowCount = rows.length;
1501    var rowNum = 0;
1502    // add new page for every 20 rows
1503    var canvases = [];
1504    while (rowNum <= (rowCount-1)) {
1505        // get page wrapper
1506        var wrapper = getPageWrapper();
1507        // banner
1508        var banner = document.getElementById('source-contents-banner').cloneNode(true);
1509        wrapper.appendChild(banner);
1510        // box
1511        var box = document.createElement('div');
1512        box.className = 'box row';
1513        box.style = "width:100%; margin:0";
1514        wrapper.appendChild(box);
1515        // top title and match percent
1516        if (rowNum == 0) {
1517            var banner = document.getElementById('source-overlap-total').cloneNode(true);
1518            box.appendChild(banner);
1519            var br = document.createElement('br');
1520            box.appendChild(br);
1521        } else {
1522            var banner = document.createElement('div');
1523            banner.innerHTML = '<br>';
1524            box.appendChild(banner);
1525        };
1526        // table of rows
1527        var table = document.createElement('table');
1528        table.style = "margin-left:10px; width:100%";
1529        var rowEnd = Math.min(rowNum + 20, rowCount-1);
1530        // add top header row
1531        if (rowNum != 0) {
1532            var row = rows[0];
1533            row.querySelector('#main-names-table-select').value = document.getElementById('#main-names-table-select').value;
1534            row.querySelector('#comparison-names-table-select').value = document.getElementById('#comparison-names-table-select').value;
1535            table.appendChild(row.cloneNode(true));
1536        };
1537        // add data rows
1538        var tbody = document.createElement('tbody');
1539        while (rowNum <= rowEnd) {
1540            var row = rows[rowNum];
1541            tbody.appendChild(row.cloneNode(true));
1542            rowNum += 1;
1543        };
1544        table.appendChild(tbody);
1545        box.appendChild(table);
1546        // add nomatch info at end of table
1547        if (rowNum == rowCount) {
1548            var noMatchDiv = document.getElementById('nomatch-div').cloneNode(true);
1549            box.appendChild(noMatchDiv);
1550        };
1551        // render to image
1552        var config = {};
1553        document.body.appendChild(wrapper);
1554        var canvas = await html2canvas(wrapper, config);
1555        wrapper.remove();
1556        canvases.push(canvas);
1557    };
1558    return canvases;
1559}
1560
1561async function makePDF(mapImgData) {
1562    // define doc
1563    var pageWidth = 210;
1564    var pageHeight = 295; 
1565    var doc = new jsPDF('portrait', 'mm');
1566
1567    // loop parts of the image and crop to each page
1568    var canvas = await renderFrontPage(mapImgData);
1569    var imgHeight = canvas.height * (pageWidth / canvas.width);
1570    doc.addImage(canvas.toDataURL("image/jpeg"), 'JPEG', 0, 0, pageWidth, imgHeight);
1571
1572    var canvas = await renderMetaStatsPage();
1573    var imgHeight = canvas.height * (pageWidth / canvas.width);
1574    doc.addPage();
1575    doc.addImage(canvas.toDataURL("image/jpeg"), 'JPEG', 0, 0, pageWidth, imgHeight);
1576
1577    var canvases = await renderAgreementPage();
1578    for (canvas of canvases) {
1579        var imgHeight = canvas.height * (pageWidth / canvas.width);
1580        doc.addPage();
1581        doc.addImage(canvas.toDataURL("image/jpeg"), 'JPEG', 0, 0, pageWidth, imgHeight);
1582    };
1583
1584    // show or save pdf
1585    //doc.output('datauri');
1586    doc.save('geoBoundaries-comparison.pdf');
1587    //window.open(doc.output('bloburl'));
1588    //doc.output('bloburi');
1589
1590    // reactivate pdf button
1591    but = document.getElementById('pdf-button');
1592    but.disabled = false;
1593    but.innerHTML = 'Generate PDF Report';
1594}
1595
1596function renderMapToImgData(processdata) {
1597    map.once('rendercomplete', function () {
1598        //alert('rendercomplete');
1599        const mapCanvas = document.createElement('canvas');
1600        const size = map.getSize();
1601        mapCanvas.width = size[0];
1602        mapCanvas.height = size[1];
1603        const mapContext = mapCanvas.getContext('2d');
1604        Array.prototype.forEach.call(
1605            document.querySelectorAll('#map .ol-layer canvas'),
1606            function (canvas) {
1607                if (canvas.width > 0) {
1608                    const opacity = canvas.parentNode.style.opacity;
1609                    mapContext.globalAlpha = opacity === '' ? 1 : Number(opacity);
1610                    const transform = canvas.style.transform;
1611                    // Get the transform parameters from the style's transform matrix
1612                    const matrix = transform
1613                        .match(/^matrix\(([^\(]*)\)$/)[1]
1614                        .split(',')
1615                        .map(Number);
1616                    // Apply the transform to the export map context
1617                    CanvasRenderingContext2D.prototype.setTransform.apply(
1618                        mapContext,
1619                        matrix
1620                    );
1621                    mapContext.drawImage(canvas, 0, 0);
1622                };
1623            }
1624        );
1625        imgData = mapCanvas.toDataURL();
1626        //alert('imgdata '+imgData);
1627        processdata(imgData);
1628    });
1629    map.renderSync();
1630};
1631
1632function printPageToPDF() {
1633    // update pdf button
1634    but = document.getElementById('pdf-button');
1635    but.innerHTML = '<div style="display:flex; flex-direction:row; align-items:center; gap:3px"><span>Generating</span><img src="images/Spinner-1s-200px.gif" style="max-width:30px; margin-right:-15px"/></div>';
1636    but.disabled = true;
1637    // render the map to image data
1638    // on success, set image data to img src and print the pdf
1639    var onsuccess = makePDF; 
1640    renderMapToImgData(onsuccess);
1641};
1642
1643

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.