<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Axons Mobility]]></title><description><![CDATA[Axons Mobility is fleet software for scooter, bike, moped and car sharing and golf cart rentals. Run vehicles, riders, pricing, repairs and money from one conso]]></description><link>https://axonsmobility.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 02 Nov 2026 23:51:02 GMT</lastBuildDate><atom:link href="https://axonsmobility.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Golf Cart Geofencing in Python: Point-in-Polygon, GPS Noise and Debounced Alerts]]></title><description><![CDATA[Geofencing sounds simple: draw a shape on a map, and raise an alert when a vehicle goes inside it. On a golf course, the shape might be a green, a tee box…]]></description><link>https://axonsmobility.hashnode.dev/golf-cart-geofencing-in-python-point-in-polygon-gps-noise-and-debounced-alerts</link><guid isPermaLink="true">https://axonsmobility.hashnode.dev/golf-cart-geofencing-in-python-point-in-polygon-gps-noise-and-debounced-alerts</guid><category><![CDATA[Python]]></category><category><![CDATA[geofencing]]></category><category><![CDATA[gps]]></category><category><![CDATA[algorithms]]></category><dc:creator><![CDATA[Tejal Laxkar]]></dc:creator><pubDate>Wed, 28 Oct 2026 15:03:00 GMT</pubDate><content:encoded><![CDATA[<p>Geofencing sounds simple: draw a shape on a map, and raise an alert when a vehicle goes inside it. On a golf course, the shape might be a green, a tee box or a pond, and the vehicle is a golf cart reporting its position every few seconds.</p>
<p>The first version most of us write fires far too often. A cart parked on the path next to a green drifts in and out of the shape as the GPS fix wobbles by a few metres, and staff get a stream of false alerts. This post walks through a small Python example that handles the three parts that matter: the polygon test, noisy fixes and debouncing.</p>
<p>The code below is a teaching example, kept short on purpose. It runs as is with Python 3.10+ and no dependencies.</p>
<h2>1. Is the point inside the polygon?</h2>
<p>The classic approach is ray casting: draw a line from the point in one direction and count how many polygon edges it crosses. An odd count means inside, an even count means outside.</p>
<pre><code class="language-python">Point = tuple[float, float]  # (lat, lon)

def point_in_polygon(p: Point, polygon: list[Point]) -&gt; bool:
    """Ray casting: count how many polygon edges a ray from p crosses."""
    lat, lon = p
    inside = False
    j = len(polygon) - 1
    for i in range(len(polygon)):
        lat_i, lon_i = polygon[i]
        lat_j, lon_j = polygon[j]
        crosses = (lon_i &gt; lon) != (lon_j &gt; lon)
        if crosses:
            lat_at = lat_i + (lon - lon_i) * (lat_j - lat_i) / (lon_j - lon_i)
            if lat &lt; lat_at:
                inside = not inside
        j = i
    return inside
</code></pre>
<p>Treating latitude and longitude as flat x and y is fine for zones the size of a golf hole. For areas many kilometres across, project the coordinates first.</p>
<h2>2. Throw away fixes you cannot trust</h2>
<p>Most GPS devices report an accuracy estimate with each fix. Under trees, next to buildings or right after start-up, that estimate can be 30 metres or worse, which is wider than many greens. A fix that poor cannot tell you whether the cart is inside or outside, so the safest choice is to ignore it rather than guess.</p>
<h2>3. Debounce before you alert</h2>
<p>One fix inside the zone is not a breach. Several fixes in a row inside the zone is. The class below waits for a number of good fixes in a row before it reports <code>entered</code>, and reports <code>left</code> as soon as a good fix is outside.</p>
<pre><code class="language-python">from dataclasses import dataclass, field

@dataclass
class ZoneWatcher:
    polygon: list[Point]
    enter_after: int = 3        # good fixes in a row inside before we call it a breach
    max_accuracy_m: float = 15  # ignore fixes the device itself says are poor
    _inside_count: int = field(default=0, init=False)
    in_breach: bool = field(default=False, init=False)

    def update(self, p: Point, accuracy_m: float) -&gt; str | None:
        if accuracy_m &gt; self.max_accuracy_m:
            return None  # too noisy to trust either way
        if point_in_polygon(p, self.polygon):
            self._inside_count += 1
            if not self.in_breach and self._inside_count &gt;= self.enter_after:
                self.in_breach = True
                return "entered"
        else:
            self._inside_count = 0
            if self.in_breach:
                self.in_breach = False
                return "left"
        return None
</code></pre>
<h2>4. Try it</h2>
<pre><code class="language-python">green = [(33.5001, -111.9001), (33.5001, -111.8995),
         (33.4996, -111.8995), (33.4996, -111.9001)]
w = ZoneWatcher(green)
fixes = [
    ((33.5010, -111.9010), 5),   # outside
    ((33.4999, -111.8998), 40),  # inside, but poor accuracy: ignored
    ((33.4999, -111.8998), 5),   # inside 1
    ((33.4999, -111.8997), 5),   # inside 2
    ((33.4998, -111.8997), 5),   # inside 3 -&gt; entered
    ((33.5010, -111.9010), 5),   # outside -&gt; left
]
for p, acc in fixes:
    print(p, acc, w.update(p, acc))
</code></pre>
<p>Output:</p>
<pre><code>(33.501, -111.901) 5 None
(33.4999, -111.8998) 40 None
(33.4999, -111.8998) 5 None
(33.4999, -111.8997) 5 None
(33.4998, -111.8997) 5 entered
(33.501, -111.901) 5 left
</code></pre>
<p>The poor fix is ignored, and the alert fires only on the third good fix inside the green.</p>
<h2>Choosing the numbers</h2>
<ul>
<li><strong><code>enter_after</code></strong>: with a fix every 5 seconds, 3 fixes means about 15 seconds inside before an alert. Shorter catches more, but brings back false alerts.</li>
<li><strong><code>max_accuracy_m</code></strong>: set it below the width of your smallest zone. A 15 m limit works for a typical green, not for a narrow path.</li>
<li><strong>Exit rule</strong>: this example clears the breach on the first good fix outside. If carts often sit right on the edge, require two or three outside fixes as well.</li>
</ul>
<h2>What to build next</h2>
<p>In a real system you would also record each breach with the cart, the time and the route, so a person can review it later, and warn the rider before the cart gets deep into the zone. Keep the rules about warning people separate from any rule that changes how the vehicle behaves: those need far more care.</p>
<p>If you want to see how a fleet platform presents this to resorts and golf courses, here is a write-up on <a href="https://axonsmobility.com/use-cases/golf-carts/golf-cart-geofencing-resorts/">golf cart geofencing</a>.</p>
]]></content:encoded></item></channel></rss>