tidwall / tile38

Real-time Geospatial and Geofencing
https://tile38.com
MIT License
9.05k stars 565 forks source link

Issue with WITHIN Query in Tile38 Geofencing #746

Open hitchanatanael opened 1 month ago

hitchanatanael commented 1 month ago

I am experiencing an issue with the WITHIN query in Tile38 for geofencing. I have successfully set up the geofence using the SET command, but when I execute the WITHIN query, it always returns that the user is outside the geofence, even when the coordinates should be inside the defined area.

Here are the details of my setup:

Code:


namespace App\Http\Controllers;

use App\Models\Absensi;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Predis\Client;

class HomeController extends Controller
{
    protected $tile38;

    public function __construct()
    {
        // Inisialisasi koneksi ke Tile38
        try {
            $this->tile38 = new Client([
                'scheme' => 'tcp',
                'host'   => config('database.redis.tile38.host'),
                'port'   => config('database.redis.tile38.port'),
            ]);
            Log::info('Successfully connected to Tile38');
        } catch (\Exception $e) {
            Log::error('Failed to connect to Tile38: ' . $e->getMessage());
        }
    }

    private function checkGeofence($latitude, $longitude)
    {
        try {
            $pingResponse = $this->tile38->ping();
            if ($pingResponse->getPayload() !== 'PONG') {
                Log::error('Tidak dapat terhubung ke Tile38.');
                return ['status' => false, 'message' => 'Tidak dapat terhubung ke Tile38.'];
            }

            $geoJson = [
                'type' => 'Polygon',
                'coordinates' => [
                    [
                        [101.37970257356153, 0.478055156390471],
                        [101.37975609768263, 0.4780623891280328],
                        [101.37977851994957, 0.47807034513933927],
                        [101.37982408778241, 0.47802839526145813],
                        [101.37982408778241, 0.478007420322403],
                        [101.37982625767923, 0.4779604075277617],
                        [101.37978141314531, 0.4779437722311241],
                        [101.37974597149756, 0.4779524515163407],
                        [101.37970691335514, 0.4779871686570672],
                        [101.37970257356153, 0.478055156390471]
                    ]
                ]
            ];

            $geoJsonString = json_encode($geoJson);

            $setCommand = ['SET', 'geofence', 'mygeofence', 'OBJECT', $geoJsonString];
            $setResponse = $this->tile38->executeRaw($setCommand);
            Log::info('Tile38 set response', ['response' => $setResponse]);

            $keysCommand = ['KEYS', '*'];
            $keysResponse = $this->tile38->executeRaw($keysCommand);
            Log::info('Tile38 keys response', ['response' => $keysCommand]);

            $withinCommand = ['WITHIN', 'geofence', 'POINT', $longitude, $latitude];
            $response = $this->tile38->executeRaw($withinCommand);
            Log::info('Tile38 response', ['response' => $response]);

            if (isset($response[0]) && $response[0] === 'OK' && !empty($response[1])) {
                return ['status' => 'success', 'isWithin' => true];
            }

            return ['status' => 'error', 'message' => 'You are outside the geofence area'];
        } catch (\Exception $e) {
            Log::error('Error in Geofence check: ' . $e->getMessage());
            return ['status' => 'error', 'message' => 'Internal Server Error'];
        }
    }

    public function clockIn(Request $request)
    {
        $user = Auth::user();
        $currentTime = now()->toTimeString();
        $currentDate = now()->toDateString();
        $latitude = $request->input('latitude');
        $longitude = $request->input('longitude');

        $geofenceCheck = $this->checkGeofence($latitude, $longitude);

        if ($geofenceCheck['status'] === 'success' && $geofenceCheck['isWithin']) {
            try {
                Absensi::create([
                    'id_user' => $user->id,
                    'tgl_absen' => $currentDate,
                    'jam_masuk' => $currentTime,
                    'koor_masuk' => json_encode(['latitude' => $latitude, 'longitude' => $longitude]),
                    'status' => 1,
                ]);

                Log::info('Absensi berhasil', ['user_id' => $user->id, 'date' => $currentDate, 'time' => $currentTime]);
                return back()->with('success', 'Absensi berhasil');
            } catch (\Exception $e) {
                Log::error('Absensi gagal', ['error' => $e->getMessage()]);
                return back()->with('error', 'Absensi gagal, Silahkan coba lagi');
            }
        }

        Log::info('User is outside the geofence.');
        return back()->with('error', $geofenceCheck['message']);
    }

    public function clockOut(Request $request)
    {
        $user = Auth::user();
        $currentTime = now()->toTimeString();
        $latitude = $request->input('latitude');
        $longitude = $request->input('longitude');

        $geofenceCheck = $this->checkGeofence($latitude, $longitude);

        if ($geofenceCheck['status'] === 'success' && $geofenceCheck['isWithin']) {
            try {
                $absensi = Absensi::where('id_user', $user->id)
                    ->whereDate('tgl_absen', now()->toDateString())
                    ->first();

                if ($absensi && $absensi->status == 1) {
                    $absensi->update([
                        'jam_keluar' => $currentTime,
                        'koor_keluar' => json_encode(['latitude' => $latitude, 'longitude' => $longitude]),
                        'status' => 0
                    ]);

                    return back()->with('success', 'Clock Out berhasil');
                } else {
                    return back()->with('error', 'Clock In belum dilakukan');
                }
            } catch (\Exception $e) {
                Log::error('Clock out gagal', ['error' => $e->getMessage()]);
                return back()->with('error', 'Clock out gagal! Silahkan coba lagi');
            }
        }

        Log::info('User is outside the geofence.');
        return back()->with('error', $geofenceCheck['message']);
    }
}

LOGS:
[2024-07-13 01:59:55] local.INFO: Successfully connected to Tile38  
[2024-07-13 01:59:55] local.INFO: Tile38 set response {"response":"OK"} 
[2024-07-13 01:59:55] local.INFO: Tile38 keys response {"response":["KEYS","*"]} 
[2024-07-13 01:59:55] local.INFO: Tile38 response {"response":[0,[]]} 
[2024-07-13 01:59:55] local.INFO: User is outside the geofence.  
[2024-07-13 01:59:55] local.INFO: Successfully connected to Tile38  
tidwall commented 1 month ago

Perhaps the problem is that the WITHIN POINT command requires that the coordinates are lat/lon. You have lon/lat.

tidwall commented 1 month ago

The geojson appears to be in the correct order to me.

$geoJson = [
    'type' => 'Polygon',
    'coordinates' => [
        [
            [101.37970257356153, 0.478055156390471],
            [101.37975609768263, 0.4780623891280328],
            [101.37977851994957, 0.47807034513933927],
            [101.37982408778241, 0.47802839526145813],
            [101.37982408778241, 0.478007420322403],
            [101.37982625767923, 0.4779604075277617],
            [101.37978141314531, 0.4779437722311241],
            [101.37974597149756, 0.4779524515163407],
            [101.37970691335514, 0.4779871686570672],
            [101.37970257356153, 0.478055156390471]
        ]
    ]
];

I only issue I see is with the WITHIN command.

$withinCommand = ['WITHIN', 'geofence', 'POINT', $longitude, $latitude];

It should be:

$withinCommand = ['WITHIN', 'geofence', 'POINT', $latitude, $longitude];
iwpnd commented 1 month ago

No idea what I was thinking. Shouldn’t answer GitHub issues first thing in the morning. 🙃🙂

tidwall commented 1 month ago

One other thing. The WITHIN command as shown is attempting to find objects from the “geofences” collection that are “within” a POINT. That may not give the results that you want.

If what you are trying to do is to find which polygon geofences contain a specific point then I recommend using the INTERSECTS instead.

hitchanatanael commented 1 month ago

Previously I had also changed from longitude latitude to longitude latitude, and the results were still the same.

here is the generated laravel log: [2024-07-15 06:00:38] local.INFO: Successfully connected to Tile38 [2024-07-15 06:00:38] local.INFO: Tile38 set response {"response":"OK"} [2024-07-15 06:00:38] local.INFO: Tile38 response {"response":[0,[]]} [2024-07-15 06:00:38] local.INFO: User is outside the geofence.

protected $tile38;

public function __construct()
{
    try {
        $this->tile38 = new Client([
            'scheme' => 'tcp',
            'host' => config('database.redis.tile38.host'),
            'port' => config('database.redis.tile38.port'),
        ]);
        Log::info('Successfully connected to Tile38');
    } catch (\Exception $e) {
        Log::error('Failed to connect to Tile38: ' . $e->getMessage());
    }
}

private function checkGeofence($latitude, $longitude)
{
    try {
        $pingResponse = $this->tile38->ping();
        if ($pingResponse->getPayload() !== 'PONG') {
            Log::error('Tidak dapat terhubung ke Tile38.');
            return ['status' => false, 'message' => 'Tidak dapat terhubung ke Tile38.'];
        }

        $geoJson = '{
            "type" : "Polygon",
            "coordinates" : [
                [
                    [101.3629689712017, 0.46937355787708773],
                    [101.36295220739564, 0.46947011415901285],
                    [101.36297098285843, 0.46951839229948555],
                    [101.3630427319483, 0.46955057772626807],
                    [101.36313727981437, 0.4695324734237228],
                    [101.36315069085921, 0.46947883104552296],
                    [101.36316141969506, 0.4694332350237229],
                    [101.36313258594868, 0.46935210092537777],
                    [101.36299713439585, 0.46932527973563537],
                    [101.3629689712017, 0.46937355787708773]
                ]
            ]
        }';

        $geoJsonString = json_encode(json_decode($geoJson, true));

        $setCommand = ['SET', 'geofence', 'mygeofence', 'OBJECT', $geoJsonString];
        $setResponse = $this->tile38->executeRaw($setCommand);
        Log::info('Tile38 set response', ['response' => $setResponse]);

        $withinCommand = ['WITHIN', 'geofence', 'POINT', $latitude, $longitude];
        $response = $this->tile38->executeRaw($withinCommand);
        Log::info('Tile38 response', ['response' => $response]);

        if (isset($response[0]) && $response[0] === 'OK' && count($response) > 1) {
            return ['status' => 'success', 'isWithin' => true];
        }

        return ['status' => 'error', 'message' => 'You are outside the geofence area'];
    } catch (\Exception $e) {
        Log::error('Error in Geofence check: ' . $e->getMessage());
        return ['status' => 'error', 'message' => 'Internal Server Error'];
    }
}
hitchanatanael commented 1 month ago

Is there a Laravel project that uses tile38 before? I've looked for several sources, and haven't found anyone who has used Laravel. Maybe you have other references, and if you use another programming language that's okay, as long as you use set and within queries. because maybe I still have errors in making the query

iwpnd commented 1 month ago

Switch WITHIN with INTERSECTS and try again as suggested above. 🙂

A Polygon will never be WITHIN a point, but a Point can INTERSECT a Polygon.

hitchanatanael commented 1 month ago

Wow, I am very grateful, finally my project was successfully completed. For geofencing, do I need to send it in this comment? I will immediately send the Laravel controller for geofencing here. once again thank you all

iwpnd commented 1 month ago

For geofencing, do I need to send it in this comment?

To setup a geofence from that polygon you would do

SETHOOK {your_geofence} {endpoint to send events to} INTERSECT {the collection you want to alert on} OBJECT {your polygon}

// if you already have the geofence in your collection you can also do it like this
// to reuse the geometry you have already stored.
SETHOOK {name of your geofence} {endpoint where to send events to} INTERSECT {the collection you want to alert on} GET geofence mygeofence

e.g. to alert on vehicles in the fleet collection entering your geofence and sending to an https endpoint you would do

SETHOOK mygeofence https://localhost:3000/my_hook INTERSECTS fleet GET geofence mygeofence
hitchanatanael commented 1 month ago

My project using Laravel and Tile38 has been completed, and I also use INTERSECTS according to your suggestion to check whether the user is in the geofencing. Below is the complete code for geofencing:

private function checkGeofence($latitude, $longitude)
{
try {
    $pingResponse = $this->tile38->ping();
    if ($pingResponse->getPayload() !== 'PONG') {
        Log::error('Cannot connect to Tile38.');
        return ['status' => false, 'message' => 'Cannot connect to Tile38.'];
    }

    $geoJson = '{
    "type" : "Polygon",
    "coordinates" : [
        [
            [Fill in the coordinates in the order longitude, latitude],
        ]
    ]
}';

    $geoJsonString = json_encode(json_decode($geoJson, true));

    $setCommand = ['SET', 'geofence', 'mygeofence', 'OBJECT', $geoJsonString];
    $setResponse = $this->tile38->executeRaw($setCommand);
    Log::info('Tile38 set response', ['setResponse' => $setResponse]);

    $intersectsCommand = ['INTERSECTS', 'geofence', 'POINT', $latitude, $longitude];
    $intersectsResponse = $this->tile38->executeRaw($intersectsCommand);
    Log::info('Tile38 response', ['intersectsResponse' => $intersectsResponse]);

    if (isset($intersectsResponse[1]) && is_array($intersectsResponse[1]) && count($intersectsResponse[1]) > 0) {
        foreach ($intersectsResponse[1] as $result) {
            if ($result[0] === 'mygeofence') {
                return ['status' => 'success', 'isWithin' => true];
            }
        }
    }

    return ['status' => 'error', 'message' => 'You are outside the geofence area'];
} catch (\Exception $e) {
    Log::error('Error in Geofence check: ' . $e->getMessage());
    return ['status' => 'error', 'message' => 'Internal Server Error'];
}

}