Monitoring travel time with Google Map’s API

Christian Christian 4 min read

I’ve recently started driving into the city for work and I wanted to start to gauge the traffic at various times during the morning and afternoon commutes.

In this post I’m going to show you how you can quickly set something up similar for yourself using Laravel and Google Maps Distance Matrix API.

We will be creating a Laravel command to fetch travel times, a schedule to run our fetch command at specific times and a front-end web UI to display a log of travel times fetched.

Checklist

  • Google Maps API key – although Google provide you with $200 free credit per month to use on their Maps API, keep in mind you still need to enable billing in your account.
  • Laravel with task scheduling enabled – this is simply configuring a cron job for the Laravel command scheduler.

Create new Laravel project

I personally use composer to create new Laravel projects.

$ composer create-project --prefer-dist laravel/laravel travel-monitor

For the sake of simplicity I will be using a SQLite database to store travel time data.

Edit the following settings in your projects .env file.

  • APP_NAME
  • DB_CONNECTION
  • DB_DATABASE
  • GOOGLE_MAPS_KEY
APP_NAME="Travel Monitor"
...
DB_CONNECTION=sqlite
DB_DATABASE=/path/to/your/database/travel.sqlite
...
GOOGLE_MAPS_KEY=[YOUR API KEY HERE]

Create a model for storing travel time data

$ php artisan make:model TravelTime

Add Google Maps Distance Matrix package

I forked a copy of the existing php-google-maps-distance-matrix package to add methods to retrieve the duration in traffic elements of the API response.

Add the repositories entry to your composer.json file to ensure you pull my version of the php-google-maps-distance-matrix package.

{
  ...
    "repositories": [
      {
        "type": "vcs",
        "url": "https://github.com/cmengler/php-google-maps-distance-matrix"
      }
     ],
   ...
}

Next, add php-google-maps-distance-matrix to your project. Make sure you add dev-duration_in_traffic argument to pull my branch from my fork.

$ composer require teampickr/php-google-maps-distance-matrix dev-duration_in_traffic

Publish the configuration file for this package.

$ php artisan vendor:publish --tag=config

This will publish config/google.php configuration. This configuration will read your GOOGLE_MAPS_KEY setting.

Create database schema for storing travel data

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateTravelTimeTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('travel_times', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('origin');
            $table->string('origin_alias');
            $table->string('destination');
            $table->string('destination_alias');
            $table->integer('duration');
            $table->integer('duration_in_traffic');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('travel_times');
    }
}

Next, run the migration script.

$ php artisan migrate

Create command for fetching travel data

$ php artisan make:command FetchTravelTime
<?php

namespace App\Console\Commands;

use App\TravelTime;
use Carbon\Carbon;
use Illuminate\Console\Command;
use TeamPickr\DistanceMatrix\DistanceMatrix;
use TeamPickr\DistanceMatrix\Avoid;
use TeamPickr\DistanceMatrix\Licenses\StandardLicense;

class FetchTravelTime extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'traveltime:fetch {origin} {origin_alias} {destination} {destination_alias}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Fetches travel time from origin to destination';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $origin = $this->argument('origin');
        $origin_alias = $this->argument('origin_alias');
        $destination = $this->argument('destination');
        $destination_alias = $this->argument('destination_alias');

        $this->info('origin=' . $origin .', destination=' . $destination);

        $now = new \DateTime('now', new \DateTimeZone('Australia/Melbourne'));

        $license = new StandardLicense(env('GOOGLE_MAPS_KEY'));

        $travelTimeResponse = DistanceMatrix::license($license)
            ->addOrigin($origin)
            ->addDestination($destination)
            ->setDepartureTime($now)
            ->setAvoid(Avoid::TOLLS)
            ->request();

        $rows = $travelTimeResponse->rows();
        $elements = $rows[0]->elements();
        $element = $elements[0];

        if ($element->successful()) {
            TravelTime::create([
                'origin' => $origin,
                'origin_alias' => $origin_alias,
                'destination' => $destination,
                'destination_alias' => $destination_alias,
                'duration' => $element->duration(),
                'duration_in_traffic' => $element->durationInTraffic(),
            ]);
        }
    }
}

Schedule the fetch command to run at the desired times.

...
    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        $home = '123 Test Street, Testville VIC 3000';
        $work = '456 John Drive, Smithville VIC 3000';

        // Home -> Work
        $schedule->command(FetchTravelTime::class, [$home, 'Home', $work, 'Work'])
            ->weekdays()
            ->everyFifteenMinutes()
            ->between('7:00', '10:00');

        // Work -> Home
        $schedule->command(FetchTravelTime::class, [$work, 'Work', $home, 'Home'])
            ->weekdays()
            ->everyFifteenMinutes()
            ->between('14:30', '18:00');
    }
...

Create controller resource to display travel times

$ php artisan make:controller TravelTimeController
<?php

namespace App\Http\Controllers;

use App\TravelTime;
use Carbon\Carbon;
use Illuminate\Http\Request;

class TravelTimeController extends Controller
{
    public function index()
    {
    	$travelTimes = TravelTime::get();

    	return view('traveltime', compact('travelTimes'));
    }
}

Update route for this controller in web.php

<?php

Route::get('/', 'TravelTimeController@index');

Create view to display travel times

<!doctype html>
<html lang="en">
	<head>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
		<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
		<title>{{ env('APP_NAME') }}</title>
	</head>
	<body>
		<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
			<span class="navbar-brand">{{ env('APP_NAME') }}</span>
		</nav>
		<div class="container">
			<table class="table table-hover table-responsive-sm">
				<thead>
						<tr>
								<th>Origin</th>
								<th>Destination</th>
								<th>Duration</th>
								<th>Traffic</th>
								<th>When</th>
						</tr>
				</thead>
				<tbody>
				@foreach($travelTimes as $travelTime)
					<tr>
						<td><attr title="{{ $travelTime->origin }}">{{ $travelTime->origin_alias }}</attr></td>
						<td><attr title="{{ $travelTime->destination }}">{{ $travelTime->destination_alias }}</attr></td>
						<td>{{ \Carbon\CarbonInterval::seconds($travelTime->duration)->cascade()->forHumans(['short' => true, 'parts' => 1]) }}</td>
						<td>{{ \Carbon\CarbonInterval::seconds($travelTime->duration_in_traffic)->cascade()->forHumans(['short' => true, 'parts' => 1]) }}</td>
						<td><a href="{{ url('dashboard', $travelTime->created_at->toDateString()) }}">{{ $travelTime->created_at }}</a></td>
					</tr>
				@endforeach
				</tbody>
			</table>
		</div>
		<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
		<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js" integrity="sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ" crossorigin="anonymous"></script>
		<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js" integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm" crossorigin="anonymous"></script>
		<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.bundle.min.js"></script>
	</body>
</html>

Done!

That’s it for now! I’ll be making further improvements to this little project along the way — stay tuned for part 2!

Leave a Reply

Your email address will not be published. Required fields are marked *