File size: 6,943 Bytes
57bc6ef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
"""OpenStreetMap Overpass API クライアント"""

import time
from typing import List, Optional, Dict, Any
from dataclasses import dataclass
from cachetools import TTLCache

from config.settings import settings


@dataclass
class POI:
    """Point of Interest"""

    osm_id: int
    name: str
    lat: float
    lon: float
    poi_type: str
    tags: Dict[str, str]


class OverpassClient:
    """
    OpenStreetMap Overpass API クライアント
    POI検索とキャッシュ機能を提供
    """

    OVERPASS_URL = "https://overpass-api.de/api/interpreter"

    # 店舗タイプマッピング
    SHOP_TYPE_MAPPING = {
        "convenience_store": ["shop=convenience", "amenity=convenience"],
        "restaurant": ["amenity=restaurant", "amenity=fast_food"],
        "hospital": ["amenity=hospital", "amenity=clinic"],
        "pharmacy": ["amenity=pharmacy", "shop=chemist"],
        "gas_station": ["amenity=fuel"],
        "parking": ["amenity=parking"],
        "station": ["railway=station", "public_transport=station"],
        "bank": ["amenity=bank"],
        "post_office": ["amenity=post_office"],
        "supermarket": ["shop=supermarket"],
    }

    def __init__(self, timeout: int = 25, cache_ttl: int = 300):
        self.timeout = timeout
        self._cache = TTLCache(maxsize=100, ttl=cache_ttl)
        self._last_request_time: float = 0
        self._min_interval: float = 1.0  # 最低1秒間隔

    def _wait_for_rate_limit(self) -> None:
        """レート制限のための待機"""
        elapsed = time.time() - self._last_request_time
        if elapsed < self._min_interval:
            time.sleep(self._min_interval - elapsed)

    def _build_name_query(
        self,
        name: str,
        lat: float,
        lon: float,
        radius: int,
    ) -> str:
        """名前でPOIを検索するクエリを構築"""
        return f"""
[out:json][timeout:{self.timeout}];
(
  node["name"~"{name}",i](around:{radius},{lat},{lon});
  way["name"~"{name}",i](around:{radius},{lat},{lon});
);
out center;
"""

    def _build_type_query(
        self,
        poi_type: str,
        lat: float,
        lon: float,
        radius: int,
    ) -> str:
        """タイプでPOIを検索するクエリを構築"""
        tags = self.SHOP_TYPE_MAPPING.get(poi_type, [])
        if not tags:
            return ""

        conditions = []
        for tag in tags:
            key, value = tag.split("=")
            conditions.append(f'node["{key}"="{value}"](around:{radius},{lat},{lon});')
            conditions.append(f'way["{key}"="{value}"](around:{radius},{lat},{lon});')

        return f"""
[out:json][timeout:{self.timeout}];
(
  {chr(10).join(conditions)}
);
out center;
"""

    def _parse_response(self, data: Dict[str, Any]) -> List[POI]:
        """Overpass APIレスポンスをパース"""
        pois = []
        elements = data.get("elements", [])

        for elem in elements:
            tags = elem.get("tags", {})
            name = tags.get("name", "")

            # 座標の取得(wayの場合はcenter)
            if elem.get("type") == "way":
                center = elem.get("center", {})
                lat = center.get("lat", 0)
                lon = center.get("lon", 0)
            else:
                lat = elem.get("lat", 0)
                lon = elem.get("lon", 0)

            # POIタイプの判定
            poi_type = "unknown"
            if tags.get("shop"):
                poi_type = tags.get("shop")
            elif tags.get("amenity"):
                poi_type = tags.get("amenity")
            elif tags.get("railway"):
                poi_type = "station"

            if lat and lon:
                pois.append(
                    POI(
                        osm_id=elem.get("id", 0),
                        name=name,
                        lat=lat,
                        lon=lon,
                        poi_type=poi_type,
                        tags=tags,
                    )
                )

        return pois

    def search_by_name(
        self,
        name: str,
        lat: float,
        lon: float,
        radius: int = 500,
    ) -> List[POI]:
        """
        名前でPOIを検索

        Args:
            name: 検索名
            lat: 緯度
            lon: 経度
            radius: 検索半径(メートル)

        Returns:
            POIのリスト
        """
        cache_key = f"name:{name}:{lat:.4f}:{lon:.4f}:{radius}"
        if cache_key in self._cache:
            return self._cache[cache_key]

        query = self._build_name_query(name, lat, lon, radius)
        result = self._execute_query(query)
        self._cache[cache_key] = result
        return result

    def search_by_type(
        self,
        poi_type: str,
        lat: float,
        lon: float,
        radius: int = 500,
    ) -> List[POI]:
        """
        タイプでPOIを検索

        Args:
            poi_type: POIタイプ
            lat: 緯度
            lon: 経度
            radius: 検索半径(メートル)

        Returns:
            POIのリスト
        """
        cache_key = f"type:{poi_type}:{lat:.4f}:{lon:.4f}:{radius}"
        if cache_key in self._cache:
            return self._cache[cache_key]

        query = self._build_type_query(poi_type, lat, lon, radius)
        if not query:
            return []

        result = self._execute_query(query)
        self._cache[cache_key] = result
        return result

    def _execute_query(self, query: str) -> List[POI]:
        """Overpass APIクエリを実行"""
        try:
            import requests

            self._wait_for_rate_limit()
            self._last_request_time = time.time()

            response = requests.post(
                self.OVERPASS_URL,
                data={"data": query},
                timeout=self.timeout,
            )
            response.raise_for_status()

            return self._parse_response(response.json())

        except Exception as e:
            print(f"Overpass query error: {e}")
            return []

    def search_combined(
        self,
        names: List[str],
        types: List[str],
        lat: float,
        lon: float,
        radius: int = 500,
    ) -> Dict[str, List[POI]]:
        """
        複合検索(名前とタイプ両方)

        Returns:
            {"names": {...}, "types": {...}} の形式
        """
        result = {"names": {}, "types": {}}

        for name in names:
            pois = self.search_by_name(name, lat, lon, radius)
            if pois:
                result["names"][name] = pois

        for poi_type in types:
            pois = self.search_by_type(poi_type, lat, lon, radius)
            if pois:
                result["types"][poi_type] = pois

        return result

    def clear_cache(self) -> None:
        """キャッシュをクリア"""
        self._cache.clear()