mirror of
https://github.com/ConnectedHumber/Air-Quality-Web
synced 2024-10-12 00:34:02 +00:00
Add list-devices-near action with appropriate API documentation
This commit is contained in:
parent
efc780b377
commit
51c76ccd58
10 changed files with 298 additions and 11 deletions
|
@ -6,7 +6,8 @@
|
|||
"yosymfony/toml": "^1.0",
|
||||
"aura/autoload": "^2.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",
|
||||
"authors": [
|
||||
|
|
70
composer.lock
generated
70
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "d42581b26b6c99bcc70da335a64f686b",
|
||||
"content-hash": "ba074004640df6fbfd575edbf073650f",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aura/autoload",
|
||||
|
@ -203,6 +203,74 @@
|
|||
],
|
||||
"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",
|
||||
"version": "v4.2.0",
|
||||
|
|
|
@ -8,6 +8,7 @@ Action | Meaning
|
|||
[`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.
|
||||
[`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.
|
||||
[`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.
|
||||
|
@ -18,6 +19,8 @@ These are explained in detail below. First though, a few notes:
|
|||
|
||||
- All dates are in UTC.
|
||||
- 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
|
||||
|
@ -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
|
||||
```
|
||||
|
||||
## 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
|
||||
> Gets (lots of) information about a single device.
|
||||
|
||||
|
|
96
logic/Actions/ListDevicesNear.php
Normal file
96
logic/Actions/ListDevicesNear.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -57,11 +57,17 @@ class Database
|
|||
error_log("[Database/SQL] $sql");
|
||||
|
||||
// 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);
|
||||
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() {
|
||||
return "{$this->settings->get("database.type")}:host={$this->settings->get("database.host")};dbname={$this->settings->get("database.name")};charset=utf8mb4";
|
||||
}
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
namespace AirQuality\Repositories;
|
||||
|
||||
/**
|
||||
* Defines the interface of repositories that fetch device information.
|
||||
*/
|
||||
interface IDeviceRepository {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ interface IMeasurementDataRepository {
|
|||
/**
|
||||
* Returns the specified reading type for all devices at the specified date
|
||||
* 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.
|
||||
* @return array The requested readings.
|
||||
*/
|
||||
|
@ -15,15 +15,15 @@ interface IMeasurementDataRepository {
|
|||
/**
|
||||
* Gets the first and last DateTimes of the readings for a specified device.
|
||||
* @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);
|
||||
/**
|
||||
* 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 string $type_id The reading type to fetch.
|
||||
* @param DateTime $start The starting DateTime.
|
||||
* @param DateTime $end The ending DateTime.
|
||||
* @param int $type_id The reading type to fetch.
|
||||
* @param \DateTime $start The starting 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.
|
||||
* @return array The requested data.
|
||||
*/
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
namespace AirQuality\Repositories;
|
||||
|
||||
use Location\Coordinate;
|
||||
use Location\Distance\Vincenty;
|
||||
|
||||
|
||||
/**
|
||||
* 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_lat = "device_latitude";
|
||||
public static $column_long = "device_longitude";
|
||||
public static $column_point = "lat_lon";
|
||||
public static $column_altitude = "device_altitude";
|
||||
|
||||
public static $table_name_type = "device_types";
|
||||
|
@ -35,14 +40,21 @@ class MariaDBDeviceRepository implements IDeviceRepository {
|
|||
*/
|
||||
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.
|
||||
* @var callable
|
||||
*/
|
||||
private $get_static;
|
||||
|
||||
function __construct(\AirQuality\Database $in_database) {
|
||||
function __construct(\AirQuality\Database $in_database, Vincenty $in_distance_calculator) {
|
||||
$this->database = $in_database;
|
||||
$this->distance_calculator = $in_distance_calculator;
|
||||
|
||||
$this->get_static = function($name) { return self::$$name; };
|
||||
}
|
||||
|
@ -103,4 +115,43 @@ class MariaDBDeviceRepository implements IDeviceRepository {
|
|||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,15 @@ class MariaDBMeasurementTypeRepository implements IMeasurementTypeRepository {
|
|||
*/
|
||||
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;
|
||||
/**
|
||||
* Functions that get a static variable by it's class and property names. Useful in preparing SQL queries.
|
||||
* @var callable
|
||||
*/
|
||||
private $get_static_extra;
|
||||
|
||||
function __construct(\AirQuality\Database $in_database) {
|
||||
|
|
2
version
2
version
|
@ -1 +1 @@
|
|||
v0.10.4
|
||||
v0.11
|
||||
|
|
Loading…
Reference in a new issue