Fix route popup distance and add areas swagger test

This commit is contained in:
Eugene Burmakin 2024-07-27 12:22:56 +02:00
parent ffe0334ebc
commit 87258df41e
18 changed files with 332 additions and 172 deletions

View file

@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [0.9.5] — 2024-07-22
## [0.9.5] — 2024-07-26
### Added
@ -14,9 +14,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- A background job to calculate your visits. This job will calculate your visits based on the areas you've created.
- Visits page. This page will show you all your visits, calculated based on the areas you've created. You can see the date and time of the visit, the area you've visited, and the duration of the visit.
- A possibility to confirm or decline a visit. When you create an area, the visit is not calculated immediately. You need to confirm or decline the visit. You can do this on the Visits page. Click on the visit, then click on the "Confirm" or "Decline" button. If you confirm the visit, it will be added to your timeline. If you decline the visit, it will be removed from your timeline.
- [ ] Glue two consecutive visits if there are no points between them
- [ ] Group visits by day and paginate them
- [ ] Tests
- [x] Swagger doc for Areas endpoint
- [ ] Atomicity for visits creation
### Fixed
- A route popup now correctly shows distance made in the route, not the distance between first and last points in the route.
---
## [0.9.4] — 2024-07-21

File diff suppressed because one or more lines are too long

View file

@ -1,17 +1,17 @@
# frozen_string_literal: true
class Api::V1::AreasController < ApplicationController
before_action :authenticate_user!
before_action :authenticate_api_key
before_action :set_area, only: %i[update destroy]
def index
@areas = current_user.areas
@areas = current_api_user.areas
render json: @areas, status: :ok
end
def create
@area = current_user.areas.build(area_params)
@area = current_api_user.areas.build(area_params)
if @area.save
render json: @area, status: :created
@ -37,7 +37,7 @@ class Api::V1::AreasController < ApplicationController
private
def set_area
@area = current_user.areas.find(params[:id])
@area = current_api_user.areas.find(params[:id])
end
def area_params

View file

@ -4,7 +4,7 @@ class Api::V1::PointsController < ApplicationController
before_action :authenticate_user!
def destroy
point = current_user.points.find(params[:id])
point = current_user.tracked_points.find(params[:id])
point.destroy
render json: { message: 'Point deleted successfully' }

View file

@ -10,17 +10,9 @@ module ApplicationHelper
end
end
def month_timespan(stat)
month = DateTime.new(stat.year, stat.month).in_time_zone(Time.zone)
start_at = month.beginning_of_month.to_time.strftime('%Y-%m-%dT%H:%M')
end_at = month.end_of_month.to_time.strftime('%Y-%m-%dT%H:%M')
{ start_at:, end_at: }
end
def year_timespan(year)
start_at = Time.utc(year).in_time_zone('Europe/Berlin').beginning_of_year.strftime('%Y-%m-%dT%H:%M')
end_at = Time.utc(year).in_time_zone('Europe/Berlin').end_of_year.strftime('%Y-%m-%dT%H:%M')
start_at = Time.utc(year).in_time_zone(Time.zone).beginning_of_year.strftime('%Y-%m-%dT%H:%M')
end_at = Time.utc(year).in_time_zone(Time.zone).end_of_year.strftime('%Y-%m-%dT%H:%M')
{ start_at:, end_at: }
end

View file

@ -17,6 +17,7 @@ export default class extends Controller {
connect() {
console.log("Map controller connected");
this.apiKey = this.element.dataset.api_key;
this.markers = JSON.parse(this.element.dataset.coordinates);
this.timezone = this.element.dataset.timezone;
this.clearFogRadius = this.element.dataset.fog_of_war_meters;
@ -233,24 +234,26 @@ export default class extends Controller {
fog.appendChild(circle);
}
addHighlightOnHover(polyline, map, startPoint, endPoint, prevPoint, nextPoint, timezone) {
addHighlightOnHover(polyline, map, polylineCoordinates, timezone) {
const originalStyle = { color: "blue", opacity: 0.6, weight: 3 };
const highlightStyle = { color: "yellow", opacity: 1, weight: 5 };
polyline.setStyle(originalStyle);
const startPoint = polylineCoordinates[0];
const endPoint = polylineCoordinates[polylineCoordinates.length - 1];
const firstTimestamp = new Date(startPoint[4] * 1000).toLocaleString("en-GB", { timeZone: timezone });
const lastTimestamp = new Date(endPoint[4] * 1000).toLocaleString("en-GB", { timeZone: timezone });
const minutes = Math.round((endPoint[4] - startPoint[4]) / 60);
const timeOnRoute = minutesToDaysHoursMinutes(minutes);
const distance = haversineDistance(startPoint[0], startPoint[1], endPoint[0], endPoint[1]);
const distanceToPrev = prevPoint ? haversineDistance(prevPoint[0], prevPoint[1], startPoint[0], startPoint[1]) : "N/A";
const distanceToNext = nextPoint ? haversineDistance(endPoint[0], endPoint[1], nextPoint[0], nextPoint[1]) : "N/A";
const timeBetweenPrev = prevPoint ? Math.round((startPoint[4] - prevPoint[4]) / 60) : "N/A";
const timeBetweenNext = nextPoint ? Math.round((nextPoint[4] - endPoint[4]) / 60) : "N/A";
const totalDistance = polylineCoordinates.reduce((acc, curr, index, arr) => {
if (index === 0) return acc;
const dist = haversineDistance(arr[index - 1][0], arr[index - 1][1], curr[0], curr[1]);
return acc + dist;
}, 0);
const startIcon = L.divIcon({ html: "🚥", className: "emoji-icon" });
const finishIcon = L.divIcon({ html: "🏁", className: "emoji-icon" });
@ -261,10 +264,18 @@ export default class extends Controller {
<b>Start:</b> ${firstTimestamp}<br>
<b>End:</b> ${lastTimestamp}<br>
<b>Duration:</b> ${timeOnRoute}<br>
<b>Distance:</b> ${formatDistance(distance)}<br>
<b>Total Distance:</b> ${formatDistance(totalDistance)}<br>
`;
if (isDebugMode) {
const prevPoint = polylineCoordinates[0];
const nextPoint = polylineCoordinates[polylineCoordinates.length - 1];
const distanceToPrev = haversineDistance(prevPoint[0], prevPoint[1], startPoint[0], startPoint[1]);
const distanceToNext = haversineDistance(endPoint[0], endPoint[1], nextPoint[0], nextPoint[1]);
const timeBetweenPrev = Math.round((startPoint[4] - prevPoint[4]) / 60);
const timeBetweenNext = Math.round((endPoint[4] - nextPoint[4]) / 60);
popupContent += `
<b>Prev Route:</b> ${Math.round(distanceToPrev)}m and ${minutesToDaysHoursMinutes(timeBetweenPrev)} away<br>
<b>Next Route:</b> ${Math.round(distanceToNext)}m and ${minutesToDaysHoursMinutes(timeBetweenNext)} away<br>
@ -341,12 +352,7 @@ export default class extends Controller {
const latLngs = polylineCoordinates.map((point) => [point[0], point[1]]);
const polyline = L.polyline(latLngs, { color: "blue", opacity: 0.6, weight: 3 });
const startPoint = polylineCoordinates[0];
const endPoint = polylineCoordinates[polylineCoordinates.length - 1];
const prevPoint = index > 0 ? splitPolylines[index - 1][splitPolylines[index - 1].length - 1] : null;
const nextPoint = index < splitPolylines.length - 1 ? splitPolylines[index + 1][0] : null;
this.addHighlightOnHover(polyline, map, startPoint, endPoint, prevPoint, nextPoint, timezone);
this.addHighlightOnHover(polyline, map, polylineCoordinates, timezone);
return polyline;
})
@ -433,7 +439,7 @@ export default class extends Controller {
this.areasLayer.addLayer(layer);
}
saveCircle(formData, layer) {
saveCircle(formData, layer, apiKey) {
const data = {};
formData.forEach((value, key) => {
const keys = key.split('[').map(k => k.replace(']', ''));
@ -445,12 +451,9 @@ export default class extends Controller {
}
});
fetch('/api/v1/areas', {
fetch(`"/api/v1/areas?api_key=${apiKey}"`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
headers: { 'Content-Type': 'application/json'},
body: JSON.stringify(data)
})
.then(response => {

View file

@ -0,0 +1,83 @@
# frozen_string_literal: true
class Areas::Visits::Create
attr_reader :user, :areas
def initialize(user, areas)
@user = user
@areas = areas
@time_threshold_minutes = 30 || user.settings['time_threshold_minutes']
@merge_threshold_minutes = 15 || user.settings['merge_threshold_minutes']
end
def call
areas.map { area_visits(_1) }
end
private
def area_visits(area)
points_grouped_by_month = area_points(area)
visits_by_month = group_points_by_month(points_grouped_by_month)
visits_by_month.each do |month, visits|
Rails.logger.info("Month: #{month}, Total visits: #{visits.size}")
visits.each do |time_range, visit_points|
create_or_update_visit(area, time_range, visit_points)
end
end
end
def area_points(area)
area_radius_in_km = area.radius / 1000.0
points = Point.where(user_id: user.id)
.near([area.latitude, area.longitude], area_radius_in_km)
.order(timestamp: :asc)
# check if all points within the area are assigned to a visit
points.group_by { |point| Time.zone.at(point.timestamp).strftime('%Y-%m') }
end
def group_points_by_month(points)
visits_by_month = {}
points.each do |month, points_in_month|
visits_by_month[month] = Visits::Group.new(
time_threshold_minutes: @time_threshold_minutes,
merge_threshold_minutes: @merge_threshold_minutes
).call(points_in_month)
end
visits_by_month
end
def create_or_update_visit(area, time_range, visit_points)
Rails.logger.info("Visit from #{time_range}, Points: #{visit_points.size}")
ActiveRecord::Base.transaction do
visit = find_visit(area.id, visit_points.first.timestamp)
visit.tap do |v|
v.name = "#{area.name}, #{time_range}"
v.ended_at = Time.zone.at(visit_points.last.timestamp)
v.duration = (visit_points.last.timestamp - visit_points.first.timestamp) / 60
v.status = :pending
end
visit.save!
visit_points.each { _1.update!(visit_id: visit.id) }
end
end
def find_visit(area_id, timestamp)
Visit.find_or_initialize_by(
area_id:,
user_id: user.id,
started_at: Time.zone.at(timestamp)
)
end
end

View file

@ -1,67 +0,0 @@
# frozen_string_literal: true
class Visits::Areas::Calculate
attr_reader :user, :areas
def initialize(user, areas)
@user = user
@areas = areas
@time_threshold_minutes = 30 || user.settings['time_threshold_minutes']
@merge_threshold_minutes = 15 || user.settings['merge_threshold_minutes']
end
def call
areas.map { area_visits(_1) }
end
private
def area_visits(area)
points_grouped_by_month = area_points(area)
visits_by_month = {}
points_grouped_by_month.each do |month, points_in_month|
visits_by_month[month] = Visits::Group.new(
time_threshold_minutes: @time_threshold_minutes,
merge_threshold_minutes: @merge_threshold_minutes
).call(points_in_month)
end
visits_by_month.each do |month, visits|
Rails.logger.info("Month: #{month}, Total visits: #{visits.size}")
visits.each do |time_range, visit_points|
Rails.logger.info("Visit from #{time_range}, Points: #{visit_points.size}")
ActiveRecord::Base.transaction do
visit = Visit.find_or_initialize_by(
area_id: area.id,
user_id: user.id,
started_at: Time.zone.at(visit_points.first.timestamp)
)
visit.update!(
name: "#{area.name}, #{time_range}",
area_id: area.id,
user_id: user.id,
started_at: Time.zone.at(visit_points.first.timestamp),
ended_at: Time.zone.at(visit_points.last.timestamp),
duration: (visit_points.last.timestamp - visit_points.first.timestamp) / 60, # in minutes
status: :pending
)
visit_points.each { _1.update!(visit_id: visit.id) }
end
end
end
end
def area_points(area)
area_radius_in_km = area.radius / 1000.0
Point.where(user_id: user.id)
.near([area.latitude, area.longitude], area_radius_in_km)
.order(timestamp: :asc)
.group_by { |point| Time.zone.at(point.timestamp).strftime('%Y-%m') }
end
end

View file

@ -73,6 +73,7 @@ class Visits::Group
previous_visit = visit
else
time_difference = (visit.start_time - previous_visit.end_time) / 60.0
if time_difference <= @merge_threshold_minutes
merge_visit(previous_visit, visit)
else

View file

@ -41,6 +41,7 @@
<div
class="w-full"
data-api_key="<%= current_user.api_key %>"
data-controller="maps"
data-coordinates="<%= @coordinates %>"
data-timezone="<%= Rails.configuration.time_zone %>"

View file

@ -8,7 +8,7 @@
<li><%= link_to 'Map', map_url, class: "#{active_class?(map_url)}" %></li>
<li><%= link_to 'Points', points_url, class: "#{active_class?(points_url)}" %></li>
<li><%= link_to 'Stats', stats_url, class: "#{active_class?(stats_url)}" %></li>
<li><%= link_to 'Visits', visits_url, class: "#{active_class?(visits_url)}" %></li>
<li><%= link_to 'Visits<sup>β</sup>'.html_safe, visits_url, class: "#{active_class?(visits_url)}" %></li>
<li><%= link_to 'Imports', imports_url, class: "#{active_class?(imports_url)}" %></li>
<li><%= link_to 'Exports', exports_url, class: "#{active_class?(exports_url)}" %></li>
</ul>
@ -42,7 +42,7 @@
<li><%= link_to 'Map', map_url, class: "#{active_class?(map_url)}" %></li>
<li><%= link_to 'Points', points_url, class: "#{active_class?(points_url)}" %></li>
<li><%= link_to 'Stats', stats_url, class: "#{active_class?(stats_url)}" %></li>
<li><%= link_to 'Visits', visits_url, class: "#{active_class?(visits_url)}" %></li>
<li><%= link_to 'Visits<sup>β</sup>'.html_safe, visits_url, class: "#{active_class?(visits_url)}" %></li>
<li><%= link_to 'Imports', imports_url, class: "#{active_class?(imports_url)}" %></li>
<li><%= link_to 'Exports', exports_url, class: "#{active_class?(exports_url)}" %></li>
</ul>

View file

@ -1,7 +1,7 @@
<div id="<%= dom_id stat %>" class="card w-full bg-base-200 shadow-xl">
<div class="card-body">
<h2 class="card-title">
<%= link_to map_url(month_timespan(stat)), class: "underline hover:no-underline text-#{header_colors.sample}" do %>
<%= link_to map_url(timespan(stat.month, stat.year)), class: "underline hover:no-underline text-#{header_colors.sample}" do %>
<%= "#{Date::MONTHNAMES[stat.month]} of #{stat.year}" %>
<% end %>
</h2>

View file

@ -1,11 +1,15 @@
<div class="w-full">
<% content_for :title, "Visits" %>
<div class="flex justify-between items-center">
<div class="flex justify-center my-5">
<h1 class="font-bold text-4xl">Visits</h1>
</div>
<div class="flex justify-center my-5">
<div class='flex'>
<%= paginate @visits %>
</div>
</div>
<ul class="timeline timeline-snap-icon max-md:timeline-compact timeline-vertical">
<% @visits.each.with_index do |date, index| %>

View file

@ -4,5 +4,10 @@ FactoryBot.define do
factory :visit do
area
user
started_at { Time.zone.now }
ended_at { Time.zone.now + 1.hour }
duration { 1.hour }
name { 'Visit' }
status { 'pending' }
end
end

View file

@ -5,11 +5,9 @@ require 'rails_helper'
RSpec.describe '/api/v1/areas', type: :request do
let(:user) { create(:user) }
before { sign_in user }
describe 'GET /index' do
it 'renders a successful response' do
get api_v1_areas_url
get api_v1_areas_url(api_key: user.api_key)
expect(response).to be_successful
end
end
@ -22,12 +20,12 @@ RSpec.describe '/api/v1/areas', type: :request do
it 'creates a new Area' do
expect do
post api_v1_areas_url, params: { area: valid_attributes }
post api_v1_areas_url(api_key: user.api_key), params: { area: valid_attributes }
end.to change(Area, :count).by(1)
end
it 'redirects to the created api_v1_area' do
post api_v1_areas_url, params: { area: valid_attributes }
post api_v1_areas_url(api_key: user.api_key), params: { area: valid_attributes }
expect(response).to have_http_status(:created)
end
@ -40,12 +38,12 @@ RSpec.describe '/api/v1/areas', type: :request do
it 'does not create a new Area' do
expect do
post api_v1_areas_url, params: { area: invalid_attributes }
post api_v1_areas_url(api_key: user.api_key), params: { area: invalid_attributes }
end.to change(Area, :count).by(0)
end
it 'renders a response with 422 status' do
post api_v1_areas_url, params: { area: invalid_attributes }
post api_v1_areas_url(api_key: user.api_key), params: { area: invalid_attributes }
expect(response).to have_http_status(:unprocessable_entity)
end
end
@ -58,14 +56,14 @@ RSpec.describe '/api/v1/areas', type: :request do
let(:new_attributes) { attributes_for(:area).merge(name: 'New Name') }
it 'updates the requested api_v1_area' do
patch api_v1_area_url(area), params: { area: new_attributes }
patch api_v1_area_url(area, api_key: user.api_key), params: { area: new_attributes }
area.reload
expect(area.reload.name).to eq('New Name')
end
it 'redirects to the api_v1_area' do
patch api_v1_area_url(area), params: { area: new_attributes }
patch api_v1_area_url(area, api_key: user.api_key), params: { area: new_attributes }
area.reload
expect(response).to have_http_status(:ok)
@ -77,7 +75,7 @@ RSpec.describe '/api/v1/areas', type: :request do
let(:invalid_attributes) { attributes_for(:area, name: nil) }
it 'renders a response with 422 status' do
patch api_v1_area_url(area), params: { area: invalid_attributes }
patch api_v1_area_url(area, api_key: user.api_key), params: { area: invalid_attributes }
expect(response).to have_http_status(:unprocessable_entity)
end
@ -89,12 +87,12 @@ RSpec.describe '/api/v1/areas', type: :request do
it 'destroys the requested api_v1_area' do
expect do
delete api_v1_area_url(area)
delete api_v1_area_url(area, api_key: user.api_key)
end.to change(Area, :count).by(-1)
end
it 'redirects to the api_v1_areas list' do
delete api_v1_area_url(area)
delete api_v1_area_url(area, api_key: user.api_key)
expect(response).to have_http_status(:ok)
end

View file

@ -12,94 +12,84 @@ require 'rails_helper'
# of tools you can use to make these specs even more expressive, but we're
# sticking to rails and rspec-rails APIs to keep things simple and stable.
RSpec.describe "/visits", type: :request do
RSpec.describe '/visits', type: :request do
# This should return the minimal set of attributes required to create a valid
# Visit. As you add validations to Visit, be sure to
# adjust the attributes here as well.
let(:valid_attributes) {
skip("Add a hash of attributes valid for your model")
}
let(:valid_attributes) do
skip('Add a hash of attributes valid for your model')
end
let(:invalid_attributes) {
skip("Add a hash of attributes invalid for your model")
}
let(:invalid_attributes) do
skip('Add a hash of attributes invalid for your model')
end
describe "GET /index" do
it "renders a successful response" do
describe 'GET /index' do
it 'renders a successful response' do
Visit.create! valid_attributes
get visits_url
expect(response).to be_successful
end
end
describe "GET /show" do
it "renders a successful response" do
describe 'GET /show' do
it 'renders a successful response' do
visit = Visit.create! valid_attributes
get visit_url(visit)
expect(response).to be_successful
end
end
describe "GET /new" do
it "renders a successful response" do
get new_visit_url
expect(response).to be_successful
end
end
describe "GET /edit" do
it "renders a successful response" do
describe 'GET /edit' do
it 'renders a successful response' do
visit = Visit.create! valid_attributes
get edit_visit_url(visit)
expect(response).to be_successful
end
end
describe "POST /create" do
context "with valid parameters" do
it "creates a new Visit" do
expect {
describe 'POST /create' do
context 'with valid parameters' do
it 'creates a new Visit' do
expect do
post visits_url, params: { visit: valid_attributes }
}.to change(Visit, :count).by(1)
end.to change(Visit, :count).by(1)
end
it "redirects to the created visit" do
it 'redirects to the created visit' do
post visits_url, params: { visit: valid_attributes }
expect(response).to redirect_to(visit_url(Visit.last))
end
end
context "with invalid parameters" do
it "does not create a new Visit" do
expect {
context 'with invalid parameters' do
it 'does not create a new Visit' do
expect do
post visits_url, params: { visit: invalid_attributes }
}.to change(Visit, :count).by(0)
end.to change(Visit, :count).by(0)
end
it "renders a response with 422 status (i.e. to display the 'new' template)" do
post visits_url, params: { visit: invalid_attributes }
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
describe "PATCH /update" do
context "with valid parameters" do
let(:new_attributes) {
skip("Add a hash of attributes valid for your model")
}
describe 'PATCH /update' do
context 'with valid parameters' do
let(:new_attributes) do
skip('Add a hash of attributes valid for your model')
end
it "updates the requested visit" do
it 'updates the requested visit' do
visit = Visit.create! valid_attributes
patch visit_url(visit), params: { visit: new_attributes }
visit.reload
skip("Add assertions for updated state")
skip('Add assertions for updated state')
end
it "redirects to the visit" do
it 'redirects to the visit' do
visit = Visit.create! valid_attributes
patch visit_url(visit), params: { visit: new_attributes }
visit.reload
@ -107,26 +97,24 @@ RSpec.describe "/visits", type: :request do
end
end
context "with invalid parameters" do
context 'with invalid parameters' do
it "renders a response with 422 status (i.e. to display the 'edit' template)" do
visit = Visit.create! valid_attributes
patch visit_url(visit), params: { visit: invalid_attributes }
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
describe "DELETE /destroy" do
it "destroys the requested visit" do
describe 'DELETE /destroy' do
it 'destroys the requested visit' do
visit = Visit.create! valid_attributes
expect {
expect do
delete visit_url(visit)
}.to change(Visit, :count).by(-1)
end.to change(Visit, :count).by(-1)
end
it "redirects to the visits list" do
it 'redirects to the visits list' do
visit = Visit.create! valid_attributes
delete visit_url(visit)
expect(response).to redirect_to(visits_url)

View file

@ -0,0 +1,67 @@
# frozen_string_literal: true
require 'swagger_helper'
describe 'Areas API', type: :request do
path '/api/v1/areas' do
post 'Creates an area' do
request_body_example value: {
'name': 'Home',
'latitude': 40.7128,
'longitude': -74.0060,
'radius': 100
}
tags 'Areas'
consumes 'application/json'
parameter name: :area, in: :body, schema: {
type: :object,
properties: {
name: { type: :string },
latitude: { type: :number },
longitude: { type: :number },
radius: { type: :number }
},
required: %w[name latitude longitude radius]
}
parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key'
response '201', 'area created' do
let(:area) { { name: 'Home', latitude: 40.7128, longitude: -74.0060, radius: 100 } }
let(:api_key) { create(:user).api_key }
run_test!
end
response '422', 'invalid request' do
let(:area) { { name: 'Home', latitude: 40.7128, longitude: -74.0060 } }
let(:api_key) { create(:user).api_key }
run_test!
end
end
get 'Retrieves all areas' do
tags 'Areas'
produces 'application/json'
parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key'
response '200', 'areas found' do
schema type: :array,
items: {
type: :object,
properties: {
id: { type: :integer },
name: { type: :string },
latitude: { type: :number },
longitude: { type: :number },
radius: { type: :number }
},
required: %w[id name latitude longitude radius]
}
let(:user) { create(:user) }
let(:areas) { create_list(:area, 3, user:) }
let(:api_key) { user.api_key }
run_test!
end
end
end
end

View file

@ -4,6 +4,87 @@ info:
title: API V1
version: v1
paths:
"/api/v1/areas":
post:
summary: Creates an area
tags:
- Areas
parameters:
- name: api_key
in: query
required: true
description: API Key
schema:
type: string
responses:
'201':
description: area created
'422':
description: invalid request
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
latitude:
type: number
longitude:
type: number
radius:
type: number
required:
- name
- latitude
- longitude
- radius
examples:
'0':
summary: Creates an area
value:
name: Home
latitude: 40.7128
longitude: -74.006
radius: 100
get:
summary: Retrieves all areas
tags:
- Areas
parameters:
- name: api_key
in: query
required: true
description: API Key
schema:
type: string
responses:
'200':
description: areas found
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: integer
name:
type: string
latitude:
type: number
longitude:
type: number
radius:
type: number
required:
- id
- name
- latitude
- longitude
- radius
"/api/v1/overland/batches":
post:
summary: Creates a batch of points