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 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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PbfReverseGeocoder
4
+ VERSION = '0.1.0'
5
+ 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: []