Add list-devices-near action with appropriate API documentation

This commit is contained in:
Starbeamrainbowlabs 2019-06-21 22:05:13 +01:00
parent efc780b377
commit 51c76ccd58
Signed by: sbrl
GPG key ID: 1BE5172E637709C2
10 changed files with 298 additions and 11 deletions

View file

@ -6,7 +6,8 @@
"yosymfony/toml": "^1.0", "yosymfony/toml": "^1.0",
"aura/autoload": "^2.0", "aura/autoload": "^2.0",
"php-di/php-di": "^6.0", "php-di/php-di": "^6.0",
"erusev/parsedown-extra": "^0.7.1" "erusev/parsedown-extra": "^0.7.1",
"mjaschen/phpgeo": "^2.1"
}, },
"license": "MPL-2.0", "license": "MPL-2.0",
"authors": [ "authors": [

70
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "d42581b26b6c99bcc70da335a64f686b", "content-hash": "ba074004640df6fbfd575edbf073650f",
"packages": [ "packages": [
{ {
"name": "aura/autoload", "name": "aura/autoload",
@ -203,6 +203,74 @@
], ],
"time": "2018-03-21T22:21:57+00:00" "time": "2018-03-21T22:21:57+00:00"
}, },
{
"name": "mjaschen/phpgeo",
"version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/mjaschen/phpgeo.git",
"reference": "0aaec41e7aff030a55db30bb8f6c2671faf03892"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mjaschen/phpgeo/zipball/0aaec41e7aff030a55db30bb8f6c2671faf03892",
"reference": "0aaec41e7aff030a55db30bb8f6c2671faf03892",
"shasum": ""
},
"require": {
"php": ">=7.0"
},
"require-dev": {
"jakub-onderka/php-parallel-lint": "^1.0",
"phpmd/phpmd": "^2.6",
"phpunit/phpunit": "~6.0",
"squizlabs/php_codesniffer": "^3.2",
"vimeo/psalm": "~3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Location\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Marcus Jaschen",
"email": "mjaschen@gmail.com",
"homepage": "https://www.marcusjaschen.de/"
}
],
"description": "Simple Geo Library",
"homepage": "https://phpgeo.marcusjaschen.de/",
"keywords": [
"Polygon",
"area",
"bearing",
"bounds",
"calculation",
"coordinate",
"distance",
"earth",
"ellipsoid",
"geo",
"geofence",
"gis",
"gps",
"haversine",
"length",
"point",
"polyline",
"projection",
"simplify",
"track",
"vincenty"
],
"time": "2019-03-21T18:53:24+00:00"
},
{ {
"name": "nikic/php-parser", "name": "nikic/php-parser",
"version": "v4.2.0", "version": "v4.2.0",

View file

@ -8,6 +8,7 @@ Action | Meaning
[`version`](#version) | Gets the version of _Air Quality Web_ that's currently running. [`version`](#version) | Gets the version of _Air Quality Web_ that's currently running.
[`fetch-data`](#fetch-data) | Fetches air quality data from the system for a specific data type at a specific date and time. [`fetch-data`](#fetch-data) | Fetches air quality data from the system for a specific data type at a specific date and time.
[`list-devices`](#list-devices) | Fetches a list of devices currently in the system. [`list-devices`](#list-devices) | Fetches a list of devices currently in the system.
[`list-devices-near`](#list-devices-near) | Lists the devices close to a given location (new since v0.11!)
[`device-info`](#device-info) | Gets (lots of) information about a single device. [`device-info`](#device-info) | Gets (lots of) information about a single device.
[`list-reading-types`](#list-reading-types) | Lists the different types of readings that can be specified. [`list-reading-types`](#list-reading-types) | Lists the different types of readings that can be specified.
[`device-data-bounds`](#device-data-bounds) | Gets the start and end DateTime bounds for the data recorded for a specific device. [`device-data-bounds`](#device-data-bounds) | Gets the start and end DateTime bounds for the data recorded for a specific device.
@ -18,6 +19,8 @@ These are explained in detail below. First though, a few notes:
- All dates are in UTC. - All dates are in UTC.
- All datetime-type fields support the keyword `now`. - All datetime-type fields support the keyword `now`.
- Additional object properties MAY be returned by the API at a later date. Clients MUST ignore additional unknown properties they do not understand.
- Clients SHOULD respect the `cache-control` headers returned by the API.
## version ## version
@ -64,6 +67,49 @@ https://example.com/path/to/api.php?action=list-devices
https://example.com/path/to/api.php?action=list-devices&only-with-location=yes https://example.com/path/to/api.php?action=list-devices&only-with-location=yes
``` ```
## list-devices-near
> Lists devices close to a given location.
**Remember:** Don't forget that unlike most other API actions, this requires a `POST` request and not a `GET`! This is to preserve privacy, as the web server stores GET parameters along with request urls in the server logs.
### GET Parameters
Parameter | Type | Meaning
--------------------|-----------|---------------------
`count` | int | Required. Specifies the number of devices to return.
### POST Parameters
POST parameters should be specified as part of the request body. The request should have a `content-type` of `application/x-www-form-urlencoded`.
Parameter | Type | Meaning
--------------------|-----------|---------------------
`latitude` | float | Required. The latitude of the location to search for nearby devices.
`longitude` | float | Required. The longitude of the location to search for nearby devices.
### Example HTTP Request
```
POST /api.php?action=list-devices-near&count=5 HTTP/1.1
host: airquality.example.com
content-length: 38
content-type: application/x-www-form-urlencoded
latitude=12.345678&longitude=98.765432
```
### Response Object Properties
Since it may not be obvious, the properties returned in a response object are detailed below:
Property | Meaning
--------------------|----------------------------------
`id` | The device's unique id.
`name` | The device's name. Should be unique, but don't count on it.
`latitude` | The latitude of the device in question.
`longitude` | The longitude of the aforementioned device.
`distance_calc` | The distance between the specified point and the device, represented as the length of a relative (lat, long) vector between the 2. _Should_ be accurate enough to get the devices in the right order with respect to the actual distance, but if not please [open an issue](https://github.com/ConnectedHumber/Aiq-Quality-Web/issues/new)
`distance_actual` | The actual distance between the specified point and the device, in metres.
## device-info ## device-info
> Gets (lots of) information about a single device. > Gets (lots of) information about a single device.

View file

@ -0,0 +1,96 @@
<?php
namespace AirQuality\Actions;
use \SBRL\TomlConfig;
use \AirQuality\Repositories\IDeviceRepository;
use \AirQuality\Repositories\IMeasurementTypeRepository;
use \AirQuality\ApiResponseSender;
use \AirQuality\Validator;
/**
* Action that lists the devices near a given location.
*/
class ListDevicesNear implements IAction {
/** @var TomlConfig */
private $settings;
/** @var \SBRL\PerformanceCounter */
private $perfcounter;
/** @var IDeviceRepository */
private $device_repo;
/** @var IMeasurementTypeRepository */
private $type_repo;
/** @var ApiResponseSender */
private $sender;
/** @var Validator */
private $validator_get;
/** @var Validator */
private $validator_post;
public function __construct(
TomlConfig $in_settings,
IDeviceRepository $in_device_repo,
ApiResponseSender $in_sender,
\SBRL\PerformanceCounter $in_perfcounter) {
$this->settings = $in_settings;
$this->device_repo = $in_device_repo;
$this->sender = $in_sender;
$this->perfcounter = $in_perfcounter;
$this->validator_get = new Validator($_GET);
$this->validator_post = new Validator($_POST);
}
public function handle() : bool {
global $start_time;
if(strtolower($_SERVER["REQUEST_METHOD"]) !== "post") {
$this->sender->send_error_plain(405, "Error: The devices-near action only takes a POST request, but you sent a {$_SERVER["REQUEST_METHOD"]} request. The parameters 'longitude' and 'latitude' should be specified in the POST body, and 'count' as a regular GET parameter.\nExample POST body (without quotes): 'latitude=12.345678&longitude=98.765432'\nDon't forget that the content-type header should be set to 'application/x-www-form-urlencoded'.", [
[ "x-time-taken", $this->perfcounter->render() ]
]);
return false;
}
// 1: Validate params
$this->validator_get->is_numberish("count");
$this->validator_get->run();
$this->validator_post->is_numberish("latitude");
$this->validator_post->is_numberish("longitude");
$this->validator_post->run();
// 2: Pull data from database
$this->perfcounter->start("sql");
$data = $this->device_repo->get_near_location(
floatval($_POST["latitude"]),
floatval($_POST["longitude"]),
intval($_GET["count"])
);
$this->perfcounter->end("sql");
// 3: Serialise data
$this->perfcounter->start("encode");
$response = json_encode($data);
$this->perfcounter->end("encode");
// 4: Send response
// Send a cache-control header, but only in production mode
if($this->settings->get("env.mode") == "production") {
header("cache-control: public, max-age=" . $this->settings->get("cache.max-age"));
}
header("content-length: " . strlen($response));
header("content-type: application/json");
header("x-time-taken: " . $this->perfcounter->render());
echo($response);
return true;
}
}

View file

@ -57,11 +57,17 @@ class Database
error_log("[Database/SQL] $sql"); error_log("[Database/SQL] $sql");
// FUTURE: Optionally cache prepared statements? // FUTURE: Optionally cache prepared statements?
$statement = $this->connection->prepare($sql); // Note that we replace tabs with spaces for debugging purposes
$statement = $this->connection->prepare(str_replace("\t", " ", $sql));
$statement->execute($variables); $statement->execute($variables);
return $statement; // fetchColumn(), fetchAll(), etc. are defined on the statement, not the return value of execute() return $statement; // fetchColumn(), fetchAll(), etc. are defined on the statement, not the return value of execute()
} }
/**
* Returns the connection string to use to connect to the database.
* This is calculated from the values specified in the settings file.
* @return string The connection string.
*/
private function get_connection_string() { private function get_connection_string() {
return "{$this->settings->get("database.type")}:host={$this->settings->get("database.host")};dbname={$this->settings->get("database.name")};charset=utf8mb4"; return "{$this->settings->get("database.type")}:host={$this->settings->get("database.host")};dbname={$this->settings->get("database.name")};charset=utf8mb4";
} }

View file

@ -2,6 +2,9 @@
namespace AirQuality\Repositories; namespace AirQuality\Repositories;
/**
* Defines the interface of repositories that fetch device information.
*/
interface IDeviceRepository { interface IDeviceRepository {
/** /**
* Returns an array of all the devices in the system with basic information * Returns an array of all the devices in the system with basic information
@ -19,4 +22,13 @@ interface IDeviceRepository {
* @return array The extended information available on the given device. * @return array The extended information available on the given device.
*/ */
public function get_device_info_ext($device_id); public function get_device_info_ext($device_id);
/**
* Gets a list of devices that are near the specified location.
* @param float $lat The latitude of the location to get devices near.
* @param float $long The longitude of the location to get devices near.
* @param int $count The number of nearby devices to return.
* @return array An array of nearby devices.
*/
public function get_near_location(float $lat, float $long, int $count);
} }

View file

@ -6,7 +6,7 @@ interface IMeasurementDataRepository {
/** /**
* Returns the specified reading type for all devices at the specified date * Returns the specified reading type for all devices at the specified date
* and time. * and time.
* @param DateTime $datetime The date and time to get the readings for. * @param \DateTime $datetime The date and time to get the readings for.
* @param int $reading_type_id The reading type id to fetch. * @param int $reading_type_id The reading type id to fetch.
* @return array The requested readings. * @return array The requested readings.
*/ */
@ -15,15 +15,15 @@ interface IMeasurementDataRepository {
/** /**
* Gets the first and last DateTimes of the readings for a specified device. * Gets the first and last DateTimes of the readings for a specified device.
* @param int $device_id The device id to fetch for. * @param int $device_id The device id to fetch for.
* @return DateTime[] The first and last DateTimes for which readings are stored. * @return \DateTime[] The first and last DateTimes for which readings are stored.
*/ */
public function get_device_reading_bounds(int $device_id); public function get_device_reading_bounds(int $device_id);
/** /**
* Gets the readings of a specified type for a specific device between the 2 given dates and times. * Gets the readings of a specified type for a specific device between the 2 given dates and times.
* @param int $device_id The id of the device to fetch data for. * @param int $device_id The id of the device to fetch data for.
* @param string $type_id The reading type to fetch. * @param int $type_id The reading type to fetch.
* @param DateTime $start The starting DateTime. * @param \DateTime $start The starting DateTime.
* @param DateTime $end The ending DateTime. * @param \DateTime $end The ending DateTime.
* @param int $average_seconds The number of seconds to averageg the data over. For example a value of 3600 (1 hour) will return 1 data point per hour, with the value of each point an average of all the readings for that hour. * @param int $average_seconds The number of seconds to averageg the data over. For example a value of 3600 (1 hour) will return 1 data point per hour, with the value of each point an average of all the readings for that hour.
* @return array The requested data. * @return array The requested data.
*/ */

View file

@ -2,6 +2,10 @@
namespace AirQuality\Repositories; namespace AirQuality\Repositories;
use Location\Coordinate;
use Location\Distance\Vincenty;
/** /**
* Fetches device info from a MariaDB database. * Fetches device info from a MariaDB database.
*/ */
@ -13,6 +17,7 @@ class MariaDBDeviceRepository implements IDeviceRepository {
public static $column_owner_id = "owner_id"; public static $column_owner_id = "owner_id";
public static $column_lat = "device_latitude"; public static $column_lat = "device_latitude";
public static $column_long = "device_longitude"; public static $column_long = "device_longitude";
public static $column_point = "lat_lon";
public static $column_altitude = "device_altitude"; public static $column_altitude = "device_altitude";
public static $table_name_type = "device_types"; public static $table_name_type = "device_types";
@ -35,14 +40,21 @@ class MariaDBDeviceRepository implements IDeviceRepository {
*/ */
private $database; private $database;
/**
* The distance calculator. From the mjaschen/phpgeo library on packagist.
* @var Vincenty
*/
private $distance_calculator;
/** /**
* Function that gets a static variable by it's name. Useful in preparing SQL queries. * Function that gets a static variable by it's name. Useful in preparing SQL queries.
* @var callable * @var callable
*/ */
private $get_static; private $get_static;
function __construct(\AirQuality\Database $in_database) { function __construct(\AirQuality\Database $in_database, Vincenty $in_distance_calculator) {
$this->database = $in_database; $this->database = $in_database;
$this->distance_calculator = $in_distance_calculator;
$this->get_static = function($name) { return self::$$name; }; $this->get_static = function($name) { return self::$$name; };
} }
@ -103,4 +115,43 @@ class MariaDBDeviceRepository implements IDeviceRepository {
} }
return $result; return $result;
} }
public function get_near_location(float $lat, float $long, int $count) {
$s = $this->get_static;
$result = $this->database->query(
"SELECT
{$s("table_name")}.{$s("column_device_id")} AS id,
{$s("table_name")}.{$s("column_device_name")} AS name,
{$s("table_name")}.{$s("column_lat")} AS latitude,
{$s("table_name")}.{$s("column_long")} AS longitude,
ST_DISTANCE(POINT(:latitude, :longitude), {$s("table_name")}.{$s("column_point")}) AS distance_calc
FROM {$s("table_name")}
WHERE {$s("table_name")}.{$s("column_point")} IS NOT NULL
ORDER BY ST_DISTANCE(POINT(:latitude_again, :longitude_again), {$s("table_name")}.{$s("column_point")})
LIMIT :count;", [
"latitude" => $lat,
"longitude" => $long,
"latitude_again" => $lat,
"longitude_again" => $long,
"count" => $count
]
)->fetchAll();
// Calculate the *actual* distance in metres.
// This is complicated and requires nasty formulae, so we're using a library here
// FUTURE: Apparently said library supports caching with PSR-6 - maybe we could take advantage of a PSR-6 implementation both here and elsewhere?
$loc = new Coordinate($lat, $long);
foreach($result as &$item) {
$item["distance_actual"] = $this->distance_calculator->getDistance(
$loc,
new Coordinate(
floatval($item["latitude"]),
floatval($item["longitude"])
)
);
}
return $result;
}
} }

View file

@ -20,8 +20,15 @@ class MariaDBMeasurementTypeRepository implements IMeasurementTypeRepository {
*/ */
private $database; private $database;
/** Functions that get a static variable by it's name. Useful in preparing SQL queries. */ /**
* Functions that get a static variable by it's name. Useful in preparing SQL queries.
* @var callable
*/
private $get_static; private $get_static;
/**
* Functions that get a static variable by it's class and property names. Useful in preparing SQL queries.
* @var callable
*/
private $get_static_extra; private $get_static_extra;
function __construct(\AirQuality\Database $in_database) { function __construct(\AirQuality\Database $in_database) {

View file

@ -1 +1 @@
v0.10.4 v0.11