JavaScriptで空間演算

2015/03/04
先日FirefoxとQGISで行った位置情報の取得・加工を、JavaScriptで行ってみる。
スクレイピングにはnode-horsemanを使用した。Nightmareによく似た使いやすいモジュール。結果をGeoJSON形式で出力する。1ページ5秒のウエイトと、結果に5000件の制限を設けてある。
var fs = require('fs');
1
var fs = require('fs');
2
 
3
var Horseman = require('node-horseman');
4
var horseman = new Horseman();
5
 
6
var results = [];
7
 
8
function getResult() {
9
    return horseman
10
        .evaluate(function() {
11
            var features = [];
12
            $('table[id="searchResult"] tbody tr:gt(1)').each(function(item) {
13
                var lat = $.trim($(this).find('td:eq(4)').text()).split("°");
14
                var lat_deg = parseFloat(lat[0]);
15
                lat = lat[1].split("′");
16
                var lat_min = parseFloat(lat[0]) / 60;
17
                lat = lat[1].split("″");
18
                var lat_sec = parseFloat(lat[0]) / 3600;
19
                var lng = $.trim($(this).find('td:eq(5)').text()).split("°");
20
                var lng_deg = parseFloat(lng[0]);
21
                lng = lng[1].split("′");
22
                var lng_min = parseFloat(lng[0]) / 60;
23
                lng = lng[1].split("″");
24
                var lng_sec = parseFloat(lng[0]) / 3600;
25
 
26
                var feature = {
27
                    'type': 'Feature',
28
                    'geometry': {
29
                        'type': 'Point',
30
                        'coordinates': [
31
                            (lng_deg + lng_min + lng_sec),
32
                            (lat_deg + lat_min + lat_sec)
33
                        ]
34
                    },
35
                    'properties': {
36
                        'id' :            $.trim($(this).find('td:eq(0) a').text()),
37
                        'project' :       $.trim($(this).find('td:eq(1)').text()),
38
                        'investigation' : $.trim($(this).find('td:eq(2)').text()),
39
                        'organization' :  $.trim($(this).find('td:eq(3)').text()),
40
                        'length' :        $.trim($(this).find('td:eq(6)').text()),
41
                        'elevation' :     $.trim($(this).find('td:eq(7)').text()),
42
                        'fig' :           $.trim($(this).find('td:eq(8) a').attr("href")),
43
                        'result' :        $.trim($(this).find('td:eq(9) a').attr("href")),
44
                        'xml' :           $.trim($(this).find('td:eq(0) a').attr("href"))
45
                    }
46
                };
47
                features.push(feature);
48
            });
49
            return features;
50
        });
51
}
52
 
53
function hasNextPage() {
54
    return horseman.exists('td.nextLink a');
55
}
56
 
57
function scrape() {
58
    var result = getResult();
59
    results = results.concat(result);
60
    var page = horseman
61
        .evaluate(function() {
62
            return $('td.pageCount').text();
63
        });
64
    console.log(page);
65
    if (hasNextPage() && results.length < 5000){
66
        horseman
67
            .click('td.nextLink a')
68
            .waitForNextPage()
69
            .wait(5000);
70
        scrape();
71
    }
72
}
73
 
74
horseman
75
    .userAgent("Mozilla/5.0 (Windows NT 6.3; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0")
76
    .viewport(1024, 768)
77
    .open('http://www.kunijiban.pwri.go.jp/jp/denshikokudo/DB_Search/boringsearch.php')
78
    .type('input[name="tl_lat_deg"]', '34')
79
    .type('input[name="tl_lat_min"]', '23')
80
    .type('input[name="tl_lat_sec"]', '52')
81
    .type('input[name="tl_lon_deg"]', '131')
82
    .type('input[name="tl_lon_min"]', '54')
83
    .type('input[name="tl_lon_sec"]', '03')
84
 
85
    .type('input[name="br_lat_deg"]', '32')
86
    .type('input[name="br_lat_min"]', '51')
87
    .type('input[name="br_lat_sec"]', '40')
88
    .type('input[name="br_lon_deg"]', '133')
89
    .type('input[name="br_lon_min"]', '52')
90
    .type('input[name="br_lon_sec"]', '03')
91
    .click('input[value="検索"]')
92
    .waitForSelector('td.nextLink a')
93
//    .screenshot('kunijiban.png');
94
 
95
scrape();
96
 
97
var GeoJSON = {
98
    "type": "FeatureCollection",
99
    "crs": {
100
        "type": "name",
101
        "properties": {
102
            "name": "urn:ogc:def:crs:EPSG::4612"
103
        }
104
    },
105
    "features": results
106
};
107
 
108
fs.writeFile('kunijiban.geojson', JSON.stringify(GeoJSON, null, "    "));
109
 
110
horseman.close();
 

次に行政区域の境界のバッファを作成して、空間演算により取得した検索結果のうち、愛媛県のみを抽出する。

行政区域の地形データはQGISで作成してもよいが、下記のGeoJSONを使用した。

dataofjapan/land


空間演算には下記のモジュールを使用した。

bjornharrtell/jsts
JSTS Example

下記のhtmlをローカルに置いてブラウザで読みこめば、同じディレクトリにあるjapan.geojson, kunijiban.geojsonを読み込んで地図上に表示する。ただしFirefoxとChromeのみ(Chromeはローカルファイルが読めるように、"--allow-file-access-from-files"オプションを付けて起動する必要がある)。空間演算が割と重くて時間がかかるので、ハングしたようになるかも。
<!DOCTYPE html>
1
<!DOCTYPE html>
2
<html>
3
 
4
<head>
5
    <title>ボーリング柱状図</title>
6
    <meta charset="utf-8" />
7
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
 
9
    <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css" />
10
 
11
    <script src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script>
12
    <script src="http://code.jquery.com/jquery-2.1.0.min.js"></script>
13
    <script src="https://rawgithub.com/bjornharrtell/jsts/master/lib/javascript.util.js"></script>
14
    <script src="https://rawgithub.com/bjornharrtell/jsts/master/lib/jsts.js"></script>
15
 
16
    <link rel="kunijiban" type="application/json" href="kunijiban.geojson">
17
    <link rel="japan" type="application/json" href="japan.geojson">
18
 
19
    <style>
20
        html,
21
        body,
22
        #map {
23
            height: 100%;
24
            width: 100%;
25
            padding: 0px;
26
            margin: 0px;
27
        }
28
    </style>
29
</head>
30
 
31
<body>
32
    <div id="map"></div>
33
 
34
    <script>
35
        var map = L.map('map').setView([33.523342, 132.863776], 10);
36
        L.control.scale({'position':'bottomleft','metric':true,'imperial':false}).addTo(map);
37
 
38
        var distance = 5 / 111.12; // 5km
39
        var geoReader = new jsts.io.GeoJSONReader(),
40
            geoWriter = new jsts.io.GeoJSONWriter();
41
 
42
        var cyber = L.tileLayer('http://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png', {
43
            maxZoom: 18,
44
            attribution: '<a href="http://www.gsi.go.jp/kikakuchousei/kikakuchousei40182.html" target="_blank">国土地理院</a>'
45
        }).addTo(map);
46
        map.addLayer(cyber);
47
 
48
        var gbank = L.tileLayer.wms("https://gbank.gsj.jp/ows/seamlessgeology200k_b", {
49
            layers: ['area,line,label'],
50
            format: 'image/png',
51
            transparent: true,
52
            opacity: 0.5,
53
            attribution: '<a href="https://gbank.gsj.jp/seamless/" target="_blank">シームレス地質図</a>'
54
        }).addTo(map);
55
        map.addLayer(gbank);
56
 
57
        L.control.layers({'地理院地図': cyber}, {'シームレス地質図': gbank}).addTo(map);
58
 
59
        function onEachFeature(feature, layer) {
60
            var popupContent = '<p><a href="' + feature.properties.fig + '" target="_blank">';
61
            popupContent += feature.properties.id + '</a><br />';
62
            popupContent += feature.properties.investigation + '</p>';
63
 
64
            layer.bindPopup(popupContent);
65
        }
66
 
67
        function createRegionBuffer() {
68
            return new Promise(function (resolve, reject) {
69
                $.getJSON($('link[rel="japan"]').attr("href"), function(data) {
70
                    var district;
71
                    data.features.forEach(function(item, index) {
72
                        if (item.properties.nam_ja == '愛媛県') district = item;
73
                    });
74
                    resolve(geoReader.read(district.geometry).buffer(distance));
75
                });
76
            });
77
        }
78
 
79
        function readBoringData(region) {
80
            return new Promise(function (resolve, reject) {
81
                $.getJSON($('link[rel="kunijiban"]').attr("href"), function(data) {
82
                    var inRegion = data.features.filter(function(item, index) {
83
                        return geoReader.read(item.geometry).within(region);
84
                    });
85
                    resolve(inRegion);
86
                });
87
            });
88
        }
89
 
90
        function renderRegionBuffer(region) {
91
            return new Promise(function (resolve, reject) {
92
                var buffer = geoWriter.write(region);
93
                L.geoJson(buffer, {
94
                    style: {
95
                        weight: 2,
96
                        color: "#999",
97
                        opacity: 1,
98
                        fillColor: "#B0DE5C",
99
                        fillOpacity: 0.8
100
                    }
101
                }).addTo(map);
102
                resolve(region);
103
            });
104
        }
105
 
106
        function renderBoringData(points) {
107
            L.geoJson(points, {
108
                onEachFeature: onEachFeature,
109
 
110
                pointToLayer: function(feature, latlng) {
111
                    return L.circleMarker(latlng, {
112
                        radius: 8,
113
                        fillColor: "#ff7800",
114
                        color: "#000",
115
                        weight: 1,
116
                        opacity: 1,
117
                        fillOpacity: 0.8
118
                    });
119
                }
120
            }).addTo(map);
121
        }
122
 
123
        createRegionBuffer()
124
//        .then(renderRegionBuffer)
125
        .then(readBoringData)
126
        .then(renderBoringData);
127
    </script>
128
</body>
129
 
130
</html>
 

マップは国土地理院のタイルと、日本シームレス地質図を使用している。

0 件のコメント:

コメントを投稿