paint-brush
비 GIS 데이터베이스에서 GIS 계산을 수행하는 방법~에 의해@joellopes
1,662 판독값
1,662 판독값

비 GIS 데이터베이스에서 GIS 계산을 수행하는 방법

~에 의해 Joel Lopes9m2024/06/15
Read on Terminal Reader

너무 오래; 읽다

백엔드 스토리지가 기본적으로 공간 쿼리를 지원하지 않는 경우 이 문서가 맞춤화되었습니다. 공간 데이터를 처리하기 위해 언제든지 다른 마이크로서비스를 구축할 수 있지만 이 옵션에는 추가 애플리케이션을 유지 관리하는 오버헤드가 포함되는 경우가 많습니다. 또 다른 접근 방식은 S2 및 H3와 같은 지리 색인 라이브러리를 사용하는 것입니다. S2는 구체를 각각 고유한 64비트 식별자가 있는 셀로 나눕니다. 레벨이 높을수록 해상도가 더 좋고 셀 영역이 더 작아집니다.
featured image - 비 GIS 데이터베이스에서 GIS 계산을 수행하는 방법
Joel Lopes HackerNoon profile picture

소개:

때때로 애플리케이션 내에서 사용자 위치 매핑이나 지리 데이터 분석과 같은 지리공간 기능을 수행해야 하는 경우가 있을 수 있습니다. GDAL, Shapely, Python용 Geopandas 등 이러한 작업에 사용할 수 있는 다양한 언어별 라이브러리가 있습니다.


또는 데이터베이스를 통해 지리공간 기능을 구현할 수 있습니다. 예를 들어 PostgreSQL과 같은 관계형 데이터베이스와 함께 PostGIS 확장을 사용하거나 Azure CosmosDB와 같은 분산 데이터베이스에서 공간 데이터 유형에 대한 기본 지원을 활용할 수 있습니다.


그러나 Redis 또는 Google Spanner와 같은 백엔드 스토리지가 기본적으로 공간 쿼리를 지원하지 않고 대규모 지리공간 쿼리를 처리해야 하는 경우 이 문서는 귀하에게 적합합니다.

내 옵션은 무엇입니까?

공간 데이터를 처리하기 위해 언제든지 다른 마이크로서비스를 구축할 수 있지만 이 옵션에는 추가 애플리케이션을 유지 관리하는 오버헤드가 포함되는 경우가 많습니다. 또 다른 접근 방식은 S2 및 H3와 같은 지리 색인 라이브러리를 사용하는 것입니다. Google이 개발한 S2는 힐베르트 곡선을 기반으로 하며, Uber가 개발한 H3는 측지선 이산 글로벌 그리드 시스템을 기반으로 합니다. S2와 H3는 많은 유사점을 공유합니다. 둘 다 주어진 영역을 셀로 나누고 64비트 정수를 사용하여 이러한 셀을 인덱싱합니다.


그러나 주요 차이점은 세포의 모양에 있습니다. S2는 정사각형 모양의 셀을 사용하는 반면 H3은 육각형 모양의 셀을 사용합니다. 일부 애플리케이션의 경우 H3가 더 나은 성능을 제공할 수 있습니다. 그러나 전반적으로 두 라이브러리 모두 충분합니다. 이 글에서는 S2를 사용하겠지만 H3을 사용해도 유사한 기능을 수행할 수 있습니다.

Google S2 라이브러리의 기본 개념

  • 셀: S2는 구체를 각각 고유한 64비트 식별자가 있는 셀로 나눕니다.


  • 셀 수준: 계층 구조는 큰 영역에서 작고 정확한 영역까지 다양한 세부 수준을 허용합니다. 각 레벨은 서로 다른 해상도를 나타냅니다.


    • 레벨 0: 지구 표면의 상당 부분을 덮고 있는 가장 큰 세포입니다.


    • 더 높은 수준: 세포는 점진적으로 더 작은 사분면으로 세분화됩니다. 예를 들어, 수준 1 셀은 각각 4개의 수준 2 셀로 나누어집니다.


    • 해상도 및 영역: 레벨이 높을수록 해상도가 더 미세하고 셀 영역이 더 작아집니다. 이 계층 구조를 사용하면 다양한 세부 수준에서 정확한 인덱싱 및 쿼리가 가능합니다.


아래 표에는 다양한 셀 수준과 해당 영역이 나와 있습니다.

수준

최소 면적

최대 면적

평균 면적

단위

셀 수

00

85011012.19

85011012.19

85011012.19

km2

6

01

21252753.05

21252753.05

21252753.05

km2

24

02

4919708.23

6026521.16

5313188.26

km2

96

03

1055377.48

1646455.50

1328297.07

km2

384

04

231564.06

413918.15

332074.27

km2

1536년

05

53798.67

104297.91

83018.57

km2

6K

06

12948.81

26113.30

20754.64

km2

24K

07

3175.44

6529.09

5188.66

km2

98K

08

786.20

1632.45

1297.17

km2

393K

09

195.59

408.12

324.29

km2

1573K

10

48.78

102.03

81.07

km2

6M

11

12.18

25.51

20.27

km2

25M

12

3.04

6.38

5.07

km2

100M

13

0.76

1.59

1.27

km2

402M

14

0.19

0.40

0.32

km2

1610M

15

47520.30

99638.93

79172.67

m2

6B

16

11880.08

24909.73

19793.17

m2

25B

17

2970.02

6227.43

4948.29

m2

103B

18

742.50

1556.86

1237.07

m2

412B

19

185.63

389.21

309.27

m2

1649B

20

46.41

97.30

77.32

m2

7T

21

11.60

24.33

19.33

m2

26T

22

2.90

6.08

4.83

m2

105T

23

0.73

1.52

1.21

m2

422T

24

0.18

0.38

0.30

m2

1689T

25

453.19

950.23

755.05

cm2

7시 15분

26

113.30

237.56

188.76

cm2

27e15

27

28.32

59.39

47.19

cm2

108e15

28

7.08

14.85

11.80

cm2

432e15

29

1.77

3.71

2.95

cm2

1729e15

30

0.44

0.93

0.74

cm2

7e18



제공된 표를 보면 S2를 사용하여 매핑 정밀도를 0.44cm^2까지 달성할 수 있다는 것이 분명합니다. S2 셀의 각 사각형 내에는 동일한 상위 셀을 공유하는 하위 셀이 존재하며 이는 계층 구조를 나타냅니다. 셀의 수준은 정적 값(모든 셀에 동일한 수준이 적용됨)일 수도 있고 S2가 가장 적합한 해상도를 결정하는 동적일 수도 있습니다.

가장 가까운 이웃 계산하기

예부터 시작해 보겠습니다. 시애틀 지역에 근접 서비스와 유사한 기능을 제공하는 애플리케이션을 작성한다고 가정해 보겠습니다. 우리는 주어진 주변에 있는 커피숍 목록을 반환하고 싶습니다. 이러한 작업을 수행하기 위해 이 작업을 4개의 하위 작업으로 나눕니다.


  • 시애틀 지도 로드 중
  • 시애틀 지도에서 S2 셀 시각화
  • 몇 개의 커피숍 위치를 데이터베이스에 저장
  • 가장 가까운 커피숍에 대한 쿼리

시애틀 지도 로드 중

Google 지도를 로드하려면 gmplot 라이브러리를 사용합니다. 이 라이브러리를 로드하려면 Google Maps API 키가 필요합니다. API 키를 생성하려면 여기 지침을 따르세요.

 import gmplot import const # plot seattle with zoom level 13 gmap = gmplot.GoogleMapPlotter(47.6061, -122.3328, 13, apikey=const.API_KEY) # Draw the map to an HTML file: gmap.draw('map.html')


위 코드는 아래와 같이 map.html 파일을 생성합니다.


시애틀 지도에서 S2 셀 시각화

이제 지도가 있으므로 지도용 S2 셀을 그려 보겠습니다.

 from s2 import * import gmplot # plot seattle with zoom level 13 gmap = gmplot.GoogleMapPlotter(47.6061, -122.3328, 13, apikey=const.API_KEY) areatobeplotted = [ (47.64395531736767,-122.43597221319135), (47.51369277846956,-122.43597221319135), (47.51369277846956,-122.24156866779164), (47.64395531736767,-122.24156866779164), (47.64395531736767,-122.43597221319135) ] region_rect = S2LatLngRect( S2LatLng.FromDegrees(47.51369277846956,-122.43597221319135), S2LatLng.FromDegrees(47.64395531736767, -122.24156866779164)) coverer = S2RegionCoverer() coverer.set_min_level(8) coverer.set_max_level(15) covering = coverer.GetCovering(region_rect) geoms = 0 for cellid in covering: new_cell = S2Cell(cellid) vertices = [] for i in range(0, 4): vertex = new_cell.GetVertex(i) latlng = S2LatLng(vertex) vertices.append((latlng.lat().degrees(), latlng.lng().degrees())) gmap.polygon(*zip(*vertices), face_color='pink', edge_color='cornflowerblue', edge_width=5) geoms+=1 gmap.polygon(*zip(*areatobeplotted), face_color='red', edge_color='green', edge_width=5) print(f"Total Geometries: {geoms}") gmap.draw('/tmp/map.html')


 Output: Total Geometries: 273


위 코드에서는 먼저 Google 지도 플로터를 시애틀 지역 중심에 배치합니다. S2RegionCoverer 에서는 최소 레벨 8에서 최대 레벨 15 사이의 동적 레벨을 갖도록 영역 커버러를 초기화합니다. 이를 통해 S2는 가장 적합하도록 모든 셀을 특정 셀 크기에 동적으로 맞출 수 있습니다. GetCovering 메서드는 시애틀 지역 주변의 직사각형에 대한 피복을 반환합니다.


그런 다음 각 셀을 반복하여 셀의 정점을 계산하고 이를 지도에 표시합니다. 생성된 셀 수를 약 273개로 유지합니다. 마지막으로 입력 직사각형을 빨간색으로 그립니다. 이 코드는 아래와 같이 시애틀 지도 /tmp/map.html 에 S2 셀을 표시합니다.


데이터베이스에 커피숍 위치 몇 개 저장

S2 셀 식별자와 함께 커피숍의 데이터베이스를 생성해 보겠습니다. 선택한 데이터베이스에 이러한 셀을 저장할 수 있습니다. 이 튜토리얼에서는 SQLite 데이터 데이터베이스를 사용합니다. 아래 코드 샘플에서는 SQLite 데이터베이스에 연결하여 Id , namecell_id 3개 필드가 있는 CoffeeShops 테이블을 생성합니다.


이전 예와 유사하게 S2RegionCoverer 사용하여 셀을 계산하지만 이번에는 점을 그리는 데 고정 수준을 사용합니다. 마지막으로 계산된 ID는 문자열로 변환되어 데이터베이스에 저장됩니다.


 import sqlite3 from s2 import S2CellId,S2LatLng,S2RegionCoverer # Connect to SQLite database conn = sqlite3.connect('/tmp/sqlite_cells.db') cursor = conn.cursor() # Create a table to store cell IDs cursor.execute('''CREATE TABLE IF NOT EXISTS CoffeeShops ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, cell_id TEXT )''') coverer = S2RegionCoverer() # Function to generate S2 cell ID for a given latitude and longitude def generate_cell_id(latitude, longitude, level=16): cell=S2CellId(S2LatLng.FromDegrees(latitude, longitude)) return str(cell.parent(level)) # Function to insert cell IDs into the database def insert_cell_ids(name,lat,lng): cell_id = generate_cell_id(lat, lng) cursor.execute("INSERT INTO CoffeeShops (name, cell_id) VALUES (?, ?)", (name, cell_id)) conn.commit() # Insert cell IDs into the database insert_cell_ids("Overcast Coffee", 47.616656277302155, -122.31156460382837) insert_cell_ids("Seattle Sunshine", 47.67366852914391, -122.29051997415843) insert_cell_ids("Sip House", 47.6682364706238, -122.31328618043693) insert_cell_ids("Victoria Coffee",47.624408595334536, -122.3117362652041) # Close connection conn.close()


이 시점에서 셀 수준에 대해 선택한 해상도에 따라 결정된 셀 ID와 함께 커피숍을 저장하는 데이터베이스가 있습니다.

가장 가까운 커피숍 검색

마지막으로 University District 지역의 커피숍을 쿼리해 보겠습니다.


 import sqlite3 from s2 import S2RegionCoverer,S2LatLngRect, S2LatLng # Connect to SQLite database conn = sqlite3.connect('/tmp/sqlite_cells.db') cursor = conn.cursor() # Function to query database for cells intersecting with the given polygon def query_intersecting_cells(start_x,start_y,end_x,end_y): # Create S2RegionCoverer region_rect = S2LatLngRect( S2LatLng.FromDegrees(start_x,start_y), S2LatLng.FromDegrees(end_x,end_y)) coverer = S2RegionCoverer() coverer.set_min_level(8) coverer.set_max_level(15) covering = coverer.GetCovering(region_rect) # Query for intersecting cells intersecting_cells = set() for cell_id in covering: cursor.execute("SELECT name FROM CoffeeShops WHERE cell_id >= ? and cell_id<=?", (str(cell_id.range_min()),str(cell_id.range_max()),)) intersecting_cells.update(cursor.fetchall()) return intersecting_cells # Query for intersecting cells intersecting_cells = query_intersecting_cells(47.6527847,-122.3286438,47.6782181, -122.2797203) # Print intersecting cells print("Intersecting cells:") for cell_id in intersecting_cells: print(cell_id[0]) # Close connection conn.close()
 Output: Intersecting cells: Sip House Seattle Sunshine

아래는 세포의 시각적 표현입니다. 간결성을 유지하기 위해 아래 시각화 코드는 추가되지 않았습니다.



모든 하위 셀과 상위 셀은 접두사를 공유하므로 min과 max 사이의 셀 범위를 쿼리하여 해당 두 값 사이의 모든 셀을 가져올 수 있습니다. 이 예에서는 동일한 원칙을 사용하여 커피숍을 쿼리합니다.

결론:

이 문서에서는 지리 공간 쿼리를 지원하지 않는 데이터베이스에 지리 공간 데이터를 저장하고 쿼리하기 위해 지리 인덱싱을 사용하는 방법을 설명했습니다. 이는 두 지점 간의 라우팅을 계산하거나 가장 가까운 이웃을 찾는 등 여러 사용 사례로 더욱 확장될 수 있습니다.


일반적으로 지리 색인이 생성된 데이터베이스 쿼리의 경우 데이터에 대해 몇 가지 추가적인 사후 처리를 수행해야 합니다. 노드에 부담을 주지 않도록 쿼리 및 사후 처리 논리에 대한 세심한 고려가 필요합니다.

참고자료: