📐 გეომეტრიული ალგორითმები¶
GIS-ში ყოველდღე ვიყენებთ ოპერაციებს, როგორიცაა „წერტილი პოლიგონშია?", „გავამარტივოთ ხაზი" ან „გარს შემოვავლოთ წერტილები". ამ გვერდზე ვნახავთ, როგორ მუშაობს ისინი შიგნიდან. ამ ალგორითმებზეა აგებული QgsGeometry, Shapely და PostGIS-ის ფუნქციები.
| ამოცანა | ალგორითმი | სირთულე |
|---|---|---|
| წერტილი პოლიგონში | Ray casting / Winding number | O(n) |
| ფართობი და ცენტროიდი | Shoelace ფორმულა | O(n) |
| ამოზნექილი გარსი | Andrew-ის monotone chain | O(n log n) |
| ხაზის გამარტივება | Douglas–Peucker | O(n log n) საშუალოდ, O(n²) უარეს შემთხვევაში |
| მონაკვეთების გადაკვეთა | ორიენტაციის ტესტი (cross) |
O(1) |
🎮 სცადეთ თქვენც¶
სამი რეჟიმია: წერტილი პოლიგონში (მაუსის ქვეშ ჩანს სხივი და გადაკვეთები), Douglas–Peucker (ε-ს ცვლილებით ხაზი მარტივდება) და ამოზნექილი გარსი (დაამატეთ წერტილები).
🧭 საფუძველი: cross (ორიენტაცია)¶
თითქმის ყველა ალგორითმი ერთ პატარა ფუნქციას ეყრდნობა: სამი წერტილისთვის cross(o, a, b) გვეუბნება, b წერტილი o→a ვექტორის მარცხნივაა (> 0), მარჯვნივ (< 0) თუ ერთ წრფეზე (= 0). ის ვექტორების სიბრტყული ნამრავლია.
🔹 წერტილი პოლიგონში¶
Ray casting: წერტილიდან მარჯვნივ ჰორიზონტალურ სხივს ვატარებთ და ვითვლით, რამდენ წიბოს კვეთს. კენტი რაოდენობა ნიშნავს „შიგნით", ლუწი კი „გარეთ".
Winding number: ვითვლით, პოლიგონი წერტილს რამდენჯერ შემოუვლის (საათის ისრის საწინააღმდეგოდ +1, საათის ისრისა −1). 0 ნიშნავს გარეთ.
საინტერესო განსხვავება
მარტივი პოლიგონისთვის ორივე ერთნაირ პასუხს იძლევა. თვითგადამკვეთი პოლიგონისთვის (მაგ. ხუთქიმიანი ვარსკვლავი) ისინი განსხვავდება: ray casting ცენტრალურ ხუთკუთხედს „გარეთ" თვლის, winding number კი „შიგნით" (2). სხვადასხვა GIS სხვადასხვა წესს იყენებს, ამიტომ პოლიგონები ჯერ ვალიდური უნდა იყოს (isValid).
🔹 ფართობი და ცენტროიდი¶
Shoelace ფორმულა პოლიგონის ფართობს წვეროების კოორდინატებით ითვლის. ნიშანი ბრუნვის მიმართულებას გვიჩვენებს (+ საათის ისრის საწინააღმდეგოდ).
🔹 ამოზნექილი გარსი (Convex Hull)¶
წარმოიდგინეთ ყველა წერტილზე მჭიდროდ დაჭიმული რეზინის ზონარი. Andrew-ის ალგორითმი წერტილებს ალაგებს და ორ ნაწილად (ქვედა და ზედა გარსი) აგებს: ყოველ ახალ წერტილზე ვშლით ბოლო წერტილებს, თუ ისინი „მარჯვნივ მოხვევას" ქმნიან.
🔹 Douglas–Peucker (ხაზის გამარტივება)¶
ხაზის პირველ და ბოლო წერტილს ვაერთებთ. ვპოულობთ წერტილს, რომელიც ამ მონაკვეთიდან ყველაზე შორსაა. თუ ის ε-ზე ახლოსაა, ყველა შუალედური წერტილი ამოვაგდოთ. თუ არა, ხაზს ამ წერტილზე ვყოფთ და ორივე ნაწილზე იგივეს რეკურსიულად ვიმეორებთ.
ε-ს ერთეული
ε კოორდინატების ერთეულშია. გრადუსებში მოცემულ მონაცემზე ε = 0.001 დაახლოებით 100 მეტრია, ამიტომ გამარტივებამდე მონაცემები პროექციულ CRS-ში გადაიყვანეთ.
ტოპოლოგია
Douglas–Peucker ხაზს ცალკე ამარტივებს და შეიძლება თვითგადაკვეთა ან მეზობელ პოლიგონთა შორის ხვრელი შექმნას. პოლიგონების საერთო საზღვრებისთვის გამოიყენეთ ტოპოლოგიის მაქსიმალური დაცვის ვარიანტები (მაგ. mapshaper, PostGIS ST_SimplifyPreserveTopology).
🐍 რეალიზაცია სუფთა Python-ით¶
| geometry_algorithms.py | |
|---|---|
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 | |
გამოყენება¶
import random
star = [(50 + (40 if i % 2 == 0 else 18) * math.cos(i * math.pi / 6),
50 + (40 if i % 2 == 0 else 18) * math.sin(i * math.pi / 6)) for i in range(12)]
print(point_in_polygon((50, 50), star)) # inside
print(point_in_polygon(star[0], star)) # boundary
print(point_in_polygon((0, 0), star)) # outside
square = [(0, 0), (4, 0), (4, 3), (0, 3)]
print(polygon_area(square), polygon_centroid(square)) # 12.0 (2.0, 1.5)
random.seed(1)
pts = [(random.random() * 100, random.random() * 100) for _ in range(500)]
print(len(convex_hull(pts)), 'წვერო 500 წერტილიდან')
line = [(i, 10 * math.sin(i / 7) + random.uniform(-1, 1)) for i in range(200)]
for eps in (0.5, 2, 5):
print(eps, len(douglas_peucker(line, eps)))
🧰 იგივე ბიბლიოთეკებით¶
poly = QgsGeometry.fromPolygonXY([[QgsPointXY(x, y) for x, y in square]])
pt = QgsGeometry.fromPointXY(QgsPointXY(1, 1))
print(poly.contains(pt)) # წერტილი პოლიგონში
print(poly.area(), poly.centroid().asPoint()) # ფართობი, ცენტროიდი
multi = QgsGeometry.fromMultiPointXY([QgsPointXY(x, y) for x, y in pts])
print(multi.convexHull().asWkt()) # ამოზნექილი გარსი
ln = QgsGeometry.fromPolylineXY([QgsPointXY(x, y) for x, y in line])
print(ln.simplify(2).constGet().numPoints()) # Douglas–Peucker
📝 დავალებები¶
დავალება 1
ააგეთ ხუთქიმიანი ვარსკვლავი (პენტაგრამა), ხუთივე წვეროს ერთმანეთისგან ერთი ნაბიჯით გამოტოვებით. შეამოწმეთ ცენტრალური წერტილი point_in_polygon-ითა და winding_number-ით. რატომ განსხვავდება პასუხი?
ამოხსნა
penta = [(50 + 40 * math.cos(math.radians(90 + 144 * i)),
50 + 40 * math.sin(math.radians(90 + 144 * i))) for i in range(5)]
print(point_in_polygon((50, 50), penta)) # outside
print(winding_number((50, 50), penta)) # 2
2 ≠ 0 → შიგნით).
დავალება 2
10 000 შემთხვევითი წერტილიდან რამდენი ხვდება ზემოთ აგებულ star პოლიგონში? შეადარეთ შედეგი მონტე-კარლოთი გამოთვლილ ფართობს და polygon_area(star)-ს (წერტილები არეში [0,100]×[0,100]).
ამოხსნა
random.seed(11)
N = 10_000
inside = sum(point_in_polygon((random.random() * 100, random.random() * 100), star) != 'outside'
for _ in range(N))
print(inside, 'წერტილი შიგნით')
print('მონტე-კარლო ფართობი:', inside / N * 100 * 100)
print('Shoelace ფართობი: ', abs(polygon_area(star)))
N-ის გაზრდით ის მცირდება.
დავალება 3
ზედა line-ისთვის ააგეთ ცხრილი: ε = 0.5, 1, 2, 5 და თითოეულისთვის წვეროების რაოდენობა და გამარტივებული ხაზის სიგრძე. რა პროცენტით იკლებს სიგრძე?
ამოხსნა
გამარტივება ყოველთვის ამცირებს სიგრძეს: კვეთს „კუთხეებს". ამიტომ შედეგი სიგრძეების გამოთვლისთვის (მაგ. სანაპირო ზოლის) არ გამოდგება.დავალება 4
იპოვეთ წერტილთა სიმრავლის დიამეტრი (ერთმანეთისგან ყველაზე დაშორებული ორი წერტილი). O(n²)-ის ნაცვლად გამოიყენეთ მხოლოდ ამოზნექილი გარსის წვეროები და შეამოწმეთ სრული გადარჩევით.