pbf_reverse_geocoder 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +15 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE.txt +21 -0
- data/README.md +264 -0
- data/Rakefile +8 -0
- data/example.rb +17 -0
- data/lib/pbf_reverse_geocoder/geometry_decoder.rb +128 -0
- data/lib/pbf_reverse_geocoder/pbf_tile_reader.rb +90 -0
- data/lib/pbf_reverse_geocoder/point_in_polygon.rb +44 -0
- data/lib/pbf_reverse_geocoder/simple_pbf_parser.rb +228 -0
- data/lib/pbf_reverse_geocoder/tile_calculator.rb +55 -0
- data/lib/pbf_reverse_geocoder/version.rb +5 -0
- data/lib/pbf_reverse_geocoder.rb +45 -0
- data/pbf_reverse_geocoder.gemspec +40 -0
- metadata +103 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 70731208b30a178b40807a72415fcf6aede25018caadf24b2606b152a87c6e81
|
|
4
|
+
data.tar.gz: 53526751cd21450e88880bb3e12175641a161fea6907531b0d33418240a49625
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d7fcf2add8ef30e4fb13eb349787d49e632c0de0001b97a159f2b7ca9d73364e52a8f6d5f367989422338948be134f2e0408055342e24863c0c831d1b8e98264
|
|
7
|
+
data.tar.gz: 6a80ed60cdc9c56e9a052568d541e84fdd864b3113894ef95bafc5c419fcda3beebeeee1150be6147f4683e7a5a3653b20ac3ab3d446eea27b443a1fc2a1d5f3
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# RuboCop configuration for pbf_reverse_geocoder
|
|
2
|
+
|
|
3
|
+
AllCops:
|
|
4
|
+
TargetRubyVersion: 3.4
|
|
5
|
+
NewCops: enable
|
|
6
|
+
SuggestExtensions: false
|
|
7
|
+
Exclude:
|
|
8
|
+
- 'vendor/**/*'
|
|
9
|
+
- 'tmp/**/*'
|
|
10
|
+
|
|
11
|
+
# gemspecとRakefileはブロックが長くなるため除外
|
|
12
|
+
Metrics/BlockLength:
|
|
13
|
+
Exclude:
|
|
14
|
+
- 'pbf_reverse_geocoder.gemspec'
|
|
15
|
+
- 'Rakefile'
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.4.7
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# 変更履歴
|
|
2
|
+
|
|
3
|
+
このプロジェクトの主要な変更はこのファイルに記録されます。
|
|
4
|
+
|
|
5
|
+
形式は [Keep a Changelog](https://keepachangelog.com/ja/1.0.0/) に基づいており、
|
|
6
|
+
バージョニングは [Semantic Versioning](https://semver.org/lang/ja/) に準拠しています。
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2025-11-19
|
|
11
|
+
|
|
12
|
+
### 追加
|
|
13
|
+
- 初回リリース
|
|
14
|
+
- Mapbox Vector Tiles用のPBFタイルリーダー
|
|
15
|
+
- MVT形式のジオメトリデコーダー
|
|
16
|
+
- Ray Casting Algorithmを使用したPoint-in-Polygon判定
|
|
17
|
+
- タイル座標計算機
|
|
18
|
+
- Protocol Buffersワイヤーフォーマット用のシンプルなPBFパーサー
|
|
19
|
+
- 日本の行政区域データ対応(都道府県、市区町村、コード)
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Keisuke Terada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# PbfReverseGeocoder
|
|
2
|
+
|
|
3
|
+
日本の行政区域情報を取得するための軽量なリバースジオコーディングライブラリです。Mapbox Vector Tiles (PBF形式) を使用して、緯度経度から都道府県・市区町村を高速に検索します。
|
|
4
|
+
|
|
5
|
+
[@geolonia/open-reverse-geocoder](https://github.com/geolonia/open-reverse-geocoder) のRuby実装版です。
|
|
6
|
+
|
|
7
|
+
## 特徴
|
|
8
|
+
|
|
9
|
+
- **軽量**: 外部APIやデータベース不要
|
|
10
|
+
- **高速**: ローカルのPBFタイルから直接検索
|
|
11
|
+
- **依存性なし**: 標準ライブラリのみで動作
|
|
12
|
+
- **オフライン対応**: インターネット接続不要
|
|
13
|
+
- **純粋Ruby実装**: ネイティブ拡張不要で簡単にインストール可能
|
|
14
|
+
|
|
15
|
+
## インストール
|
|
16
|
+
|
|
17
|
+
Gemfileに追加:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem 'pbf_reverse_geocoder'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
コマンドラインからインストール:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
gem install pbf_reverse_geocoder
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 使い方
|
|
30
|
+
|
|
31
|
+
### クイックスタート
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
require 'pbf_reverse_geocoder'
|
|
35
|
+
|
|
36
|
+
# タイルディレクトリのパスを指定
|
|
37
|
+
tiles_dir = './tiles' # ダウンロードしたタイルの場所
|
|
38
|
+
|
|
39
|
+
# 緯度経度から行政区域情報を取得(東京駅の例)
|
|
40
|
+
result = PbfReverseGeocoder.reverse_geocode(139.7671, 35.6812, tiles_dir)
|
|
41
|
+
|
|
42
|
+
puts result
|
|
43
|
+
# => { "prefecture" => "東京都", "city" => "千代田区", "code" => "13101" }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### より詳しい使用例
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
require 'pbf_reverse_geocoder'
|
|
50
|
+
|
|
51
|
+
# 複数の地点を検索
|
|
52
|
+
locations = [
|
|
53
|
+
{ name: '東京駅', lng: 139.7671, lat: 35.6812 },
|
|
54
|
+
{ name: '大阪城', lng: 135.5258, lat: 34.6873 },
|
|
55
|
+
{ name: '札幌駅', lng: 141.3506, lat: 43.0686 }
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
tiles_dir = './tiles'
|
|
59
|
+
|
|
60
|
+
locations.each do |loc|
|
|
61
|
+
result = PbfReverseGeocoder.reverse_geocode(loc[:lng], loc[:lat], tiles_dir)
|
|
62
|
+
|
|
63
|
+
if result
|
|
64
|
+
puts "#{loc[:name]}: #{result['prefecture']} #{result['city']} (#{result['code']})"
|
|
65
|
+
else
|
|
66
|
+
puts "#{loc[:name]}: 該当する行政区域が見つかりません"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# 出力:
|
|
71
|
+
# 東京駅: 東京都 千代田区 (13101)
|
|
72
|
+
# 大阪城: 大阪府 大阪市中央区 (27128)
|
|
73
|
+
# 札幌駅: 北海道 札幌市北区 (01102)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Railsでの使用例
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
# config/initializers/reverse_geocoder.rb
|
|
80
|
+
TILES_DIR = Rails.root.join('public', 'tiles').to_s
|
|
81
|
+
|
|
82
|
+
# app/controllers/locations_controller.rb
|
|
83
|
+
class LocationsController < ApplicationController
|
|
84
|
+
def reverse_geocode
|
|
85
|
+
lng = params[:lng].to_f
|
|
86
|
+
lat = params[:lat].to_f
|
|
87
|
+
|
|
88
|
+
result = PbfReverseGeocoder.reverse_geocode(lng, lat, TILES_DIR)
|
|
89
|
+
|
|
90
|
+
if result
|
|
91
|
+
render json: result
|
|
92
|
+
else
|
|
93
|
+
render json: { error: 'Not found' }, status: :not_found
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### タイルデータの準備
|
|
100
|
+
|
|
101
|
+
このgemを使用するには、事前にPBF形式のタイルデータを準備する必要があります。
|
|
102
|
+
|
|
103
|
+
以下の2つの方法があります:
|
|
104
|
+
|
|
105
|
+
#### 方法1: Geoloniaのリポジトリから取得
|
|
106
|
+
|
|
107
|
+
[@geolonia/open-reverse-geocoder](https://github.com/geolonia/open-reverse-geocoder)のリポジトリから直接タイルを取得できます。
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# Geoloniaのリポジトリをクローン(タイルのみ)
|
|
111
|
+
git clone --depth 1 --filter=blob:none --sparse \
|
|
112
|
+
https://github.com/geolonia/open-reverse-geocoder.git
|
|
113
|
+
|
|
114
|
+
cd open-reverse-geocoder
|
|
115
|
+
git sparse-checkout set docs/tiles
|
|
116
|
+
|
|
117
|
+
# タイルをホスティング用のディレクトリに配置
|
|
118
|
+
cp -r docs/tiles /path/to/hosting/directory
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
> **💡 配置例**
|
|
122
|
+
> - **Webサーバ**: `public/tiles`(Rails/Nginxなど)
|
|
123
|
+
> - **共有ストレージ**: NFS/S3マウントポイントなど
|
|
124
|
+
> - **アプリケーション内蔵**: `vendor/tiles`
|
|
125
|
+
> - タイルは約560個、合計サイズは数十MB程度です
|
|
126
|
+
>
|
|
127
|
+
> **ホスティングのポイント**:
|
|
128
|
+
> - タイルデータは全サーバで共有可能(読み取り専用)
|
|
129
|
+
> - CDN経由での配信も可能(PBFファイルはバイナリ)
|
|
130
|
+
> - コンテナ環境ではボリュームマウントで共有
|
|
131
|
+
|
|
132
|
+
#### 方法2: ソースデータから自分でビルド(最新データが必要な場合)
|
|
133
|
+
|
|
134
|
+
国土数値情報から最新の行政区域データを使って、自分でタイルをビルドすることもできます。
|
|
135
|
+
|
|
136
|
+
**🔧 必要なツール:**
|
|
137
|
+
- [ogr2ogr](https://gdal.org/programs/ogr2ogr.html) - GDALツール(GeoJSON変換用)
|
|
138
|
+
- [Tippecanoe](https://github.com/felt/tippecanoe) - Mapboxのタイル生成ツール
|
|
139
|
+
- [mb-util](https://github.com/mapbox/mbutil) - MBTiles変換ツール
|
|
140
|
+
|
|
141
|
+
**macOSの場合:**
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# 必要なツールをインストール
|
|
145
|
+
brew install gdal tippecanoe
|
|
146
|
+
pip install mbutil
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**🏗️ ビルド手順:**
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# 1. 元のリポジトリをクローン(ビルドスクリプトを使用)
|
|
153
|
+
git clone https://github.com/geolonia/open-reverse-geocoder.git
|
|
154
|
+
cd open-reverse-geocoder
|
|
155
|
+
|
|
156
|
+
# 2. 依存関係をインストール
|
|
157
|
+
npm install
|
|
158
|
+
|
|
159
|
+
# 3. タイルをビルド(国土数値情報から自動ダウンロード&ビルド)
|
|
160
|
+
npm run build:tiles
|
|
161
|
+
|
|
162
|
+
# 4. ビルドされたタイルをコピー
|
|
163
|
+
cp -r docs/tiles /path/to/your/project/tiles
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**ビルドプロセスの詳細:**
|
|
167
|
+
|
|
168
|
+
1. 国土数値情報から最新の[行政区域データ](https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N03-v2_4.html)をダウンロード
|
|
169
|
+
2. `ogr2ogr`でShapefileをGeoJSONに変換
|
|
170
|
+
3. プロパティ調整スクリプトを実行(prefecture, city, codeフィールドを整形)
|
|
171
|
+
4. `tippecanoe`でMBTilesを生成(非圧縮で出力)
|
|
172
|
+
5. `mb-util`でタイルを分解して静的ファイル化
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
> 詳細は[@geolonia/open-reverse-geocoder](https://github.com/geolonia/open-reverse-geocoder#%E3%82%BF%E3%82%A4%E3%83%AB%E3%81%AE%E3%83%93%E3%83%AB%E3%83%89%E6%96%B9%E6%B3%95)のREADMEを参照してください。
|
|
176
|
+
|
|
177
|
+
#### ディレクトリ構造
|
|
178
|
+
|
|
179
|
+
タイルは以下の構造で配置してください:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
tiles/
|
|
183
|
+
└── 10/ # ズームレベル
|
|
184
|
+
├── 896/
|
|
185
|
+
│ ├── 396.pbf
|
|
186
|
+
│ └── 397.pbf
|
|
187
|
+
├── 904/
|
|
188
|
+
│ ├── 403.pbf # 例: 東京周辺
|
|
189
|
+
│ └── 404.pbf
|
|
190
|
+
└── 926/
|
|
191
|
+
└── 413.pbf
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
#### タイルの範囲について
|
|
195
|
+
|
|
196
|
+
- **ズームレベル10固定**: このライブラリは @geolonia/open-reverse-geocoder と同じくズームレベル10のタイルのみを使用します(約30km四方)
|
|
197
|
+
- **日本全体**: X座標 896-926、Y座標 396-413の範囲で日本全国をカバー
|
|
198
|
+
- **個別地域**: 必要な地域のタイルのみをダウンロードすることも可能
|
|
199
|
+
|
|
200
|
+
### 戻り値
|
|
201
|
+
|
|
202
|
+
成功時:
|
|
203
|
+
```ruby
|
|
204
|
+
{
|
|
205
|
+
"prefecture" => "東京都",
|
|
206
|
+
"city" => "千代田区",
|
|
207
|
+
"code" => "13101" # 全国地方公共団体コード
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
該当する行政区域が見つからない場合は `nil` を返します。
|
|
212
|
+
|
|
213
|
+
## 仕組み
|
|
214
|
+
|
|
215
|
+
1. **タイル座標計算**: 緯度経度からズームレベル10のタイル座標を計算
|
|
216
|
+
2. **PBFパース**: 該当タイルのPBFファイルを読み込んで解析
|
|
217
|
+
3. **ジオメトリデコード**: Mapbox Vector Tileのジオメトリを緯度経度に変換
|
|
218
|
+
4. **Point-in-Polygon判定**: Ray Casting Algorithmで位置を判定
|
|
219
|
+
|
|
220
|
+
## API ドキュメント
|
|
221
|
+
|
|
222
|
+
### `PbfReverseGeocoder.reverse_geocode(lng, lat, tiles_dir)`
|
|
223
|
+
|
|
224
|
+
指定された緯度経度の行政区域情報を取得します。
|
|
225
|
+
|
|
226
|
+
**パラメータ:**
|
|
227
|
+
- `lng` (Float): 経度 (-180 ~ 180)
|
|
228
|
+
- `lat` (Float): 緯度 (-90 ~ 90)
|
|
229
|
+
- `tiles_dir` (String): タイルディレクトリのパス
|
|
230
|
+
|
|
231
|
+
**戻り値:**
|
|
232
|
+
- Hash: 行政区域情報 (`{ "prefecture" => ..., "city" => ..., "code" => ... }`)
|
|
233
|
+
- nil: 該当する行政区域が見つからない場合
|
|
234
|
+
|
|
235
|
+
## 開発
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
# リポジトリをクローン
|
|
239
|
+
git clone https://github.com/yourusername/pbf_reverse_geocoder.git
|
|
240
|
+
cd pbf_reverse_geocoder
|
|
241
|
+
|
|
242
|
+
# 依存関係をインストール
|
|
243
|
+
bundle install
|
|
244
|
+
|
|
245
|
+
# テストを実行
|
|
246
|
+
bundle exec rspec
|
|
247
|
+
|
|
248
|
+
# RuboCopを実行
|
|
249
|
+
bundle exec rubocop
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## ライセンス
|
|
253
|
+
|
|
254
|
+
MIT License
|
|
255
|
+
|
|
256
|
+
## クレジット
|
|
257
|
+
|
|
258
|
+
このライブラリは以下のプロジェクトにインスパイアされています:
|
|
259
|
+
- [@geolonia/open-reverse-geocoder](https://github.com/geolonia/open-reverse-geocoder)
|
|
260
|
+
- [Mapbox Vector Tile Specification](https://github.com/mapbox/vector-tile-spec)
|
|
261
|
+
|
|
262
|
+
## Contributing
|
|
263
|
+
|
|
264
|
+
バグ報告やプルリクエストは大歓迎です。GitHubリポジトリでお待ちしています。
|
data/Rakefile
ADDED
data/example.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/pbf_reverse_geocoder'
|
|
4
|
+
|
|
5
|
+
# 使用例
|
|
6
|
+
tiles_dir = '/path/to/tiles'
|
|
7
|
+
|
|
8
|
+
# 東京駅の座標
|
|
9
|
+
result = PbfReverseGeocoder.reverse_geocode(139.7671, 35.6812, tiles_dir)
|
|
10
|
+
|
|
11
|
+
if result
|
|
12
|
+
puts "都道府県: #{result['prefecture']}"
|
|
13
|
+
puts "市区町村: #{result['city']}"
|
|
14
|
+
puts "地方公共団体コード: #{result['code']}"
|
|
15
|
+
else
|
|
16
|
+
puts '該当する行政区域が見つかりませんでした'
|
|
17
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Mapbox Vector Tile (MVT) のジオメトリをデコードするモジュール
|
|
4
|
+
# Command integers と ZigZag encoding, Delta encoding を処理
|
|
5
|
+
module PbfReverseGeocoder
|
|
6
|
+
|
|
7
|
+
class GeometryDecoder
|
|
8
|
+
|
|
9
|
+
# MVT標準のタイル解像度
|
|
10
|
+
EXTENT = 4096
|
|
11
|
+
|
|
12
|
+
# MVTジオメトリをデコードして緯度経度ポリゴンに変換
|
|
13
|
+
#
|
|
14
|
+
# @param geometry [Array<Integer>] MVTエンコードされたジオメトリ
|
|
15
|
+
# @param tile_x [Integer] タイルX座標
|
|
16
|
+
# @param tile_y [Integer] タイルY座標
|
|
17
|
+
# @param zoom [Integer] ズームレベル
|
|
18
|
+
# @return [Array<Array<Float>>] [[lng, lat], ...] 緯度経度座標の配列
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# GeometryDecoder.decode([9, 50, 34, ...], 904, 403, 10)
|
|
22
|
+
# #=> [[139.7671, 35.6812], ...]
|
|
23
|
+
def self.decode(geometry, tile_x, tile_y, zoom)
|
|
24
|
+
return [] if geometry.nil? || geometry.empty?
|
|
25
|
+
|
|
26
|
+
coordinates = decode_commands(geometry)
|
|
27
|
+
tile_coords_to_lng_lat(coordinates, tile_x, tile_y, zoom)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Command integersをデコードしてタイル内座標に変換
|
|
31
|
+
#
|
|
32
|
+
# @param geometry [Array<Integer>] MVTエンコードされたジオメトリ
|
|
33
|
+
# @return [Array<Array<Integer>>] [[x, y], ...] タイル内座標の配列
|
|
34
|
+
#
|
|
35
|
+
# @private
|
|
36
|
+
def self.decode_commands(geometry)
|
|
37
|
+
coords = []
|
|
38
|
+
x = 0
|
|
39
|
+
y = 0
|
|
40
|
+
i = 0
|
|
41
|
+
|
|
42
|
+
while i < geometry.length
|
|
43
|
+
command_int = geometry[i]
|
|
44
|
+
command = command_int & 0x7 # 下位3ビット:コマンド種別
|
|
45
|
+
count = command_int >> 3 # 上位ビット:繰り返し回数
|
|
46
|
+
|
|
47
|
+
case command
|
|
48
|
+
when 1 # MoveTo:新しいパスを開始
|
|
49
|
+
count.times do
|
|
50
|
+
i += 1
|
|
51
|
+
dx = decode_zigzag(geometry[i])
|
|
52
|
+
i += 1
|
|
53
|
+
dy = decode_zigzag(geometry[i])
|
|
54
|
+
|
|
55
|
+
x += dx
|
|
56
|
+
y += dy
|
|
57
|
+
coords << [x, y]
|
|
58
|
+
end
|
|
59
|
+
when 2 # LineTo:現在位置から線を引く
|
|
60
|
+
count.times do
|
|
61
|
+
i += 1
|
|
62
|
+
dx = decode_zigzag(geometry[i])
|
|
63
|
+
i += 1
|
|
64
|
+
dy = decode_zigzag(geometry[i])
|
|
65
|
+
|
|
66
|
+
x += dx
|
|
67
|
+
y += dy
|
|
68
|
+
coords << [x, y]
|
|
69
|
+
end
|
|
70
|
+
when 7 # ClosePath:ポリゴンを閉じる
|
|
71
|
+
# 座標追加は不要(最初の点に戻る)
|
|
72
|
+
else
|
|
73
|
+
# 不明なコマンド:スキップ
|
|
74
|
+
warn "Unknown MVT command: #{command}" if $DEBUG
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
i += 1
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
coords
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# ZigZag デコーディング
|
|
84
|
+
# 負数をサポートするための変換を戻す
|
|
85
|
+
#
|
|
86
|
+
# @param n [Integer] エンコードされた値
|
|
87
|
+
# @return [Integer] デコードされた値
|
|
88
|
+
#
|
|
89
|
+
# @private
|
|
90
|
+
def self.decode_zigzag(n)
|
|
91
|
+
(n >> 1) ^ -(n & 1)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# タイル内座標を緯度経度に変換
|
|
95
|
+
#
|
|
96
|
+
# @param coords [Array<Array<Integer>>] [[x, y], ...] タイル内座標
|
|
97
|
+
# @param tile_x [Integer] タイルX座標
|
|
98
|
+
# @param tile_y [Integer] タイルY座標
|
|
99
|
+
# @param zoom [Integer] ズームレベル
|
|
100
|
+
# @return [Array<Array<Float>>] [[lng, lat], ...] 緯度経度座標
|
|
101
|
+
#
|
|
102
|
+
# @private
|
|
103
|
+
def self.tile_coords_to_lng_lat(coords, tile_x, tile_y, zoom)
|
|
104
|
+
n = 2**zoom
|
|
105
|
+
|
|
106
|
+
coords.map do |x, y|
|
|
107
|
+
# タイル内相対座標 (0-4096) → 絶対座標 (0-1)
|
|
108
|
+
rel_x = x.to_f / EXTENT
|
|
109
|
+
rel_y = y.to_f / EXTENT
|
|
110
|
+
|
|
111
|
+
# Google XYZ → 緯度経度(Web Mercator逆投影)
|
|
112
|
+
pixel_x = (tile_x + rel_x) / n
|
|
113
|
+
pixel_y = (tile_y + rel_y) / n
|
|
114
|
+
|
|
115
|
+
lng = (pixel_x * 360.0) - 180.0
|
|
116
|
+
|
|
117
|
+
lat_rad = Math.atan(Math.sinh(Math::PI * (1 - (2 * pixel_y))))
|
|
118
|
+
lat = lat_rad * 180.0 / Math::PI
|
|
119
|
+
|
|
120
|
+
[lng, lat]
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private_class_method :decode_commands, :decode_zigzag, :tile_coords_to_lng_lat
|
|
125
|
+
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'simple_pbf_parser'
|
|
4
|
+
require_relative 'geometry_decoder'
|
|
5
|
+
|
|
6
|
+
# PBFタイルを読み込んでフィーチャー一覧を返すモジュール
|
|
7
|
+
module PbfReverseGeocoder
|
|
8
|
+
|
|
9
|
+
class PbfTileReader
|
|
10
|
+
|
|
11
|
+
# @geoloniaと同じレイヤー名
|
|
12
|
+
LAYER_NAME = 'japanese-admins'
|
|
13
|
+
|
|
14
|
+
# PBFタイルを読み込んでフィーチャー一覧を返す
|
|
15
|
+
#
|
|
16
|
+
# @param tile_path [String, Pathname] PBFファイルパス
|
|
17
|
+
# @param tile_x [Integer] タイルX座標
|
|
18
|
+
# @param tile_y [Integer] タイルY座標
|
|
19
|
+
# @param zoom [Integer] ズームレベル
|
|
20
|
+
# @return [Array<Hash>] フィーチャー配列
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# features = PbfTileReader.read_tile('/app/public/tiles/10/904/403.pbf', 904, 403, 10)
|
|
24
|
+
def self.read_tile(tile_path, tile_x, tile_y, zoom)
|
|
25
|
+
return [] unless File.exist?(tile_path)
|
|
26
|
+
|
|
27
|
+
# バイナリ読み込み
|
|
28
|
+
pbf_data = File.binread(tile_path)
|
|
29
|
+
|
|
30
|
+
# SimplePbfParserでパース
|
|
31
|
+
tile = SimplePbfParser.parse(pbf_data)
|
|
32
|
+
|
|
33
|
+
# japanese-admins レイヤーを抽出
|
|
34
|
+
layer = tile[:layers].find { |l| l[:name] == LAYER_NAME }
|
|
35
|
+
return [] unless layer
|
|
36
|
+
|
|
37
|
+
# フィーチャーをGeoJSON形式に変換
|
|
38
|
+
layer[:features].map do |feature|
|
|
39
|
+
geometry = GeometryDecoder.decode(feature[:geometry], tile_x, tile_y, zoom)
|
|
40
|
+
properties = decode_properties(feature, layer)
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
geometry: geometry,
|
|
44
|
+
properties: properties
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
warn "Failed to read PBF tile: #{tile_path}, error: #{e.message}"
|
|
49
|
+
warn e.backtrace.join("\n") if $DEBUG
|
|
50
|
+
[]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# フィーチャーのプロパティをデコード
|
|
54
|
+
# tagsは [key_index, value_index, key_index, value_index, ...] の形式
|
|
55
|
+
#
|
|
56
|
+
# @param feature [Hash] フィーチャーデータ
|
|
57
|
+
# @param layer [Hash] レイヤーデータ
|
|
58
|
+
# @return [Hash] プロパティハッシュ
|
|
59
|
+
#
|
|
60
|
+
# @private
|
|
61
|
+
def self.decode_properties(feature, layer)
|
|
62
|
+
props = {}
|
|
63
|
+
|
|
64
|
+
# IDがある場合はcodeとして扱う
|
|
65
|
+
if feature[:id]
|
|
66
|
+
code = feature[:id].to_s
|
|
67
|
+
# 4桁の場合は先頭にゼロを追加(例: 1101 → 01101)
|
|
68
|
+
code = code.rjust(5, '0') if code.length == 4
|
|
69
|
+
props['code'] = code
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# tagsをデコード(2要素ずつペアになっている)
|
|
73
|
+
feature[:tags].each_slice(2) do |key_idx, val_idx|
|
|
74
|
+
key = layer[:keys][key_idx]
|
|
75
|
+
value_obj = layer[:values][val_idx]
|
|
76
|
+
|
|
77
|
+
# 値を文字列として取得
|
|
78
|
+
value = value_obj&.dig(:string_value) || ''
|
|
79
|
+
|
|
80
|
+
props[key] = value
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
props
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private_class_method :decode_properties
|
|
87
|
+
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# 点がポリゴン内にあるかを判定するモジュール
|
|
4
|
+
# Ray Casting Algorithm を実装
|
|
5
|
+
# d3-geo の geoContains に相当
|
|
6
|
+
module PbfReverseGeocoder
|
|
7
|
+
class PointInPolygon
|
|
8
|
+
|
|
9
|
+
# 点がポリゴン内にあるか判定
|
|
10
|
+
# 点からX軸正方向に伸ばした半直線が、ポリゴンの辺と何回交差するかを数える
|
|
11
|
+
# 奇数回 = 内側、偶数回 = 外側
|
|
12
|
+
#
|
|
13
|
+
# @param point [Array<Float>] [lng, lat] 判定する点の座標
|
|
14
|
+
# @param polygon [Array<Array<Float>>] [[lng, lat], ...] ポリゴンの頂点座標配列
|
|
15
|
+
# @return [Boolean] true: 内側, false: 外側
|
|
16
|
+
def self.contains?(point, polygon)
|
|
17
|
+
return false if polygon.nil? || polygon.empty?
|
|
18
|
+
|
|
19
|
+
px, py = point
|
|
20
|
+
inside = false
|
|
21
|
+
|
|
22
|
+
# ポリゴンの各辺について交差判定
|
|
23
|
+
j = polygon.length - 1
|
|
24
|
+
polygon.length.times do |i|
|
|
25
|
+
xi, yi = polygon[i]
|
|
26
|
+
xj, yj = polygon[j]
|
|
27
|
+
|
|
28
|
+
# Y座標の範囲チェック:辺が点のY座標をまたいでいるか
|
|
29
|
+
if (yi > py) != (yj > py)
|
|
30
|
+
# X座標の交差判定:半直線が辺と交差する点のX座標を計算
|
|
31
|
+
x_intersect = ((xj - xi) * (py - yi) / (yj - yi)) + xi
|
|
32
|
+
|
|
33
|
+
# 点のX座標より右側で交差していたら、inside を反転
|
|
34
|
+
inside = !inside if px < x_intersect
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
j = i
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
inside
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Mapbox Vector Tile (MVT) に特化した簡易PBFパーサー
|
|
4
|
+
# Protocol Buffersの基本的なワイヤフォーマットをパース
|
|
5
|
+
module PbfReverseGeocoder
|
|
6
|
+
|
|
7
|
+
class SimplePbfParser
|
|
8
|
+
|
|
9
|
+
# Protocol Buffersのワイヤタイプ
|
|
10
|
+
WIRE_TYPE_VARINT = 0
|
|
11
|
+
WIRE_TYPE_64BIT = 1
|
|
12
|
+
WIRE_TYPE_LENGTH_DELIMITED = 2
|
|
13
|
+
WIRE_TYPE_32BIT = 5
|
|
14
|
+
|
|
15
|
+
# PBFバイナリをパースしてMVTタイルデータを返す
|
|
16
|
+
#
|
|
17
|
+
# @param data [String] バイナリデータ
|
|
18
|
+
# @return [Hash] パースされたタイルデータ
|
|
19
|
+
def self.parse(data)
|
|
20
|
+
buffer = data.bytes
|
|
21
|
+
pos = 0
|
|
22
|
+
tile = { layers: [] }
|
|
23
|
+
|
|
24
|
+
while pos < buffer.length
|
|
25
|
+
field_key, pos = read_varint(buffer, pos)
|
|
26
|
+
field_number = field_key >> 3
|
|
27
|
+
wire_type = field_key & 0x7
|
|
28
|
+
|
|
29
|
+
case wire_type
|
|
30
|
+
when WIRE_TYPE_LENGTH_DELIMITED
|
|
31
|
+
length, pos = read_varint(buffer, pos)
|
|
32
|
+
value_bytes = buffer[pos, length]
|
|
33
|
+
pos += length
|
|
34
|
+
|
|
35
|
+
# Field 3: layers
|
|
36
|
+
if field_number == 3
|
|
37
|
+
layer = parse_layer(value_bytes)
|
|
38
|
+
tile[:layers] << layer if layer
|
|
39
|
+
end
|
|
40
|
+
else
|
|
41
|
+
# その他のフィールドはスキップ
|
|
42
|
+
pos = skip_field(buffer, pos, wire_type)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
tile
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# レイヤーをパース
|
|
50
|
+
#
|
|
51
|
+
# @param data [Array<Integer>] バイナリデータ
|
|
52
|
+
# @return [Hash] パースされたレイヤーデータ
|
|
53
|
+
# @private
|
|
54
|
+
def self.parse_layer(data)
|
|
55
|
+
pos = 0
|
|
56
|
+
layer = { name: '', features: [], keys: [], values: [] }
|
|
57
|
+
|
|
58
|
+
while pos < data.length
|
|
59
|
+
field_key, pos = read_varint(data, pos)
|
|
60
|
+
field_number = field_key >> 3
|
|
61
|
+
wire_type = field_key & 0x7
|
|
62
|
+
|
|
63
|
+
case wire_type
|
|
64
|
+
when WIRE_TYPE_LENGTH_DELIMITED
|
|
65
|
+
length, pos = read_varint(data, pos)
|
|
66
|
+
value_bytes = data[pos, length]
|
|
67
|
+
pos += length
|
|
68
|
+
|
|
69
|
+
case field_number
|
|
70
|
+
when 1 # name
|
|
71
|
+
layer[:name] = value_bytes.pack('C*').force_encoding('UTF-8')
|
|
72
|
+
when 2 # features
|
|
73
|
+
feature = parse_feature(value_bytes)
|
|
74
|
+
layer[:features] << feature if feature
|
|
75
|
+
when 3 # keys
|
|
76
|
+
layer[:keys] << value_bytes.pack('C*').force_encoding('UTF-8')
|
|
77
|
+
when 4 # values
|
|
78
|
+
value = parse_value(value_bytes)
|
|
79
|
+
layer[:values] << value if value
|
|
80
|
+
end
|
|
81
|
+
else
|
|
82
|
+
pos = skip_field(data, pos, wire_type)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
layer
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# フィーチャーをパース
|
|
90
|
+
#
|
|
91
|
+
# @param data [Array<Integer>] バイナリデータ
|
|
92
|
+
# @return [Hash] パースされたフィーチャーデータ
|
|
93
|
+
# @private
|
|
94
|
+
def self.parse_feature(data)
|
|
95
|
+
pos = 0
|
|
96
|
+
feature = { id: nil, tags: [], type: 0, geometry: [] }
|
|
97
|
+
|
|
98
|
+
while pos < data.length
|
|
99
|
+
field_key, pos = read_varint(data, pos)
|
|
100
|
+
field_number = field_key >> 3
|
|
101
|
+
wire_type = field_key & 0x7
|
|
102
|
+
|
|
103
|
+
case wire_type
|
|
104
|
+
when WIRE_TYPE_VARINT
|
|
105
|
+
value, pos = read_varint(data, pos)
|
|
106
|
+
case field_number
|
|
107
|
+
when 1 # id
|
|
108
|
+
feature[:id] = value
|
|
109
|
+
when 3 # type
|
|
110
|
+
feature[:type] = value
|
|
111
|
+
end
|
|
112
|
+
when WIRE_TYPE_LENGTH_DELIMITED
|
|
113
|
+
length, pos = read_varint(data, pos)
|
|
114
|
+
value_bytes = data[pos, length]
|
|
115
|
+
pos += length
|
|
116
|
+
|
|
117
|
+
case field_number
|
|
118
|
+
when 2 # tags (packed)
|
|
119
|
+
feature[:tags] = unpack_packed_varint(value_bytes)
|
|
120
|
+
when 4 # geometry (packed)
|
|
121
|
+
feature[:geometry] = unpack_packed_varint(value_bytes)
|
|
122
|
+
end
|
|
123
|
+
else
|
|
124
|
+
pos = skip_field(data, pos, wire_type)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
feature
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# 値をパース
|
|
132
|
+
#
|
|
133
|
+
# @param data [Array<Integer>] バイナリデータ
|
|
134
|
+
# @return [Hash] パースされた値
|
|
135
|
+
# @private
|
|
136
|
+
def self.parse_value(data)
|
|
137
|
+
pos = 0
|
|
138
|
+
value = {}
|
|
139
|
+
|
|
140
|
+
while pos < data.length
|
|
141
|
+
field_key, pos = read_varint(data, pos)
|
|
142
|
+
field_number = field_key >> 3
|
|
143
|
+
wire_type = field_key & 0x7
|
|
144
|
+
|
|
145
|
+
case wire_type
|
|
146
|
+
when WIRE_TYPE_LENGTH_DELIMITED
|
|
147
|
+
length, pos = read_varint(data, pos)
|
|
148
|
+
value_bytes = data[pos, length]
|
|
149
|
+
pos += length
|
|
150
|
+
|
|
151
|
+
value[:string_value] = value_bytes.pack('C*').force_encoding('UTF-8') if field_number == 1 # string_value
|
|
152
|
+
else
|
|
153
|
+
pos = skip_field(data, pos, wire_type)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
value
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Varint (可変長整数) を読み取る
|
|
161
|
+
#
|
|
162
|
+
# @param buffer [Array<Integer>] バイトバッファ
|
|
163
|
+
# @param pos [Integer] 現在位置
|
|
164
|
+
# @return [Array<Integer, Integer>] [値, 新しい位置]
|
|
165
|
+
# @private
|
|
166
|
+
def self.read_varint(buffer, pos)
|
|
167
|
+
value = 0
|
|
168
|
+
shift = 0
|
|
169
|
+
|
|
170
|
+
loop do
|
|
171
|
+
byte = buffer[pos]
|
|
172
|
+
pos += 1
|
|
173
|
+
|
|
174
|
+
value |= (byte & 0x7F) << shift
|
|
175
|
+
shift += 7
|
|
176
|
+
|
|
177
|
+
break unless byte.anybits?(0x80)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
[value, pos]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Packed varint配列を展開
|
|
184
|
+
#
|
|
185
|
+
# @param data [Array<Integer>] バイトデータ
|
|
186
|
+
# @return [Array<Integer>] 展開された整数配列
|
|
187
|
+
# @private
|
|
188
|
+
def self.unpack_packed_varint(data)
|
|
189
|
+
pos = 0
|
|
190
|
+
result = []
|
|
191
|
+
|
|
192
|
+
while pos < data.length
|
|
193
|
+
value, pos = read_varint(data, pos)
|
|
194
|
+
result << value
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
result
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# フィールドをスキップ
|
|
201
|
+
#
|
|
202
|
+
# @param buffer [Array<Integer>] バイトバッファ
|
|
203
|
+
# @param pos [Integer] 現在位置
|
|
204
|
+
# @param wire_type [Integer] ワイヤタイプ
|
|
205
|
+
# @return [Integer] 新しい位置
|
|
206
|
+
# @private
|
|
207
|
+
def self.skip_field(buffer, pos, wire_type)
|
|
208
|
+
case wire_type
|
|
209
|
+
when WIRE_TYPE_VARINT
|
|
210
|
+
_value, pos = read_varint(buffer, pos)
|
|
211
|
+
when WIRE_TYPE_64BIT
|
|
212
|
+
pos += 8
|
|
213
|
+
when WIRE_TYPE_LENGTH_DELIMITED
|
|
214
|
+
length, pos = read_varint(buffer, pos)
|
|
215
|
+
pos += length
|
|
216
|
+
when WIRE_TYPE_32BIT
|
|
217
|
+
pos += 4
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
pos
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
private_class_method :parse_layer, :parse_feature, :parse_value,
|
|
224
|
+
:read_varint, :unpack_packed_varint, :skip_field
|
|
225
|
+
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# 緯度経度からタイル座標を計算するモジュール
|
|
4
|
+
# @geolonia/open-reverse-geocoder の lngLatToGoogle ロジックを実装
|
|
5
|
+
module PbfReverseGeocoder
|
|
6
|
+
|
|
7
|
+
class TileCalculator
|
|
8
|
+
|
|
9
|
+
# ズームレベル10固定(@geoloniaと同じ、約30km四方)
|
|
10
|
+
ZOOM = 10
|
|
11
|
+
|
|
12
|
+
# Google XYZ タイル座標を計算
|
|
13
|
+
# Web Mercator投影を使用
|
|
14
|
+
#
|
|
15
|
+
# @param lng [Float] 経度 (-180 ~ 180)
|
|
16
|
+
# @param lat [Float] 緯度 (-90 ~ 90)
|
|
17
|
+
# @return [Array<Integer>] [x, y, zoom]
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# TileCalculator.lng_lat_to_tile(139.7671, 35.6812)
|
|
21
|
+
# #=> [904, 403, 10]
|
|
22
|
+
def self.lng_lat_to_tile(lng, lat)
|
|
23
|
+
# global-mercator の pointToTileFraction ロジック
|
|
24
|
+
n = 2.0**ZOOM
|
|
25
|
+
lat_rad = lat * Math::PI / 180.0
|
|
26
|
+
sin_lat = Math.sin(lat_rad)
|
|
27
|
+
|
|
28
|
+
# X座標(経度ベース)
|
|
29
|
+
x = ((lng + 180.0) / 360.0 * n).floor
|
|
30
|
+
|
|
31
|
+
# Y座標(緯度ベース、メルカトル投影)
|
|
32
|
+
y = ((0.5 - (0.25 * Math.log((1 + sin_lat) / (1 - sin_lat)) / Math::PI)) * n).floor
|
|
33
|
+
|
|
34
|
+
[x, y, ZOOM]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# タイルのファイルパスを生成
|
|
38
|
+
#
|
|
39
|
+
# @param x [Integer] タイルX座標
|
|
40
|
+
# @param y [Integer] タイルY座標
|
|
41
|
+
# @param zoom [Integer] ズームレベル
|
|
42
|
+
# @param tiles_dir [String, Pathname] タイルディレクトリのベースパス
|
|
43
|
+
# @return [Pathname] PBFファイルのパス
|
|
44
|
+
#
|
|
45
|
+
# @example
|
|
46
|
+
# TileCalculator.tile_path(904, 403, 10, '/app/public/tiles')
|
|
47
|
+
# #=> #<Pathname:/app/public/tiles/10/904/403.pbf>
|
|
48
|
+
def self.tile_path(x, y, zoom, tiles_dir)
|
|
49
|
+
require 'pathname'
|
|
50
|
+
Pathname.new(tiles_dir).join(zoom.to_s, x.to_s, "#{y}.pbf")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'pbf_reverse_geocoder/version'
|
|
4
|
+
require_relative 'pbf_reverse_geocoder/simple_pbf_parser'
|
|
5
|
+
require_relative 'pbf_reverse_geocoder/geometry_decoder'
|
|
6
|
+
require_relative 'pbf_reverse_geocoder/point_in_polygon'
|
|
7
|
+
require_relative 'pbf_reverse_geocoder/tile_calculator'
|
|
8
|
+
require_relative 'pbf_reverse_geocoder/pbf_tile_reader'
|
|
9
|
+
|
|
10
|
+
# 日本の行政区域のためのPBFベースのリバースジオコーディング
|
|
11
|
+
module PbfReverseGeocoder
|
|
12
|
+
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
|
|
15
|
+
# リバースジオコーディングのメインエントリーポイント
|
|
16
|
+
#
|
|
17
|
+
# @param lng [Float] 経度
|
|
18
|
+
# @param lat [Float] 緯度
|
|
19
|
+
# @param tiles_dir [String, Pathname] タイルディレクトリへのパス
|
|
20
|
+
# @return [Hash, nil] 行政区域情報、見つからない場合はnil
|
|
21
|
+
# { prefecture: '東京都', city: '千代田区', code: '13101' }
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# result = PbfReverseGeocoder.reverse_geocode(139.7671, 35.6812, '/path/to/tiles')
|
|
25
|
+
# #=> { 'prefecture' => '東京都', 'city' => '千代田区', 'code' => '13101' }
|
|
26
|
+
def self.reverse_geocode(lng, lat, tiles_dir)
|
|
27
|
+
# タイル座標を計算
|
|
28
|
+
tile_x, tile_y, zoom = TileCalculator.lng_lat_to_tile(lng, lat)
|
|
29
|
+
tile_path = TileCalculator.tile_path(tile_x, tile_y, zoom, tiles_dir)
|
|
30
|
+
|
|
31
|
+
# PBFタイルを読み込んでパース
|
|
32
|
+
features = PbfTileReader.read_tile(tile_path, tile_x, tile_y, zoom)
|
|
33
|
+
|
|
34
|
+
# 点を含むポリゴンを検索
|
|
35
|
+
point = [lng, lat]
|
|
36
|
+
features.each do |feature|
|
|
37
|
+
if PointInPolygon.contains?(point, feature[:geometry])
|
|
38
|
+
return feature[:properties]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/pbf_reverse_geocoder/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'pbf_reverse_geocoder'
|
|
7
|
+
spec.version = PbfReverseGeocoder::VERSION
|
|
8
|
+
spec.authors = ['Keisuke Terada']
|
|
9
|
+
spec.email = ['rorensu2236@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'PBF-based reverse geocoding library for Japanese administrative areas'
|
|
12
|
+
spec.description = 'A lightweight reverse geocoding library that uses Mapbox Vector Tiles (PBF format) to find Japanese administrative areas (prefecture, city) from latitude/longitude coordinates. Ruby implementation of @geolonia/open-reverse-geocoder.'
|
|
13
|
+
spec.homepage = 'https://github.com/keisuke2236/pbf_reverse_geocoder'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 3.0.0'
|
|
16
|
+
|
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/keisuke2236/pbf_reverse_geocoder'
|
|
19
|
+
spec.metadata['changelog_uri'] = 'https://github.com/keisuke2236/pbf_reverse_geocoder/blob/main/CHANGELOG.md'
|
|
20
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
21
|
+
|
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
25
|
+
(File.expand_path(f) == __FILE__) ||
|
|
26
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
spec.bindir = 'exe'
|
|
30
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
31
|
+
spec.require_paths = ['lib']
|
|
32
|
+
|
|
33
|
+
# Runtime dependencies
|
|
34
|
+
# (現時点では標準ライブラリのみ使用)
|
|
35
|
+
|
|
36
|
+
# Development dependencies
|
|
37
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
|
38
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
39
|
+
spec.add_development_dependency 'rubocop', '~> 1.21'
|
|
40
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pbf_reverse_geocoder
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Keisuke Terada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rake
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '13.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '13.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rspec
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rubocop
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.21'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.21'
|
|
54
|
+
description: A lightweight reverse geocoding library that uses Mapbox Vector Tiles
|
|
55
|
+
(PBF format) to find Japanese administrative areas (prefecture, city) from latitude/longitude
|
|
56
|
+
coordinates. Ruby implementation of @geolonia/open-reverse-geocoder.
|
|
57
|
+
email:
|
|
58
|
+
- rorensu2236@gmail.com
|
|
59
|
+
executables: []
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- ".rubocop.yml"
|
|
64
|
+
- ".ruby-version"
|
|
65
|
+
- CHANGELOG.md
|
|
66
|
+
- LICENSE.txt
|
|
67
|
+
- README.md
|
|
68
|
+
- Rakefile
|
|
69
|
+
- example.rb
|
|
70
|
+
- lib/pbf_reverse_geocoder.rb
|
|
71
|
+
- lib/pbf_reverse_geocoder/geometry_decoder.rb
|
|
72
|
+
- lib/pbf_reverse_geocoder/pbf_tile_reader.rb
|
|
73
|
+
- lib/pbf_reverse_geocoder/point_in_polygon.rb
|
|
74
|
+
- lib/pbf_reverse_geocoder/simple_pbf_parser.rb
|
|
75
|
+
- lib/pbf_reverse_geocoder/tile_calculator.rb
|
|
76
|
+
- lib/pbf_reverse_geocoder/version.rb
|
|
77
|
+
- pbf_reverse_geocoder.gemspec
|
|
78
|
+
homepage: https://github.com/keisuke2236/pbf_reverse_geocoder
|
|
79
|
+
licenses:
|
|
80
|
+
- MIT
|
|
81
|
+
metadata:
|
|
82
|
+
homepage_uri: https://github.com/keisuke2236/pbf_reverse_geocoder
|
|
83
|
+
source_code_uri: https://github.com/keisuke2236/pbf_reverse_geocoder
|
|
84
|
+
changelog_uri: https://github.com/keisuke2236/pbf_reverse_geocoder/blob/main/CHANGELOG.md
|
|
85
|
+
rubygems_mfa_required: 'true'
|
|
86
|
+
rdoc_options: []
|
|
87
|
+
require_paths:
|
|
88
|
+
- lib
|
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: 3.0.0
|
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - ">="
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: '0'
|
|
99
|
+
requirements: []
|
|
100
|
+
rubygems_version: 3.6.9
|
|
101
|
+
specification_version: 4
|
|
102
|
+
summary: PBF-based reverse geocoding library for Japanese administrative areas
|
|
103
|
+
test_files: []
|