diff --git a/client/package.json b/client/package.json index f05752d7..fd8ad73e 100644 --- a/client/package.json +++ b/client/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "koji", - "version": "0.3.5", + "version": "0.4.0", "description": "Tool to make RDM routes", "main": "server/dist/index.js", "author": "TurtIeSocks <58572875+TurtIeSocks@users.noreply.github.com>", diff --git a/client/src/App.tsx b/client/src/App.tsx index 4af641d5..42dee060 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -3,10 +3,10 @@ import { CssBaseline, ThemeProvider } from '@mui/material' import { createBrowserRouter, RouterProvider } from 'react-router-dom' import createTheme from '@assets/theme' -import { Config } from '@assets/types' +import type { Config } from '@assets/types' import { usePersist } from '@hooks/usePersist' import { useStatic } from '@hooks/useStatic' -import { getData } from '@services/fetches' +import { fetchWrapper } from '@services/fetches' import Home from '@pages/Home' import Map from '@pages/map' @@ -64,7 +64,7 @@ export default function App() { const [error, setError] = React.useState('') React.useEffect(() => { - getData('/config/').then((res) => { + fetchWrapper('/config/').then((res) => { if (res) { if (location[0] === 0 && location[1] === 0) { setStore('location', [res.start_lat, res.start_lon]) diff --git a/client/src/assets/constants.ts b/client/src/assets/constants.ts index 31355a5d..2d8eab05 100644 --- a/client/src/assets/constants.ts +++ b/client/src/assets/constants.ts @@ -95,6 +95,32 @@ export const UNOWN_ROUTES = [ 'ManualQuest', ] as const +export const ALL_FENCES = [...new Set([...RDM_FENCES, ...UNOWN_FENCES])] + +export const ALL_ROUTES = [...new Set([...RDM_ROUTES, ...UNOWN_ROUTES])] + +export const CONVERSION_TYPES = [ + 'array', + 'multiArray', + 'geometry', + 'geometry_vec', + 'feature', + 'feature_vec', + 'featureCollection', + 'struct', + 'multiStruct', + 'text', + 'altText', + 'poracle', +] as const + +export const GEOMETRY_CONVERSION_TYPES = [ + 'Point', + 'MultiPoint', + 'Polygon', + 'MultiPolygon', +] as const + export const COLORS = [ '#F0F8FF', '#FAEBD7', diff --git a/client/src/assets/types.ts b/client/src/assets/types.ts index 4f9785c5..df6bc679 100644 --- a/client/src/assets/types.ts +++ b/client/src/assets/types.ts @@ -1,35 +1,138 @@ +/* eslint-disable @typescript-eslint/ban-types */ import type { - Feature, - FeatureCollection, + Feature as BaseFeature, + FeatureCollection as BaseFc, + GeoJsonProperties, GeoJsonTypes, + Geometry, + LineString, MultiPoint, + MultiPolygon, + Point, + Polygon, } from 'geojson' import type { UsePersist } from '@hooks/usePersist' import type { UseStatic } from '@hooks/useStatic' -import { TABS } from './constants' - -export type SpecificValueType = { - [k in keyof T]: T[k] extends U ? k : never +import { CONVERSION_TYPES, RDM_FENCES, RDM_ROUTES, TABS } from './constants' + +// UTILITY TYPES ================================================================================== + +export type SpecificValueType = { + [k in keyof T]: T[k] extends U + ? V extends true + ? k + : never + : V extends true + ? never + : k }[keyof T] -export type OnlyType = { [k in SpecificValueType]: U } +/* + * OnlyType - returns a type with only the keys of T that have a value of U + */ +export type OnlyType = { [k in SpecificValueType]: U } + +export type StoreNoFn = keyof OnlyType + +// ================================================================================================ + +// GEOJSON TYPES ================================================================================== + +export type Properties = + GeoJsonProperties & { + __leafletId?: number + __forward?: G extends Point ? number : undefined + __backward?: G extends Point ? number : undefined + __start?: G extends LineString ? number : undefined + __end?: G extends LineString ? number : undefined + __multipoint_id?: G extends Point ? KojiKey : undefined + __name?: string + __id?: number + __geofence_id?: number + __mode?: KojiModes + __projects?: number[] + } -export interface Data { - gyms: PixiMarker[] - pokestops: PixiMarker[] - spawnpoints: PixiMarker[] +export interface Feature + extends Omit, 'id'> { + id: G extends Point + ? number + : G extends LineString + ? `${number}__${number}` + : KojiKey | string } -export interface PixiMarker { - i: `${'p' | 'g' | 'v' | 'u'}${number}` & { [0]: 'p' | 'g' | 'v' | 'u' } - p: [number, number] +export interface FeatureCollection< + G extends Geometry | null = Geometry, + P = Properties, +> extends BaseFc { + features: Feature[] } -export interface Instance { +export type GeometryTypes = Exclude< + GeoJsonTypes, + 'Feature' | 'FeatureCollection' | 'GeometryCollection' +> + +// ================================================================================================ + +// KOJI TYPES ===================================================================================== + +export type KojiModes = + | typeof RDM_FENCES[number] + | typeof RDM_ROUTES[number] + | 'Unset' + +export type KojiKey = `${number}__${KojiModes}__${ + | 'KOJI' + | 'SCANNER' + | 'CLIENT'}` + +export type BasicKojiEntry = { + id: number name: string - type: string - data: FeatureCollection + created_at: Date | string + updated_at: Date | string +} + +export interface KojiGeofence extends BasicKojiEntry { + mode?: KojiModes + area: Feature +} + +export interface KojiProject extends BasicKojiEntry { + api_endpoint?: string + api_key?: string + scanner: boolean +} + +export interface KojiRoute extends BasicKojiEntry { + geofence_id: number + mode: KojiModes + description?: string + geometry: MultiPoint +} + +export interface AdminGeofence extends KojiGeofence { + properties: { key: string; value: string | number | boolean }[] + related: number[] +} + +export interface AdminProject extends KojiProject { + related: number[] +} + +export interface KojiStats { + best_clusters: [number, number][] + best_cluster_point_count: number + cluster_time: number + total_points: number + points_covered: number + total_clusters: number + total_distance: number + longest_distance: number + fetch_time: number } export interface KojiResponse { @@ -37,16 +140,31 @@ export interface KojiResponse { status_code: number status: string message: string - stats?: { - best_clusters: [number, number][] - best_cluster_point_count: number - cluster_time: number - total_points: number - points_covered: number - total_clusters: number - total_distance: number - longest_distance: number - } + stats?: KojiStats +} + +export interface DbOption + extends Omit { + mode: KojiModes + geo_type?: GeometryTypes + geofence_id?: number +} + +// ================================================================================================ + +// GENERAL TYPES ================================================================================== + +export type TabOption = typeof TABS[number] + +export interface Data { + gyms: PixiMarker[] + pokestops: PixiMarker[] + spawnpoints: PixiMarker[] +} + +export interface PixiMarker { + i: `${'p' | 'g' | 'v' | 'u'}${number}` & { [0]: 'p' | 'g' | 'v' | 'u' } + p: [number, number] } export interface Config { @@ -58,23 +176,13 @@ export interface Config { dangerous: boolean } -export interface Circle { - id: string - lat: number - lng: number - radius: number - type: 'circle' -} +export type CombinedState = Partial & Partial -export interface Polygon { - id: string - positions: [number, number][] - type: 'polygon' -} +export type Category = 'pokestop' | 'gym' | 'spawnpoint' -export type Shape = Circle | Polygon +// ================================================================================================ -export type CombinedState = Partial & Partial +// DATA TYPES ===================================================================================== export type ObjectInput = { lat: number; lon: number }[] export type MultiObjectInput = ObjectInput[] @@ -94,68 +202,29 @@ export interface Poracle { display_in_matches?: boolean } -export type ToConvert = +export type Conversions = | ObjectInput | MultiObjectInput | ArrayInput | MultiArrayInput + | Geometry + | Geometry[] | Feature + | Feature[] | FeatureCollection | string | Poracle - | Feature[] + | Poracle[] + +export type ConversionOptions = typeof CONVERSION_TYPES[number] +// ================================================================================================ + +// PROPS ========================================================================================== export interface PopupProps { id: Feature['id'] properties: Feature['properties'] + dbRef: DbOption | null } -export interface KojiGeofence { - id: number - name: string - mode: string - area: Feature -} - -export interface ClientGeofence extends KojiGeofence { - properties: { key: string; value: string | number | boolean }[] - related: number[] -} - -export interface KojiProject { - id: number - name: string -} - -export interface ClientProject extends KojiProject { - related: number[] -} - -export interface KojiRoute { - id: number - geofence_id: number - name: string - mode: string - geometry: MultiPoint -} - -export interface KojiStats { - best_clusters: [number, number][] - best_cluster_point_count: number - cluster_time: number - total_points: number - points_covered: number - total_clusters: number - total_distance: number - longest_distance: number - fetch_time: number -} - -export interface Option { - id: number - type: string - name: string - geoType?: Exclude -} - -export type TabOption = typeof TABS[number] +// ================================================================================================ diff --git a/client/src/components/Code.tsx b/client/src/components/Code.tsx index 9bb13925..0442344f 100644 --- a/client/src/components/Code.tsx +++ b/client/src/components/Code.tsx @@ -4,7 +4,7 @@ import { json, jsonParseLinter } from '@codemirror/lang-json' import { linter } from '@codemirror/lint' import { usePersist } from '@hooks/usePersist' -import { getData } from '@services/fetches' +import { fetchWrapper } from '@services/fetches' interface EditProps extends ReactCodeMirrorProps { code?: string @@ -42,7 +42,7 @@ export function Code({ if (setCode) { const newValue = value.state.doc.toString() if (newValue.startsWith('http')) { - const remoteValue = await getData(newValue) + const remoteValue = await fetchWrapper(newValue) setCode(JSON.stringify(remoteValue, null, 2)) } else { setCode(newValue) diff --git a/client/src/components/GeojsonWrapper.tsx b/client/src/components/GeojsonWrapper.tsx new file mode 100644 index 00000000..485d8808 --- /dev/null +++ b/client/src/components/GeojsonWrapper.tsx @@ -0,0 +1,56 @@ +import * as React from 'react' +import { GeoJSON, GeoJSONProps, useMap } from 'react-leaflet' +import distance from '@turf/distance' +import * as L from 'leaflet' + +import type { FeatureCollection } from '@assets/types' +import { getColor, mpToPoints } from '@services/utils' + +export default function GeoJsonWrapper({ + data: fc, + mode, + ...rest +}: { + data: FeatureCollection + mode?: string +} & GeoJSONProps) { + const map = useMap() + const featureCollection: FeatureCollection = { + ...fc, + features: fc.features.flatMap((feat) => + feat.geometry.type === 'MultiPoint' ? mpToPoints(feat.geometry) : feat, + ), + } + return ( + { + if (feat.properties?.next) { + L.polyline( + [ + [latlng.lat, latlng.lng], + [feat.properties.next[1], feat.properties.next[0]], + ], + { + color: getColor( + distance(feat, feat.properties.next, { + units: 'meters', + }), + ), + }, + ).addTo(map) + } + return L.circle(latlng, { + radius: mode + ? { + ManualQuest: 80, + CircleRaid: 1100, + CircleSmartRaid: 1100, + }[mode] || 70 + : feat.properties?.radius || 70, + }) + }} + {...rest} + /> + ) +} diff --git a/client/src/components/buttons/CombineByName.tsx b/client/src/components/buttons/CombineByName.tsx index cb6e1417..4f6cb0ac 100644 --- a/client/src/components/buttons/CombineByName.tsx +++ b/client/src/components/buttons/CombineByName.tsx @@ -1,6 +1,6 @@ import Button, { ButtonProps } from '@mui/material/Button/Button' import { combineByProperty } from '@services/utils' -import type { FeatureCollection } from 'geojson' +import type { FeatureCollection } from '@assets/types' import * as React from 'react' interface Props extends ButtonProps { diff --git a/client/src/components/buttons/SaveToKoji.tsx b/client/src/components/buttons/SaveToKoji.tsx index bff4dfda..b7e1cfce 100644 --- a/client/src/components/buttons/SaveToKoji.tsx +++ b/client/src/components/buttons/SaveToKoji.tsx @@ -1,21 +1,51 @@ -import Button, { ButtonProps } from '@mui/material/Button' -import { save } from '@services/fetches' import * as React from 'react' +import Button, { ButtonProps } from '@mui/material/Button' + +import type { FeatureCollection } from '@assets/types' +import { getKojiCache, save } from '@services/fetches' interface Props extends ButtonProps { - fc: string + fc: FeatureCollection } export default function SaveToKoji({ fc, ...rest }: Props) { + const routes = { + type: 'FeatureCollection', + features: fc.features.filter((feat) => feat.geometry.type === 'MultiPoint'), + } + const fences = { + type: 'FeatureCollection', + features: fc.features.filter((feat) => + feat.geometry.type.includes('Polygon'), + ), + } return ( ) } diff --git a/client/src/components/buttons/SaveToScanner.tsx b/client/src/components/buttons/SaveToScanner.tsx index 87051730..44a96c32 100644 --- a/client/src/components/buttons/SaveToScanner.tsx +++ b/client/src/components/buttons/SaveToScanner.tsx @@ -1,7 +1,6 @@ -/* eslint-disable no-console */ import { useStatic } from '@hooks/useStatic' import Button, { ButtonProps } from '@mui/material/Button' -import { save } from '@services/fetches' +import { getScannerCache, save } from '@services/fetches' import * as React from 'react' interface Props extends ButtonProps { @@ -13,9 +12,7 @@ export default function SaveToScanner({ fc, ...rest }: Props) { + ) +} + +export function PushToProd({ + resource, +}: { + resource: string +}) { + const record = useRecordContext() + const notify = useNotify() + + const sync = useMutation( + () => fetchWrapper(`/api/v1/${resource}/push/${record.id}`), + { + onSuccess: () => { + notify(`${record.name} synced with scanner`, { + type: 'success', + }) + }, + onError: () => { + notify(`Failed to sync ${record.name}`, { + type: 'error', + }) + }, + }, + ) + + return ( + { + event.stopPropagation() + sync.mutate() + }} + /> + ) +} + +export function BulkPushToProd({ + resource, +}: { + resource: string +}) { + const { selectedIds } = useListContext() + const unselectAll = useUnselectAll(resource) + const notify = useNotify() + + const sync = useMutation( + () => + Promise.all( + selectedIds.map((id) => fetchWrapper(`/api/v1/${resource}/push/${id}`)), + ), + { + onSuccess: () => { + notify( + `${selectedIds.length} ${capitalize(resource)}${ + selectedIds.length > 1 ? 's' : '' + } synced with scanner`, + { + type: 'success', + }, + ) + }, + onError: () => { + notify(`Failed to start quest on ${selectedIds.length} area(s)`, { + type: 'error', + }) + }, + }, + ) + + return ( + { + event.stopPropagation() + unselectAll() + sync.mutate() + }} + /> + ) +} diff --git a/client/src/pages/admin/dataProvider.ts b/client/src/pages/admin/dataProvider.ts index 381bbda5..294d6f21 100644 --- a/client/src/pages/admin/dataProvider.ts +++ b/client/src/pages/admin/dataProvider.ts @@ -122,8 +122,8 @@ export const dataProvider: typeof defaultProvider = { data: { ...json, id: 'id' in json ? json.id : '0' }, } }, - update: (resource, params) => - httpClient(`/internal/admin/${resource}/${params.id}/`, { + update: async (resource, params) => { + return httpClient(`/internal/admin/${resource}/${params.id}/`, { method: 'PATCH', body: JSON.stringify(params.data), }).then(({ json }) => { @@ -136,7 +136,8 @@ export const dataProvider: typeof defaultProvider = { } } return { data: { ...json, id: 'id' in json ? json.id : params.id } } - }), + }) + }, delete: (resource, params) => httpClient(`/internal/admin/${resource}/${params.id}/`, { method: 'DELETE', diff --git a/client/src/pages/admin/geofence/GeofenceCreate.tsx b/client/src/pages/admin/geofence/GeofenceCreate.tsx index ea44a385..21cd4d2e 100644 --- a/client/src/pages/admin/geofence/GeofenceCreate.tsx +++ b/client/src/pages/admin/geofence/GeofenceCreate.tsx @@ -1,12 +1,12 @@ import * as React from 'react' import { Create, SimpleForm, useNotify, useRedirect } from 'react-admin' import { Divider, Typography } from '@mui/material' -import { ClientGeofence } from '@assets/types' +import { AdminGeofence } from '@assets/types' import GeofenceCreateButton from './CreateDialog' import GeofenceForm from './GeofenceForm' -const transformPayload = async (geofence: ClientGeofence) => { +const transformPayload = async (geofence: AdminGeofence) => { return { id: 0, name: geofence.name, diff --git a/client/src/pages/admin/geofence/GeofenceEdit.tsx b/client/src/pages/admin/geofence/GeofenceEdit.tsx index 2799c63c..bc18d3ba 100644 --- a/client/src/pages/admin/geofence/GeofenceEdit.tsx +++ b/client/src/pages/admin/geofence/GeofenceEdit.tsx @@ -7,20 +7,23 @@ import { useRecordContext, } from 'react-admin' -import { ClientGeofence, KojiProject } from '@assets/types' -import { getData } from '@services/fetches' +import { AdminGeofence, KojiProject } from '@assets/types' +import { fetchWrapper } from '@services/fetches' import GeofenceForm from './GeofenceForm' -const transformPayload = async (geofence: ClientGeofence) => { +const transformPayload = async (geofence: AdminGeofence) => { if (Array.isArray(geofence.related)) { - await getData(`/internal/admin/geofence_project/geofence/${geofence.id}/`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', + await fetchWrapper( + `/internal/admin/geofence_project/geofence/${geofence.id}/`, + { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(geofence.related), }, - body: JSON.stringify(geofence.related), - }) + ) } return { ...geofence, diff --git a/client/src/pages/admin/geofence/GeofenceForm.tsx b/client/src/pages/admin/geofence/GeofenceForm.tsx index d9474179..efe4a7c6 100644 --- a/client/src/pages/admin/geofence/GeofenceForm.tsx +++ b/client/src/pages/admin/geofence/GeofenceForm.tsx @@ -13,7 +13,7 @@ import center from '@turf/center' import { useStatic } from '@hooks/useStatic' import { RDM_FENCES, UNOWN_FENCES } from '@assets/constants' import { safeParse } from '@services/utils' -import type { Feature } from 'geojson' +import type { Feature } from '@assets/types' import CodeInput from '../inputs/CodeInput' @@ -64,7 +64,12 @@ export default function GeofenceForm() { - + ) } diff --git a/client/src/pages/admin/geofence/GeofenceList.tsx b/client/src/pages/admin/geofence/GeofenceList.tsx index 70cce0fb..d17483f9 100644 --- a/client/src/pages/admin/geofence/GeofenceList.tsx +++ b/client/src/pages/admin/geofence/GeofenceList.tsx @@ -12,6 +12,7 @@ import { CreateButton, } from 'react-admin' import { BulkAssignButton } from '../actions/bulk/AssignButton' +import { BulkPushToProd, PushToProd } from '../actions/bulk/PushToApi' function ListActions() { return ( @@ -28,6 +29,7 @@ function BulkActions() { <> + ) } @@ -51,6 +53,7 @@ export default function GeofenceList() { + ) diff --git a/client/src/pages/admin/index.tsx b/client/src/pages/admin/index.tsx index 7730350c..d90a2e58 100644 --- a/client/src/pages/admin/index.tsx +++ b/client/src/pages/admin/index.tsx @@ -4,6 +4,7 @@ import { useTheme } from '@mui/material' import Architecture from '@mui/icons-material/Architecture' import AccountTree from '@mui/icons-material/AccountTree' import Route from '@mui/icons-material/Route' +import { getFullCache } from '@services/fetches' import { dataProvider } from './dataProvider' import Layout from './Layout' @@ -26,6 +27,10 @@ import RouteCreate from './route/RouteCreate' export default function AdminPanel() { const theme = useTheme() + React.useEffect(() => { + getFullCache() + }, []) + return ( record.name || ''} /> - record.name || ''} - /> record.name || ''} /> + record.name || ''} + /> ) } diff --git a/client/src/pages/admin/inputs/CodeInput.tsx b/client/src/pages/admin/inputs/CodeInput.tsx index 0886ac5e..353bc19d 100644 --- a/client/src/pages/admin/inputs/CodeInput.tsx +++ b/client/src/pages/admin/inputs/CodeInput.tsx @@ -1,4 +1,5 @@ -import { ToConvert } from '@assets/types' +import { GEOMETRY_CONVERSION_TYPES } from '@assets/constants' +import { ConversionOptions, Conversions } from '@assets/types' import { Code } from '@components/Code' import { usePersist } from '@hooks/usePersist' import { Typography } from '@mui/material' @@ -10,9 +11,13 @@ import { useInput } from 'react-admin' export default function CodeInput({ source, label, + conversionType, + geometryType, }: { source: string label?: string + conversionType: ConversionOptions + geometryType: typeof GEOMETRY_CONVERSION_TYPES[number] }) { const { field } = useInput({ source }) const [error, setError] = React.useState('') @@ -33,23 +38,24 @@ export default function CodeInput({ field.onChange({ target: { value: newCode } }) }} onBlurCapture={async () => { - const geofence = safeParse(field.value) - if (!geofence.error) { - await convert(geofence.value, 'feature', simplifyPolygons).then( - (res) => { - if (Array.isArray(res)) { - setError( - 'Warning, multiple features were found, you should only assign one feature!', - ) - } else { - field.onChange({ - target: { value: JSON.stringify(res, null, 2) }, - }) - setError('') - } - }, - ) - } + const geofence = safeParse(field.value) + await convert( + geofence.error ? field.value : geofence.value, + conversionType, + simplifyPolygons, + geometryType, + ).then((res) => { + if (Array.isArray(res)) { + setError( + 'Warning, multiple features were found, you should only assign one feature!', + ) + } else { + field.onChange({ + target: { value: JSON.stringify(res, null, 2) }, + }) + setError('') + } + }) }} /> {error} diff --git a/client/src/pages/admin/project/ProjectCreate.tsx b/client/src/pages/admin/project/ProjectCreate.tsx index a705fb4d..636bf1fa 100644 --- a/client/src/pages/admin/project/ProjectCreate.tsx +++ b/client/src/pages/admin/project/ProjectCreate.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Create, useNotify, useRedirect } from 'react-admin' +import { Create, SimpleForm, useNotify, useRedirect } from 'react-admin' import ProjectForm from './ProjectForm' @@ -14,7 +14,9 @@ export default function ProjectCreate() { return ( - + + + ) } diff --git a/client/src/pages/admin/project/ProjectEdit.tsx b/client/src/pages/admin/project/ProjectEdit.tsx index 9e783621..6e482ba1 100644 --- a/client/src/pages/admin/project/ProjectEdit.tsx +++ b/client/src/pages/admin/project/ProjectEdit.tsx @@ -4,22 +4,25 @@ import { Edit, ReferenceArrayInput, SimpleForm, - TextInput, useRecordContext, } from 'react-admin' -import { ClientProject, KojiGeofence } from '@assets/types' -import { getData } from '@services/fetches' +import { AdminProject, KojiGeofence } from '@assets/types' +import { fetchWrapper } from '@services/fetches' +import ProjectForm from './ProjectForm' -const transformPayload = async (project: ClientProject) => { +const transformPayload = async (project: AdminProject) => { if (Array.isArray(project.related)) { - await getData(`/internal/admin/geofence_project/project/${project.id}/`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', + await fetchWrapper( + `/internal/admin/geofence_project/project/${project.id}/`, + { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(project.related), }, - body: JSON.stringify(project.related), - }) + ) } return project } @@ -38,7 +41,7 @@ export default function ProjectEdit() { return ( - + + <> - + + + + ) } diff --git a/client/src/pages/admin/project/ProjectList.tsx b/client/src/pages/admin/project/ProjectList.tsx index ec6118f7..098cc6a0 100644 --- a/client/src/pages/admin/project/ProjectList.tsx +++ b/client/src/pages/admin/project/ProjectList.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { + BooleanField, BulkDeleteWithUndoButton, CreateButton, Datagrid, @@ -47,6 +48,10 @@ export default function ProjectList() { > }> + + + + diff --git a/client/src/pages/admin/project/ProjectShow.tsx b/client/src/pages/admin/project/ProjectShow.tsx index 337b0bed..9a2de1cd 100644 --- a/client/src/pages/admin/project/ProjectShow.tsx +++ b/client/src/pages/admin/project/ProjectShow.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { + BooleanField, ChipField, ReferenceArrayField, Show, @@ -17,6 +18,9 @@ export default function ProjectShow() { Overview + + + { +const transformPayload = (route: KojiRoute) => { return { id: 0, - name: geofence.name, - mode: geofence.mode, - geometry: JSON.parse(JSON.stringify(geofence.geometry)), + name: route.name, + mode: route.mode, + geometry: + typeof route.geometry === 'string' + ? JSON.parse(route.geometry) + : route.geometry, } } diff --git a/client/src/pages/admin/route/RouteEdit.tsx b/client/src/pages/admin/route/RouteEdit.tsx index dc3909d4..300bb6ed 100644 --- a/client/src/pages/admin/route/RouteEdit.tsx +++ b/client/src/pages/admin/route/RouteEdit.tsx @@ -1,11 +1,23 @@ import * as React from 'react' import { Edit, SimpleForm } from 'react-admin' +import type { KojiRoute } from '@assets/types' + import RouteForm from './RouteForm' +const transformPayload = async (route: KojiRoute) => { + return { + ...route, + geometry: + typeof route.geometry === 'string' + ? JSON.parse(route.geometry) + : route.geometry, + } +} + export default function RouteEdit() { return ( - + diff --git a/client/src/pages/admin/route/RouteForm.tsx b/client/src/pages/admin/route/RouteForm.tsx index 79cda2ab..defb122b 100644 --- a/client/src/pages/admin/route/RouteForm.tsx +++ b/client/src/pages/admin/route/RouteForm.tsx @@ -1,54 +1,17 @@ import * as React from 'react' import { FormDataConsumer, SelectInput, TextInput } from 'react-admin' import { Box } from '@mui/material' -import Map from '@components/Map' -import { GeoJSON, useMap } from 'react-leaflet' +import type { MultiPoint } from 'geojson' import center from '@turf/center' -import { useStatic } from '@hooks/useStatic' + import { RDM_ROUTES, UNOWN_ROUTES } from '@assets/constants' -import { getColor, safeParse } from '@services/utils' -import type { FeatureCollection, MultiPoint } from 'geojson' -import * as L from 'leaflet' -import distance from '@turf/distance' -import CodeInput from '../inputs/CodeInput' +import type { FeatureCollection } from '@assets/types' +import GeoJsonWrapper from '@components/GeojsonWrapper' +import Map from '@components/Map' +import { useStatic } from '@hooks/useStatic' +import { safeParse } from '@services/utils' -export function GeoJsonWrapper({ - fc, - mode, -}: { - fc: FeatureCollection - mode: string -}) { - const map = useMap() - return ( - { - L.polyline( - [ - [latlng.lat, latlng.lng], - [feat.properties?.next[1], feat.properties?.next[0]], - ], - { - color: getColor( - distance(feat, feat.properties?.next, { - units: 'meters', - }), - ), - }, - ).addTo(map) - return L.circle(latlng, { - radius: - { - ManualQuest: 80, - CircleRaid: 1100, - CircleSmartRaid: 1100, - }[mode] || 70, - }) - }} - /> - ) -} +import CodeInput from '../inputs/CodeInput' export default function RouteForm() { const scannerType = useStatic((s) => s.scannerType) @@ -56,6 +19,7 @@ export default function RouteForm() { return ( <> + { return { + id: `${i}`, type: 'Feature', geometry: { type: 'Point', @@ -103,13 +68,18 @@ export default function RouteForm() { zoomControl style={{ width: '100%', height: '50vh' }} > - + ) }} - + ) } diff --git a/client/src/pages/admin/route/RouteList.tsx b/client/src/pages/admin/route/RouteList.tsx index ab97c3dc..fab2e386 100644 --- a/client/src/pages/admin/route/RouteList.tsx +++ b/client/src/pages/admin/route/RouteList.tsx @@ -13,6 +13,7 @@ import { ReferenceField, } from 'react-admin' import { BulkAssignButton } from '../actions/bulk/AssignButton' +import { BulkPushToProd, PushToProd } from '../actions/bulk/PushToApi' function ListActions() { return ( @@ -29,6 +30,7 @@ function BulkActions() { <> + ) } @@ -48,11 +50,13 @@ export default function GeofenceList() { > }> + + ) diff --git a/client/src/pages/admin/route/RouteShow.tsx b/client/src/pages/admin/route/RouteShow.tsx index 50302411..14eab9f0 100644 --- a/client/src/pages/admin/route/RouteShow.tsx +++ b/client/src/pages/admin/route/RouteShow.tsx @@ -18,6 +18,7 @@ export default function GeofenceShow() { Overview + diff --git a/client/src/pages/map/index.tsx b/client/src/pages/map/index.tsx index 8d2e79e6..5071602c 100644 --- a/client/src/pages/map/index.tsx +++ b/client/src/pages/map/index.tsx @@ -10,6 +10,7 @@ import DrawerIndex from '@components/drawer' import Main from '@components/styled/Main' import Loading from '@components/Loading' import NetworkAlert from '@components/notifications/NetworkStatus' +import { getFullCache } from '@services/fetches' import Markers from './markers' import Interface from './interface' @@ -25,8 +26,11 @@ import { export default function MapWrapper() { const drawer = usePersist((s) => s.drawer) const menuItem = usePersist((s) => s.menuItem) + const drawerWidth = menuItem === 'Geojson' ? 515 : 345 - const drawerWidth = menuItem === 'Geojson' ? 500 : 325 + React.useEffect(() => { + getFullCache() + }, []) return ( diff --git a/client/src/pages/map/interface/Drawing.tsx b/client/src/pages/map/interface/Drawing.tsx index a2d746df..cea0da7b 100644 --- a/client/src/pages/map/interface/Drawing.tsx +++ b/client/src/pages/map/interface/Drawing.tsx @@ -2,10 +2,11 @@ import * as React from 'react' import { FeatureGroup, useMap } from 'react-leaflet' import * as L from 'leaflet' -import type { Feature, MultiPolygon, Point, Polygon } from 'geojson' +import type { MultiPolygon, Point, Polygon } from 'geojson' import 'leaflet-arrowheads' - import { GeomanControls } from 'react-leaflet-geoman-v2' + +import type { Feature } from '@assets/types' import { useStatic } from '@hooks/useStatic' import { usePersist } from '@hooks/usePersist' import { useShapes } from '@hooks/useShapes' @@ -146,16 +147,16 @@ export function Drawing() { case 'Polygon': if (layer instanceof L.Polygon) { const feature = layer.toGeoJSON() - feature.id = id + feature.id = id.toString() if (feature.geometry.type === 'Polygon') { setShapes('Polygon', (prev) => ({ ...prev, - [id]: feature as Feature, + [id.toString()]: feature as Feature, })) } else if (feature.geometry.type === 'MultiPolygon') { setShapes('MultiPolygon', (prev) => ({ ...prev, - [id]: feature as Feature, + [id.toString()]: feature as Feature, })) } } @@ -169,25 +170,29 @@ export function Drawing() { const last = getters.getLast() if (feature.properties) { - feature.properties.forward = first?.id - feature.properties.backward = last?.id + if (typeof first?.id === 'number') { + feature.properties.__forward = first.id + } + if (typeof last?.id === 'number') { + feature.properties.__backward = last.id + } } if (last?.properties) { - last.properties.forward = id + last.properties.__forward = id } if (first?.properties) { - first.properties.backward = id + first.properties.__backward = id } if (last && first) { setShapes('LineString', (prev) => { const newState: typeof prev = { ...prev, - [`${last.id}_${feature.id}`]: { + [`${+last.id}__${+feature.id}`]: { type: 'Feature', - id: `${last.id}_${feature.id}`, + id: `${+last.id}__${+feature.id}`, properties: { - start: last.id, - end: feature.id, + __start: +last.id, + __end: +feature.id, }, geometry: { type: 'LineString', @@ -199,25 +204,30 @@ export function Drawing() { }, } if (Object.keys(useShapes.getState().Point).length > 1) { - newState[`${feature.id}_${first.id}`] = { - type: 'Feature', - id: `${feature.id}_${first.id}`, - properties: { - start: feature.id, - end: first.id, - }, - geometry: { - type: 'LineString', - coordinates: [ - feature.geometry.coordinates, - first.geometry.coordinates, - ], - }, + if ( + typeof feature.id === 'number' && + typeof first.id === 'number' + ) { + newState[`${feature.id}__${first.id}`] = { + type: 'Feature', + id: `${feature.id}__${first.id}`, + properties: { + __start: feature.id, + __end: first.id, + }, + geometry: { + type: 'LineString', + coordinates: [ + feature.geometry.coordinates, + first.geometry.coordinates, + ], + }, + } } } return newState }) - setters.remove('LineString', `${last.id}_${first.id}`) + setters.remove('LineString', `${last.id}__${first.id}`) } setShapes('Point', (prev) => ({ ...prev, @@ -226,7 +236,7 @@ export function Drawing() { if (last) { setShapes('Point', (prev) => ({ ...prev, - [last?.id as number]: last, + [last?.id]: last, })) } if (!first) { @@ -234,7 +244,7 @@ export function Drawing() { } else { setShapes('Point', (prev) => ({ ...prev, - [first?.id as number]: first, + [first?.id]: first, })) } setShapes('lastPoint', id) diff --git a/client/src/pages/map/interface/Locate.tsx b/client/src/pages/map/interface/Locate.tsx index 171131e8..8776d516 100644 --- a/client/src/pages/map/interface/Locate.tsx +++ b/client/src/pages/map/interface/Locate.tsx @@ -16,6 +16,9 @@ export default function Locate() { React.useEffect(() => { lc.addTo(map) + return () => { + lc.remove() + } }, []) return null diff --git a/client/src/pages/map/interface/index.tsx b/client/src/pages/map/interface/index.tsx index c5ece966..6d4b9133 100644 --- a/client/src/pages/map/interface/index.tsx +++ b/client/src/pages/map/interface/index.tsx @@ -12,15 +12,17 @@ import Locate from './Locate' import MemoizedDrawing from './Drawing' import EasyButton from './EasyButton' +const { isEditing } = useStatic.getState() + export default function Interface() { + useLayers() + usePopupStyle() + useSyncGeojson() const navigate = useNavigate() const map = useMapEvents({ popupopen(e) { - const isEditing = Object.values(useStatic.getState().layerEditing).some( - (v) => v, - ) - if (isEditing || useStatic.getState().combinePolyMode) { + if (isEditing() || useStatic.getState().combinePolyMode) { e.popup.close() } }, @@ -40,10 +42,6 @@ export default function Interface() { } }, [onMove]) - useLayers() - usePopupStyle() - useSyncGeojson() - return ( <> diff --git a/client/src/pages/map/markers/LineString.tsx b/client/src/pages/map/markers/LineString.tsx index dfeab8d9..88c59797 100644 --- a/client/src/pages/map/markers/LineString.tsx +++ b/client/src/pages/map/markers/LineString.tsx @@ -1,9 +1,10 @@ /* eslint-disable react/destructuring-assignment */ import * as React from 'react' import { Polyline } from 'react-leaflet' -import type { Feature, LineString } from 'geojson' +import type { LineString } from 'geojson' import distance from '@turf/distance' +import type { Feature } from '@assets/types' import { getColor } from '@services/utils' import { MemoLinePopup } from '../popups/LineString' diff --git a/client/src/pages/map/markers/MultiLineString.tsx b/client/src/pages/map/markers/MultiLineString.tsx index 6796307e..a11d130d 100644 --- a/client/src/pages/map/markers/MultiLineString.tsx +++ b/client/src/pages/map/markers/MultiLineString.tsx @@ -1,6 +1,7 @@ /* eslint-disable react/destructuring-assignment */ import * as React from 'react' -import type { Feature, MultiLineString } from 'geojson' +import type { MultiLineString } from 'geojson' +import type { Feature } from '@assets/types' import { MemoLineString } from './LineString' @@ -11,15 +12,18 @@ export function KojiMultiLineString({ }) { return ( <> - {feature.geometry.coordinates.map((coords) => ( - - ))} + {feature.geometry.coordinates.map((coords, i) => { + return ( + + ) + })} ) } diff --git a/client/src/pages/map/markers/MultiPoint.tsx b/client/src/pages/map/markers/MultiPoint.tsx index 0cb83273..863d3c2a 100644 --- a/client/src/pages/map/markers/MultiPoint.tsx +++ b/client/src/pages/map/markers/MultiPoint.tsx @@ -1,5 +1,7 @@ import * as React from 'react' -import type { Feature, MultiPoint } from 'geojson' +import type { MultiPoint } from 'geojson' + +import type { Feature, DbOption, KojiKey } from '@assets/types' import { MemoPoint } from './Point' import { MemoLineString } from './LineString' @@ -11,9 +13,11 @@ export function KojiMultiPoint({ geometry: { coordinates }, }, radius, + dbRef, }: { feature: Feature radius: number + dbRef: DbOption | null }) { return ( <> @@ -29,18 +33,22 @@ export function KojiMultiPoint({ radius={radius} feature={{ type: 'Feature', - id: `${id}___${i}`, - properties, + id: i, + properties: { + ...properties, + __multipoint_id: id as KojiKey, + }, geometry: { coordinates: first, type: 'Point' }, }} type="MultiPoint" + dbRef={dbRef} /> diff --git a/client/src/pages/map/markers/Point.tsx b/client/src/pages/map/markers/Point.tsx index a317f15e..4d2e7e24 100644 --- a/client/src/pages/map/markers/Point.tsx +++ b/client/src/pages/map/markers/Point.tsx @@ -1,8 +1,9 @@ import * as React from 'react' -import type { Feature, Point } from 'geojson' +import type { Point } from 'geojson' import { Circle } from 'react-leaflet' import * as L from 'leaflet' +import type { Feature, DbOption, KojiKey } from '@assets/types' import { useShapes } from '@hooks/useShapes' import { useStatic } from '@hooks/useStatic' import { usePersist } from '@hooks/usePersist' @@ -10,6 +11,8 @@ import { usePersist } from '@hooks/usePersist' import BasePopup from '../popups/Styled' import { MemoPointPopup } from '../popups/Point' +const { isEditing } = useStatic.getState() + export function KojiPoint({ feature: { id, @@ -20,15 +23,19 @@ export function KojiPoint({ }, radius, type = 'Point', + dbRef, }: { feature: Feature radius: number type?: 'Point' | 'MultiPoint' + dbRef: DbOption | null + parentId?: KojiKey }) { return ( { if (circle && id !== undefined) { + if (circle.isPopupOpen()) circle.closePopup() circle.removeEventListener('pm:remove') circle.on('pm:remove', function remove() { useShapes.getState().setters.remove(type, id) @@ -47,17 +54,21 @@ export function KojiPoint({ circle.removeEventListener('mouseover') circle.on('mouseover', function onClick() { if ( - typeof id === 'string' && type === 'MultiPoint' && - !Object.values(useStatic.getState().layerEditing).some((v) => v) + properties?.__multipoint_id && + !isEditing() ) { - useShapes.getState().setters.activeRoute(id.split('___')[0]) + useShapes + .getState() + .setters.activeRoute(properties?.__multipoint_id) } }) } else { circle.on('click', function onClick() { - if (typeof id === 'string' && type === 'MultiPoint') { - useShapes.getState().setters.activeRoute(id.split('___')[0]) + if (type === 'MultiPoint' && properties?.__multipoint_id) { + useShapes + .getState() + .setters.activeRoute(properties?.__multipoint_id) } }) } @@ -78,6 +89,7 @@ export function KojiPoint({ lat={lat} lon={lon} type={type} + dbRef={dbRef} /> diff --git a/client/src/pages/map/markers/Polygon.tsx b/client/src/pages/map/markers/Polygon.tsx index e9986508..51e19c7a 100644 --- a/client/src/pages/map/markers/Polygon.tsx +++ b/client/src/pages/map/markers/Polygon.tsx @@ -1,19 +1,24 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Feature, Polygon as PolygonType, MultiPolygon } from 'geojson' +import type { Polygon as PolygonType, MultiPolygon } from 'geojson' import * as React from 'react' import { Polygon } from 'react-leaflet' import * as L from 'leaflet' +import { Feature, DbOption } from '@assets/types' import { useShapes } from '@hooks/useShapes' import { useStatic } from '@hooks/useStatic' import { MemoPolyPopup } from '../popups/Polygon' import Popup from '../popups/Styled' +const { isEditing, setStatic } = useStatic.getState() + export function KojiPolygon({ feature, + dbRef, }: { feature: Feature | Feature + dbRef: DbOption | null }) { const [loadData, setLoadData] = React.useState(false) @@ -27,11 +32,11 @@ export function KojiPolygon({ ref.addOneTimeEventListener('click', () => setLoadData(true)) ref.addEventListener('click', ({ latlng }) => { const { lat, lng } = latlng - useStatic.getState().setStatic('clickedLocation', [lng, lat]) + setStatic('clickedLocation', [lng, lat]) }) if (!ref.hasEventListeners('mouseover')) { ref.on('mouseover', function mouseOver() { - if (!useStatic.getState().combinePolyMode) { + if (!useStatic.getState().combinePolyMode && !isEditing()) { ref.setStyle({ color: 'red' }) // ref.bringToFront() } @@ -87,7 +92,7 @@ export function KojiPolygon({ id: feature.id, properties: { ...feature.properties, - leafletId: layer._leaflet_id, + __leafletId: layer._leaflet_id, }, } as any) // TODO: fix this originalLayer.remove() @@ -137,7 +142,7 @@ export function KojiPolygon({ pane="polygons" > - + ) diff --git a/client/src/pages/map/markers/Vectors.tsx b/client/src/pages/map/markers/Vectors.tsx index 7c992ddc..1ab8f8a5 100644 --- a/client/src/pages/map/markers/Vectors.tsx +++ b/client/src/pages/map/markers/Vectors.tsx @@ -1,25 +1,31 @@ import * as React from 'react' -import { usePersist } from '@hooks/usePersist' import shallow from 'zustand/shallow' -import type { Feature, GeometryCollection } from 'geojson' import { useShapes } from '@hooks/useShapes' +import { usePersist } from '@hooks/usePersist' +import { useDbCache } from '@hooks/useDbCache' import { MemoPoint } from './Point' import { KojiLineString } from './LineString' import { MemoPolygon } from './Polygon' -import { KojiMultiPoint, MemoMultiPoint } from './MultiPoint' +import { MemoMultiPoint } from './MultiPoint' import { MemoMultiLineString } from './MultiLineString' export function Points() { const shapes = useShapes((s) => s.Point) const radius = usePersist((s) => s.radius) const setActiveMode = usePersist((s) => s.setActiveMode) + const { getFromKojiKey } = useDbCache.getState() return ( {Object.entries(shapes).map(([id, feature]) => ( - + ))} ) @@ -29,11 +35,17 @@ export function MultiPoints() { const shapes = useShapes((s) => s.MultiPoint) const radius = usePersist((s) => s.radius) const setActiveMode = usePersist((s) => s.setActiveMode) + const { getFromKojiKey } = useDbCache.getState() return ( {Object.entries(shapes).map(([id, feature]) => ( - + ))} ) @@ -68,130 +80,131 @@ export function Polygons() { (s) => ({ ...s.Polygon, ...s.MultiPolygon }), shallow, ) + const { getFromKojiKey } = useDbCache.getState() return ( <> {Object.entries(shapes).map(([id, feature]) => ( - + ))} ) } -interface Props { - id: Feature['id'] - feature: Feature - radius?: number -} - -export function GeometryFeature({ id, feature, radius }: Props) { - return ( - <> - {feature.geometry.geometries.map((geometry, i) => { - switch (geometry.type) { - case 'Point': - return ( - - ) - case 'MultiPoint': - return ( - - ) - case 'LineString': - return ( - - ) - case 'MultiLineString': - return ( - - ) - case 'Polygon': - return ( - - ) - case 'MultiPolygon': - return ( - - ) - case 'GeometryCollection': - return ( - - ) - default: - return null - } - })} - - ) -} - -export function GeometryCollections() { - const shapes = useShapes((s) => s.GeometryCollection) - const radius = usePersist((s) => s.radius) - - return ( - <> - {Object.entries(shapes).map(([id, feature]) => ( - - ))} - - ) -} +// interface Props { +// id: Feature['id'] +// feature: Feature +// radius?: number +// } + +// export function GeometryFeature({ id, feature, radius }: Props) { +// return ( +// <> +// {feature.geometry.geometries.map((geometry, i) => { +// switch (geometry.type) { +// case 'Point': +// return ( +// +// ) +// case 'MultiPoint': +// return ( +// +// ) +// case 'LineString': +// return ( +// +// ) +// case 'MultiLineString': +// return ( +// +// ) +// case 'Polygon': +// return ( +// +// ) +// case 'MultiPolygon': +// return ( +// +// ) +// case 'GeometryCollection': +// return ( +// +// ) +// default: +// return null +// } +// })} +// +// ) +// } + +// export function GeometryCollections() { +// const shapes = useShapes((s) => s.GeometryCollection) +// const radius = usePersist((s) => s.radius) + +// return ( +// <> +// {Object.entries(shapes).map(([id, feature]) => ( +// +// ))} +// +// ) +// } diff --git a/client/src/pages/map/markers/index.tsx b/client/src/pages/map/markers/index.tsx index adc0d77f..cd29e8ae 100644 --- a/client/src/pages/map/markers/index.tsx +++ b/client/src/pages/map/markers/index.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect } from 'react' import { Circle, Popup, useMap } from 'react-leaflet' import geohash from 'ngeohash' @@ -25,40 +25,60 @@ export default function Markers({ const geojson = useStatic((s) => s.geojson) const [markers, setMarkers] = React.useState([]) + const [focused, setFocused] = React.useState(true) + const map = useMap() + const memoSetFocused = React.useCallback( + () => setFocused(document.hasFocus()), + [], + ) + usePixi(nativeLeaflet ? [] : markers) useDeepCompareEffect(() => { - const controller = new AbortController() - if (enabled && (data === 'area' ? geojson.features.length : true)) { - getMarkers( - category, - getMapBounds(map), - data, - { - ...geojson, - features: geojson.features.filter( - (feature) => - feature.geometry.type === 'Polygon' || - feature.geometry.type === 'MultiPolygon', - ), - }, - last_seen, - controller.signal, - ).then((res) => { - if (res.length) setMarkers(res) - }) + if (focused) { + const controller = new AbortController() + const filtered = geojson.features.filter((feature) => + feature.geometry.type.includes('Polygon'), + ) + if (enabled && (data === 'area' ? filtered.length : true)) { + getMarkers( + category, + getMapBounds(map), + data, + { + ...geojson, + features: filtered, + }, + last_seen, + controller.signal, + ).then((res) => { + if (res.length && res.length !== markers.length) setMarkers(res) + }) + } else { + setMarkers([]) + } + return () => controller.abort() } - return () => controller.abort() }, [ data, data === 'area' ? geojson : {}, data === 'bound' ? location : {}, enabled, last_seen, + focused, ]) + useEffect(() => { + window.addEventListener('focus', memoSetFocused) + window.addEventListener('blur', memoSetFocused) + return () => { + window.removeEventListener('focus', memoSetFocused) + window.removeEventListener('blur', memoSetFocused) + } + }, [memoSetFocused]) + return nativeLeaflet ? ( <> {markers.map((i) => ( diff --git a/client/src/pages/map/popups/LineString.tsx b/client/src/pages/map/popups/LineString.tsx index 467000f2..6993e787 100644 --- a/client/src/pages/map/popups/LineString.tsx +++ b/client/src/pages/map/popups/LineString.tsx @@ -3,7 +3,7 @@ import type { PopupProps } from '@assets/types' import { Button, Typography } from '@mui/material' import { useShapes } from '@hooks/useShapes' -interface Props extends PopupProps { +interface Props extends Omit { dis: number } diff --git a/client/src/pages/map/popups/Point.tsx b/client/src/pages/map/popups/Point.tsx index ef81dec9..4dc50f83 100644 --- a/client/src/pages/map/popups/Point.tsx +++ b/client/src/pages/map/popups/Point.tsx @@ -10,34 +10,39 @@ import { import ChevronLeft from '@mui/icons-material/ChevronLeft' import Add from '@mui/icons-material/Add' import geohash from 'ngeohash' -import type { Feature, MultiPoint } from 'geojson' +import type { MultiPoint } from 'geojson' -import { KojiResponse, KojiRoute, Option, PopupProps } from '@assets/types' +import { Feature, KojiResponse, KojiRoute, PopupProps } from '@assets/types' import ExportRoute from '@components/dialogs/ExportRoute' import { useShapes } from '@hooks/useShapes' import Grid2 from '@mui/material/Unstable_Grid2/Grid2' import { RDM_ROUTES, UNOWN_ROUTES } from '@assets/constants' import { useStatic } from '@hooks/useStatic' -import { getData } from '@services/fetches' +import { fetchWrapper, getKojiCache } from '@services/fetches' +import { useDbCache } from '@hooks/useDbCache' + +const { add, remove, splitLine, activeRoute, updateProperty } = + useShapes.getState().setters interface Props extends PopupProps { - id: Feature['id'] lat: number lon: number type: 'Point' | 'MultiPoint' } -export function PointPopup({ id, lat, lon, type: geoType }: Props) { +export function PointPopup({ id, lat, lon, type: geoType, dbRef }: Props) { const [open, setOpen] = React.useState('') - const feature = useShapes((s) => s[geoType][id as number | string]) - const { add, remove, splitLine } = useShapes.getState().setters + const feature = useShapes((s) => s[geoType][id]) + const { setRecord, geofence } = useDbCache.getState() - const [name, setName] = React.useState(feature.properties?.__name || '') - const [type, setType] = React.useState(feature.properties?.__type || '') - const [fenceId, setFenceId] = React.useState( - feature.properties?.__geofence_id || 0, + const [name, setName] = React.useState( + dbRef?.name || feature.properties?.__name || '', + ) + const [mode, setMode] = React.useState( + dbRef?.mode || feature.properties?.__mode || '', ) - const options = Object.values(useShapes.getState().kojiRefCache) + const [fenceId, setFenceId] = React.useState(dbRef?.geofence_id || 0) + const options = Object.values(geofence) const [loading, setLoading] = React.useState(false) @@ -46,6 +51,8 @@ export function PointPopup({ id, lat, lon, type: geoType }: Props) { ? remove(feature.geometry.type, feature.id) : remove('Point') + const isInKoji = feature.properties?.__multipoint_id?.endsWith('KOJI') + return id !== undefined ? (
Lat: {lat.toFixed(6)} @@ -71,14 +78,20 @@ export function PointPopup({ id, lat, lon, type: geoType }: Props) { fullWidth value={name} onChange={({ target }) => setName(target.value)} + onBlur={() => + updateProperty(feature.geometry.type, feature.id, '__name', name) + } /> setType(target.value)} + value={mode === 'Unset' ? '' : mode || ''} + onChange={async ({ target }) => setMode(target.value as KojiModes)} + onBlur={() => + updateProperty(feature.geometry.type, feature.id, '__mode', mode) + } > {(useStatic.getState().scannerType === 'rdm' ? RDM_FENCES @@ -251,19 +279,19 @@ export function PolygonPopup({ { - getData>( - feature.properties?.__koji_id - ? `/internal/admin/geofence/${feature.properties?.__koji_id}/` + fetchWrapper>( + isKoji + ? `/internal/admin/geofence/${dbRef?.id}/` : '/internal/admin/geofence/', { - method: feature.properties?.__koji_id ? 'PATCH' : 'POST', + method: isKoji ? 'PATCH' : 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ - id: feature.properties?.__koji_id || 0, + id: isKoji ? dbRef?.id : 0, name, - mode: type, + mode, area: feature, updated_at: new Date(), created_at: new Date(), @@ -272,46 +300,80 @@ export function PolygonPopup({ ).then((res) => { if (res) { useStatic.setState({ - networkError: { + networkStatus: { message: 'Saved successfully!', status: 200, severity: 'success', }, }) - const newFeature = { - ...res.data.area, - properties: { - ...res.data.area.properties, - __name: res.data.name, - __type: res.data.mode, - __koji_id: res.data.id, - }, - } + const { area, mode: newMode = 'Unset', ...rest } = res.data + const newId = `${rest.id}__${newMode}__KOJI` as const + area.id = newId + setRecord('geofence', rest.id, { + ...rest, + mode: newMode, + geo_type: area.geometry.type, + }) remove(feature.geometry.type, feature.id) - add(newFeature, '__KOJI') + add( + { + ...area, + properties: { ...area.properties, ...feature.properties }, + }, + '__KOJI', + ) } handleClose() }) }} > - {feature.properties?.__koji_id ? 'Save' : 'Create'} + {isKoji ? 'Update Kōji' : 'Save to Kōji'} { remove(feature.geometry.type, feature.id) - await getData( - `/internal/admin/geofence/${feature.properties?.__koji_id}/`, - { - method: 'DELETE', - }, - ).then(() => { + await fetchWrapper(`/internal/admin/geofence/${dbRef?.id}/`, { + method: 'DELETE', + }).then(() => { handleClose() }) }} > - Delete + Delete from Kōji + {/* { + await save( + '/api/v1/geofence/save-scanner', + JSON.stringify({ + ...feature, + properties: { + ...feature.properties, + __id: isScanner ? dbRef?.id : undefined, + }, + }), + ).then((res) => { + if (res) { + if (dbRef && mode) { + setRecord('scanner', feature.id as KojiKey, { + ...dbRef, + mode, + geo_type: feature.geometry.type, + }) + setRecord('feature', feature.id as KojiKey, feature) + } + remove(refFeature.geometry.type, feature.id) + add(feature) + } + + handleClose() + }) + }} + > + Update Scanner + */} {open && ( diff --git a/client/src/services/fetches.ts b/client/src/services/fetches.ts index 385343f3..db6bc859 100644 --- a/client/src/services/fetches.ts +++ b/client/src/services/fetches.ts @@ -1,37 +1,35 @@ /* eslint-disable no-console */ /* eslint-disable no-nested-ternary */ -import type { Feature, FeatureCollection } from 'geojson' - -import type { CombinedState, PixiMarker, ToConvert } from '@assets/types' +import type { + KojiResponse, + PixiMarker, + Conversions, + Feature, + FeatureCollection, + DbOption, +} from '@assets/types' import { UsePersist, usePersist } from '@hooks/usePersist' import { UseStatic, useStatic } from '@hooks/useStatic' import { useShapes } from '@hooks/useShapes' +import { UseDbCache, useDbCache } from '@hooks/useDbCache' import { fromSnakeCase, getMapBounds } from './utils' -export async function getData( +export async function fetchWrapper( url: string, options: RequestInit = {}, - settings: CombinedState & { area?: Feature } = {}, ): Promise { try { - const res = Object.keys(settings).length - ? await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(settings), - }) - : await fetch(url, options) + const res = await fetch(url, options) if (!res.ok) { useStatic.setState({ - networkError: { + networkStatus: { message: await res.text(), status: res.status, severity: 'error', }, }) + return null } return await res.json() } catch (e) { @@ -40,6 +38,73 @@ export async function getData( } } +export async function getKojiCache( + resource: T, +): Promise { + const res = await fetch(`/internal/admin/${resource}/ref/`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }) + if (!res.ok) { + useStatic.setState({ + networkStatus: { + message: await res.text(), + status: res.status, + severity: 'error', + }, + }) + return null + } + const { data }: KojiResponse = await res.json() + const asObject = Object.fromEntries(data.map((d) => [d.id, d])) + useDbCache.setState({ [resource]: asObject }) + console.log( + 'Cache set:', + resource, + process.env.NODE_ENV === 'development' ? data : data.length, + ) + return asObject +} + +export async function refreshKojiCache() { + await Promise.allSettled([ + getKojiCache('geofence'), + getKojiCache('project'), + getKojiCache('route'), + ]) +} + +export async function getScannerCache() { + return fetchWrapper>( + '/internal/routes/from_scanner', + ).then((res) => { + if (res) { + const asObject = Object.fromEntries( + res.data.map((t) => [`${t.id}__${t.mode}__SCANNER`, t]), + ) + useDbCache.setState({ + scanner: asObject, + }) + console.log( + 'Cache set:', + 'scanner', + process.env.NODE_ENV === 'development' ? res.data : res.data.length, + ) + return asObject + } + }) +} + +export async function getFullCache() { + Promise.all( + (['geofence', 'route', 'project', 'scanner'] as const).map((resource) => + resource === 'scanner' ? getScannerCache() : getKojiCache(resource), + ), + ) +} + export async function clusteringRouting(): Promise { const { mode, @@ -50,23 +115,24 @@ export async function clusteringRouting(): Promise { routing_time, only_unique, save_to_db, + save_to_scanner, + skipRendering, last_seen, route_chunk_size, } = usePersist.getState() const { geojson, setStatic } = useStatic.getState() const { add, activeRoute } = useShapes.getState().setters + const { getFromKojiKey, getRouteByCategory } = useDbCache.getState() + activeRoute('layer_1') setStatic( 'loading', Object.fromEntries( geojson.features - .filter( - (feat) => - feat.geometry.type === 'Polygon' || - feat.geometry.type === 'MultiPolygon', - ) + .filter((feat) => feat.geometry.type.includes('Polygon')) .map((k) => [ - k.properties?.__name || `${k.geometry.type}${k.id ? `-${k.id}` : ''}`, + getFromKojiKey(k.id as string)?.name || + `${k.geometry.type}${k.id ? `-${k.id}` : ''}`, null, ]), ), @@ -76,11 +142,10 @@ export async function clusteringRouting(): Promise { const totalStartTime = Date.now() const features = await Promise.allSettled( (geojson?.features || []) - .filter( - (x) => - x.geometry.type === 'Polygon' || x.geometry.type === 'MultiPolygon', - ) + .filter((x) => x.geometry.type.includes('Polygon')) .map(async (area) => { + const fenceRef = getFromKojiKey(area.id as string) + const routeRef = getRouteByCategory(category, fenceRef?.name) const startTime = Date.now() const res = await fetch( mode === 'bootstrap' @@ -93,9 +158,17 @@ export async function clusteringRouting(): Promise { }, body: JSON.stringify({ return_type: 'feature', - area, + area: { + ...area, + properties: { + __id: routeRef?.id, + __name: fenceRef?.name, + __geofence_id: fenceRef?.id, + __mode: fenceRef?.mode, + }, + }, instance: - area.properties?.__name || + fenceRef?.name || `${area.geometry.type}${area.id ? `-${area.id}` : ''}`, route_chunk_size, last_seen: Math.floor((last_seen?.getTime?.() || 0) / 1000), @@ -105,17 +178,20 @@ export async function clusteringRouting(): Promise { routing_time, only_unique, save_to_db, + save_to_scanner, }), }, ) if (!res.ok) { - setStatic('loading', (prev) => ({ - ...prev, - [area.properties?.__name || - `${area.geometry.type}${area.id ? `-${area.id}` : ''}`]: false, - })) + if (fenceRef?.name) { + setStatic('loading', (prev) => ({ + ...prev, + [fenceRef?.name || + `${area.geometry.type}${area.id ? `-${area.id}` : ''}`]: false, + })) + } useStatic.setState({ - networkError: { + networkStatus: { message: await res.text(), status: res.status, severity: 'error', @@ -125,27 +201,23 @@ export async function clusteringRouting(): Promise { } const json = await res.json() const fetch_time = Date.now() - startTime - setStatic('loading', (prev) => ({ - ...prev, - [json.data?.properties?.__name]: { - ...json.stats, - fetch_time, - }, - })) - console.log(area.properties?.__name) + if (fenceRef?.name) { + setStatic('loading', (prev) => ({ + ...prev, + [fenceRef?.name]: { + ...json.stats, + fetch_time, + }, + })) + } + console.log(fenceRef?.name) Object.entries(json.stats).forEach(([k, v]) => // eslint-disable-next-line no-console console.log(fromSnakeCase(k), v), ) console.log(`Total Time: ${fetch_time / 1000}s\n`) console.log('-----------------') - return { - ...json.data, - properties: { - ...json.data.properties, - __geofence_id: area.properties?.__koji_id, - }, - } + return json.data }), ).then((feats) => feats @@ -157,8 +229,9 @@ export async function clusteringRouting(): Promise { ) setStatic('totalLoadingTime', Date.now() - totalStartTime) - add(features) - + if (!skipRendering) add(features) + if (save_to_db) await getKojiCache('route') + if (save_to_scanner) await getScannerCache() return { type: 'FeatureCollection', features, @@ -196,10 +269,11 @@ export async function getMarkers( 408: 'Check CloudFlare or Nginx/Apache Settings', 413: 'Check CloudFlare or Nginx/Apache Settings', 500: 'Refresh the page, resetting the Kōji server, or contacting the developer', + 524: 'Check CloudFlare or Nginx/Apache Timeout Settings', }[res.status] || '' useStatic.setState({ - networkError: { + networkStatus: { message, status: res.status, severity: 'error', @@ -218,10 +292,11 @@ export async function getMarkers( } } -export async function convert( - area: ToConvert, +export async function convert( + area: Conversions, return_type: UsePersist['polygonExportMode'], simplify: UsePersist['simplifyPolygons'], + geometry_type?: UsePersist['geometryType'], url = '/api/v1/convert/data', ): Promise { try { @@ -234,11 +309,12 @@ export async function convert( area, return_type, simplify, + geometry_type, }), }) if (!res.ok) { useStatic.setState({ - networkError: { + networkStatus: { message: await res.text(), status: res.status, severity: 'error', @@ -264,7 +340,7 @@ export async function save(url: string, code: string): Promise { }) if (!res.ok) { useStatic.setState({ - networkError: { + networkStatus: { message: await res.text(), status: res.status, severity: 'error', @@ -272,9 +348,11 @@ export async function save(url: string, code: string): Promise { }) throw new Error('Unable to save') } + const json: KojiResponse<{ updates: number; inserts: number }> = + await res.json() useStatic.setState({ - networkError: { - message: 'Saved successfully!', + networkStatus: { + message: `Created ${json.data.inserts} and updated ${json.data.updates}}`, status: res.status, severity: 'success', }, diff --git a/client/src/services/utils.ts b/client/src/services/utils.ts index a393c3f7..737372a9 100644 --- a/client/src/services/utils.ts +++ b/client/src/services/utils.ts @@ -1,11 +1,12 @@ import * as L from 'leaflet' import { capitalize } from '@mui/material' -import type { Feature, FeatureCollection, MultiPolygon, Polygon } from 'geojson' +import type { MultiPoint, MultiPolygon, Point, Polygon } from 'geojson' import union from '@turf/union' import bbox from '@turf/bbox' import { useStatic } from '@hooks/useStatic' import booleanPointInPolygon from '@turf/boolean-point-in-polygon' import { useShapes } from '@hooks/useShapes' +import { Feature, FeatureCollection } from '@assets/types' export function getMapBounds(map: L.Map) { const mapBounds = map.getBounds() @@ -57,7 +58,7 @@ export function safeParse(value: string): { export function collectionToObject(collection: FeatureCollection) { return Object.fromEntries( collection.features.map((feat) => [ - `${feat.properties?.__name}_${feat.properties?.__type}`, + `${feat.properties?.__name}__${feat.properties?.__mode}`, feat, ]), ) @@ -91,7 +92,7 @@ export function combineByProperty( if (merged) { featureHash[name] = { ...existing, - ...merged, + ...(merged as Feature), properties: { ...existing.properties, ...feat.properties, @@ -123,14 +124,16 @@ export function splitMultiPolygons( coordinates.forEach((polygon, i) => { features.push({ ...feature, - id: `${feature.id}_${i}`, + id: `${ + coordinates.length === 1 ? feature.properties.__id || i : i + }__${feature.properties?.__mode}__CLIENT`, properties: { ...feature.properties, - __koji_id: undefined, + __id: undefined, __name: coordinates.length === 1 ? feature.properties?.__name || '' - : `${feature.properties?.__name}_${i}`, + : `${feature.properties?.__name}__${i}`, }, geometry: { ...feature.geometry, @@ -200,3 +203,21 @@ export function removeAllOthers(feature: Feature) { export function getKey() { return Math.random().toString(36).substring(2, 10) } + +export function mpToPoints(geometry: MultiPoint): Feature[] { + return (geometry.coordinates || []).map((c, i) => { + return { + id: i, + type: 'Feature', + geometry: { + type: 'Point', + coordinates: c, + }, + properties: { + next: geometry.coordinates[ + i === geometry.coordinates.length - 1 ? 0 : i + 1 + ], + }, + } + }) +} diff --git a/server/Cargo.lock b/server/Cargo.lock index 4d9c6535..aaf876a9 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -59,7 +59,7 @@ dependencies = [ "actix-service", "actix-utils", "ahash", - "base64", + "base64 0.13.1", "bitflags", "brotli", "bytes", @@ -235,7 +235,7 @@ checksum = "6dda62cf04bc3a9ad2ea8f314f721951cfdb4cdacec4e984d20e77c7bb170991" dependencies = [ "actix-utils", "actix-web", - "base64", + "base64 0.13.1", "futures-core", "futures-util", "log", @@ -305,7 +305,7 @@ dependencies = [ [[package]] name = "algorithms" -version = "0.1.0" +version = "0.4.0" dependencies = [ "geo", "geohash", @@ -355,24 +355,24 @@ checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" [[package]] name = "api" -version = "0.1.0" +version = "0.4.0" dependencies = [ "actix-files", "actix-session", "actix-web", "actix-web-httpauth", "algorithms", - "dotenv", - "env_logger", "geo", "geojson", "log", "migration", "model", "nominatim", + "reqwest", "sea-orm", "serde", "serde_json", + "thiserror", "url", ] @@ -603,6 +603,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + [[package]] name = "base64ct" version = "1.5.3" @@ -870,7 +876,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "344adc371239ef32293cb1c4fe519592fcf21206c79c02854320afcdf3ab4917" dependencies = [ "aes-gcm", - "base64", + "base64 0.13.1", "hkdf", "hmac", "percent-encoding", @@ -1439,7 +1445,7 @@ dependencies = [ [[package]] name = "geo_repair" -version = "0.1.0" +version = "0.4.0" dependencies = [ "byteorder", "geo", @@ -1824,9 +1830,12 @@ dependencies = [ [[package]] name = "koji" -version = "0.3.5" +version = "0.4.0" dependencies = [ "api", + "dotenv", + "env_logger", + "log", ] [[package]] @@ -1960,7 +1969,7 @@ dependencies = [ [[package]] name = "migration" -version = "0.1.0" +version = "0.4.0" dependencies = [ "async-std", "sea-orm-migration", @@ -2011,18 +2020,20 @@ dependencies = [ [[package]] name = "model" -version = "0.1.0" +version = "0.4.0" dependencies = [ "chrono", "futures", "geo", "geo_repair", "geojson", + "log", "num-traits", "sea-orm", "serde", "serde_json", "serde_with", + "thiserror", ] [[package]] @@ -2055,7 +2066,7 @@ dependencies = [ [[package]] name = "nominatim" -version = "0.1.0" +version = "0.4.0" dependencies = [ "derive_builder", "geojson", @@ -2385,7 +2396,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "878c6cbf956e03af9aa8204b407b9cbf47c072164800aa918c516cd4b056c50c" dependencies = [ - "base64", + "base64 0.13.1", "byteorder", "bytes", "fallible-iterator", @@ -2575,11 +2586,11 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" +checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9" dependencies = [ - "base64", + "base64 0.21.0", "bytes", "encoding_rs", "futures-core", @@ -2975,7 +2986,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25bf4a5a814902cd1014dbccfa4d4560fb8432c779471e96e035602519f82eef" dependencies = [ - "base64", + "base64 0.13.1", "chrono", "hex", "indexmap", @@ -3262,18 +3273,18 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", diff --git a/server/Cargo.toml b/server/Cargo.toml index c653497e..1c9a4670 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "koji" -version = "0.3.5" +version = "0.4.0" edition = "2021" [workspace] @@ -16,3 +16,6 @@ members = [ [dependencies] api = { path = "api" } +dotenv = "0.15.0" +env_logger = "0.9.0" +log = "0.4.17" diff --git a/server/algorithms/Cargo.toml b/server/algorithms/Cargo.toml index f0b8e00d..914de301 100644 --- a/server/algorithms/Cargo.toml +++ b/server/algorithms/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "algorithms" -version = "0.1.0" +version = "0.4.0" edition = "2021" publish = false diff --git a/server/algorithms/src/bootstrapping.rs b/server/algorithms/src/bootstrapping.rs index b05df302..ccc7b607 100644 --- a/server/algorithms/src/bootstrapping.rs +++ b/server/algorithms/src/bootstrapping.rs @@ -100,12 +100,12 @@ pub fn as_geojson(feature: Feature, radius: f64, stats: &mut Stats) -> Feature { new_feature.set_property("__name", name); } } - if let Some(geofence_id) = feature.property("__koji_id") { + if let Some(geofence_id) = feature.property("__id") { if let Some(geofence_id) = geofence_id.as_str() { new_feature.set_property("__geofence_id", geofence_id); } } - new_feature.set_property("__type", "CirclePokemon"); + new_feature.set_property("__mode", "CirclePokemon"); new_feature.bbox = feature.to_single_vec().get_bbox(); new_feature } diff --git a/server/algorithms/src/routing/tsp.rs b/server/algorithms/src/routing/tsp.rs index 57060f1c..d0372950 100644 --- a/server/algorithms/src/routing/tsp.rs +++ b/server/algorithms/src/routing/tsp.rs @@ -15,7 +15,7 @@ pub fn base(segment: SingleVec, routing_time: i64, fast: bool) -> Tour { routing_time } else { ((segment.len() as f32 / 100.) + 1.) - .powf(if fast { 1. } else { 1.25 }) + .powf(if fast { 1. } else { 1.5 }) .floor() as i64 }), ) diff --git a/server/api/Cargo.toml b/server/api/Cargo.toml index 43b38ebe..429eaea1 100644 --- a/server/api/Cargo.toml +++ b/server/api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "api" -version = "0.1.0" +version = "0.4.0" edition = "2021" publish = false @@ -14,14 +14,13 @@ actix-files = "0.6.0" actix-session = { version = "0.7.2", features = ["cookie-session"] } actix-web-httpauth = "0.8.0" algorithms = { path = "../algorithms" } -dotenv = "0.15.0" -env_logger = "0.9.0" geo = "0.22.1" geojson = "0.24.0" log = "0.4.17" migration = { path = "../migration" } model = { path = "../model" } nominatim = { path = "../nominatim" } +reqwest = "0.11.14" sea-orm = { version = "0.9.3", features = [ "sqlx-mysql", "runtime-actix-native-tls", @@ -29,4 +28,5 @@ sea-orm = { version = "0.9.3", features = [ ] } serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0" +thiserror = "1.0.38" url = "2.3.1" diff --git a/server/api/src/lib.rs b/server/api/src/lib.rs index e59ae90c..62c80684 100644 --- a/server/api/src/lib.rs +++ b/server/api/src/lib.rs @@ -24,14 +24,7 @@ mod public; mod utils; #[actix_web::main] -pub async fn main() -> io::Result<()> { - dotenv::from_filename(env::var("ENV").unwrap_or(".env".to_string())).ok(); - // error | warn | info | debug | trace - env_logger::init_from_env( - env_logger::Env::new() - .default_filter_or(env::var("LOG_LEVEL").unwrap_or("info".to_string())), - ); - +pub async fn start() -> io::Result<()> { let koji_db_url = env::var("KOJI_DB_URL").expect("Need KOJI_DB_URL env var to run migrations"); let scanner_db_url = if env::var("DATABASE_URL").is_ok() { println!("[WARNING] `DATABASE_URL` is deprecated in favor of `SCANNER_DB_URL`"); @@ -124,6 +117,7 @@ pub async fn main() -> io::Result<()> { // increase max payload size to 20MB .app_data(web::JsonConfig::default().limit(20_971_520)) .wrap(middleware::Logger::new("%s | %r - %b bytes in %D ms (%a)")) + .wrap(middleware::Compress::default()) .wrap( SessionMiddleware::builder(CookieSessionStore::default(), Key::from(&[0; 64])) .cookie_secure(false) @@ -144,7 +138,7 @@ pub async fn main() -> io::Result<()> { web::scope("/routes") .service(private::data::instance::from_koji) .service(private::data::instance::from_scanner) - .service(private::data::instance::route_from_scanner), + .service(private::data::instance::route_from_db), ) .service( web::scope("/data") @@ -158,6 +152,7 @@ pub async fn main() -> io::Result<()> { .service( web::scope("/geofence") .service(private::admin::geofence::get_all) + .service(private::admin::geofence::get_ref) .service(private::admin::geofence::paginate) .service(private::admin::geofence::get_one) .service(private::admin::geofence::create) @@ -167,6 +162,7 @@ pub async fn main() -> io::Result<()> { .service( web::scope("/project") .service(private::admin::project::get_all) + .service(private::admin::project::get_ref) .service(private::admin::project::search) .service(private::admin::project::paginate) .service(private::admin::project::get_one) @@ -185,6 +181,7 @@ pub async fn main() -> io::Result<()> { .service( web::scope("/route") .service(private::admin::route::get_all) + .service(private::admin::route::get_ref) .service(private::admin::route::paginate) .service(private::admin::route::get_one) .service(private::admin::route::create) @@ -214,8 +211,17 @@ pub async fn main() -> io::Result<()> { .service(public::v1::geofence::all) .service(public::v1::geofence::save_koji) .service(public::v1::geofence::save_scanner) + .service(public::v1::geofence::push_to_prod) .service(public::v1::geofence::specific_return_type) .service(public::v1::geofence::specific_project), + ) + .service( + web::scope("/route") + .service(public::v1::route::all) + .service(public::v1::route::save_koji) + .service(public::v1::route::push_to_prod) + .service(public::v1::route::specific_return_type) + .service(public::v1::route::specific_project), ), ), ) diff --git a/server/api/src/private/admin/geofence.rs b/server/api/src/private/admin/geofence.rs index b795f908..8b181d49 100644 --- a/server/api/src/private/admin/geofence.rs +++ b/server/api/src/private/admin/geofence.rs @@ -64,6 +64,21 @@ async fn get_all(conn: web::Data) -> Result { })) } +#[get("/ref/")] +async fn get_ref(conn: web::Data) -> Result { + let geofences = geofence::Query::get_json_cache(&conn.koji_db) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + + Ok(HttpResponse::Ok().json(Response { + data: Some(json!(geofences)), + message: "Success".to_string(), + status: "ok".to_string(), + stats: None, + status_code: 200, + })) +} + #[get("/{id}/")] async fn get_one( conn: web::Data, diff --git a/server/api/src/private/admin/project.rs b/server/api/src/private/admin/project.rs index 70f90d4d..c2de7bae 100644 --- a/server/api/src/private/admin/project.rs +++ b/server/api/src/private/admin/project.rs @@ -64,6 +64,21 @@ async fn get_all(conn: web::Data) -> Result { })) } +#[get("/ref/")] +async fn get_ref(conn: web::Data) -> Result { + let geofences = project::Query::get_json_cache(&conn.koji_db) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + + Ok(HttpResponse::Ok().json(Response { + data: Some(json!(geofences)), + message: "Success".to_string(), + status: "ok".to_string(), + stats: None, + status_code: 200, + })) +} + #[get("/{id}/")] async fn get_one( conn: web::Data, diff --git a/server/api/src/private/admin/route.rs b/server/api/src/private/admin/route.rs index 700ce9ac..1453f80f 100644 --- a/server/api/src/private/admin/route.rs +++ b/server/api/src/private/admin/route.rs @@ -66,6 +66,21 @@ async fn get_all(conn: web::Data) -> Result { })) } +#[get("/ref/")] +async fn get_ref(conn: web::Data) -> Result { + let geofences = route::Query::get_json_cache(&conn.koji_db) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + + Ok(HttpResponse::Ok().json(Response { + data: Some(json!(geofences)), + message: "Success".to_string(), + status: "ok".to_string(), + stats: None, + status_code: 200, + })) +} + #[get("/{id}/")] async fn get_one( conn: web::Data, diff --git a/server/api/src/private/data/instance.rs b/server/api/src/private/data/instance.rs index e4229c4d..5d7b599c 100644 --- a/server/api/src/private/data/instance.rs +++ b/server/api/src/private/data/instance.rs @@ -23,11 +23,13 @@ async fn from_scanner( println!("\n[INSTANCE-ALL] Scanner Type: {}", scanner_type); let instances = if scanner_type.eq("rdm") { - instance::Query::all(&conn.data_db, None).await + instance::Query::get_json_cache(&conn.data_db).await } else if let Some(unown_db) = conn.unown_db.as_ref() { area::Query::all(unown_db).await } else { - Ok(vec![]) + Err(DbErr::Custom( + "[DB] Scanner is not configured correctly".to_string(), + )) } .map_err(actix_web::error::ErrorInternalServerError)?; @@ -58,7 +60,8 @@ async fn from_koji( .map(|instance| NameTypeId { id: instance.id, name: instance.name, - r#type: get_enum(instance.mode), + mode: get_enum(instance.mode), + geo_type: None, }) .collect(); @@ -69,7 +72,8 @@ async fn from_koji( fences.push(NameTypeId { id: instance.id, name: instance.name, - r#type: get_enum(Some(instance.mode)), + mode: get_enum(Some(instance.mode)), + geo_type: None, }) }); @@ -86,12 +90,12 @@ async fn from_koji( #[derive(Debug, Deserialize)] struct UrlVars { source: String, - name: String, + id: u32, instance_type: String, } -#[get("/one/{source}/{name}/{instance_type}")] -async fn route_from_scanner( +#[get("/one/{source}/{id}/{instance_type}")] +async fn route_from_db( conn: web::Data, instance: actix_web::web::Path, scanner_type: web::Data, @@ -99,31 +103,28 @@ async fn route_from_scanner( let scanner_type = scanner_type.to_string(); let UrlVars { source, - name, + id, instance_type, } = instance.into_inner(); let instances = if source.eq("scanner") { if scanner_type.eq("rdm") { - instance::Query::route(&conn.data_db, &name).await + instance::Query::feature(&conn.data_db, id).await } else if let Some(unown_db) = conn.unown_db.as_ref() { - let instance_type = get_enum(Some(instance_type)); - if let Some(instance_type) = instance_type { - area::Query::route(unown_db, &name, &instance_type).await - } else { - Err(DbErr::Custom("Invalid Mode".to_string())) - } + area::Query::feature(unown_db, id, instance_type).await } else { Ok(Feature::default()) } } else { if instance_type.eq("CirclePokemon") + || instance_type.eq("CircleSmartPokemon") || instance_type.eq("ManualQuest") || instance_type.eq("CircleRaid") + || instance_type.eq("CircleSmartRaid") { - route::Query::route(&conn.koji_db, &name).await + route::Query::feature(&conn.koji_db, id).await } else { - geofence::Query::route(&conn.koji_db, &name).await + geofence::Query::feature(&conn.koji_db, id).await } } .map_err(actix_web::error::ErrorInternalServerError)?; diff --git a/server/api/src/public/v1/calculate.rs b/server/api/src/public/v1/calculate.rs index 07e3d3e5..e9c6518c 100644 --- a/server/api/src/public/v1/calculate.rs +++ b/server/api/src/public/v1/calculate.rs @@ -1,3 +1,5 @@ +use crate::utils::request; + use super::*; use std::{ @@ -12,9 +14,9 @@ use model::{ api::{ args::{Args, ArgsUnwrapped, Response, Stats}, point_array::PointArray, - FeatureHelpers, Precision, ToCollection, ToFeature, + FeatureHelpers, GeoFormats, Precision, ToCollection, ToFeature, }, - db::{route, sea_orm_active_enums::Type, GenericData}, + db::{area, instance, route, sea_orm_active_enums::Type, GenericData}, KojiDb, }; @@ -33,6 +35,7 @@ async fn bootstrap( radius, return_type, save_to_db, + save_to_scanner, .. } = payload.into_inner().init(Some("bootstrap")); @@ -61,18 +64,35 @@ async fn bootstrap( if !feat.contains_property("__name") && !instance.is_empty() { feat.set_property("__name", instance.clone()); } - feat.set_property("__type", Type::CirclePokemon.to_string()); + feat.set_property("__mode", Type::CircleSmartPokemon.to_string()); if save_to_db { - route::Query::upsert_from_collection( - &conn.koji_db, - feat.clone().to_collection(Some(instance.clone()), None), - true, - true, - ) - .await + route::Query::upsert_from_geometry(&conn.koji_db, GeoFormats::Feature(feat.clone())) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + } + if save_to_scanner { + if scanner_type == "rdm" { + instance::Query::upsert_from_geometry( + &conn.data_db, + GeoFormats::Feature(feat.clone()), + true, + ) + .await + } else if let Some(conn) = conn.unown_db.as_ref() { + area::Query::upsert_from_geometry(conn, GeoFormats::Feature(feat.clone())).await + } else { + Err(DbErr::Custom( + "Scanner not configured correctly".to_string(), + )) + } .map_err(actix_web::error::ErrorInternalServerError)?; } } + if save_to_scanner { + request::update_project_api(&conn.koji_db, Some(scanner_type)) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + } Ok(utils::response::send( features.to_collection(Some(instance.clone()), None), @@ -105,6 +125,7 @@ async fn cluster( routing_time, only_unique, save_to_db, + save_to_scanner, last_seen, route_chunk_size, .. @@ -118,11 +139,11 @@ async fn cluster( let mut stats = Stats::new(); let enum_type = if category == "gym" { - Type::CircleRaid + Type::CircleSmartRaid } else if category == "pokestop" { Type::ManualQuest } else { - Type::CirclePokemon + Type::CircleSmartPokemon }; let area = utils::create_or_find_collection(&instance, scanner_type, &conn, area, &data_points) @@ -205,13 +226,39 @@ async fn cluster( clusters = final_clusters.into(); } - let mut feature = clusters.to_feature(Some(&enum_type)).remove_last_coord(); - feature.add_instance_properties(Some(instance.to_string()), Some(&enum_type)); - + let mut feature = clusters + .to_feature(Some(enum_type.clone())) + .remove_last_coord(); + feature.add_instance_properties(Some(instance.to_string()), Some(enum_type)); let feature = feature.to_collection(Some(instance.clone()), None); if !instance.is_empty() && save_to_db { - route::Query::upsert_from_collection(&conn.koji_db, feature.clone(), true, false) + route::Query::upsert_from_geometry( + &conn.koji_db, + GeoFormats::FeatureCollection(feature.clone()), + ) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + } + if save_to_scanner { + if scanner_type == "rdm" { + instance::Query::upsert_from_geometry( + &conn.data_db, + GeoFormats::FeatureCollection(feature.clone()), + true, + ) + .await + } else if let Some(conn) = conn.unown_db.as_ref() { + area::Query::upsert_from_geometry(conn, GeoFormats::FeatureCollection(feature.clone())) + .await + } else { + Err(DbErr::Custom( + "Scanner not configured correctly".to_string(), + )) + } + .map_err(actix_web::error::ErrorInternalServerError)?; + + request::update_project_api(&conn.koji_db, Some(scanner_type)) .await .map_err(actix_web::error::ErrorInternalServerError)?; } diff --git a/server/api/src/public/v1/convert.rs b/server/api/src/public/v1/convert.rs index fcc8ac2c..464097bb 100644 --- a/server/api/src/public/v1/convert.rs +++ b/server/api/src/public/v1/convert.rs @@ -69,7 +69,7 @@ async fn merge_points(payload: web::Json) -> Result { foreign_members: None, value: Value::MultiPoint(new_multi_point), } - .to_feature(Some(&Type::CirclePokemon)) + .to_feature(Some(Type::CirclePokemon)) .to_collection(None, None), return_type, None, diff --git a/server/api/src/public/v1/geofence.rs b/server/api/src/public/v1/geofence.rs index 6da586ef..9d4ecf9e 100644 --- a/server/api/src/public/v1/geofence.rs +++ b/server/api/src/public/v1/geofence.rs @@ -1,3 +1,5 @@ +use crate::utils::request::send_api_req; + use super::*; use serde_json::json; @@ -5,35 +7,21 @@ use serde_json::json; use model::{ api::{ args::{get_return_type, Args, ArgsUnwrapped, Response, ReturnTypeArg}, - ToCollection, + GeoFormats, ToCollection, }, - db::{area, geofence, instance}, + db::{area, geofence, instance, project}, KojiDb, }; #[get("/all")] async fn all(conn: web::Data) -> Result { - let geofences = geofence::Query::get_all(&conn.koji_db) + let fc = geofence::Query::as_collection(&conn.koji_db) .await .map_err(actix_web::error::ErrorInternalServerError)?; - let features: Vec = geofences - .into_iter() - .map(|item| { - let feature = Feature::from_json_value(item.area); - let mut feature = if feature.is_ok() { - feature.unwrap() - } else { - Feature::default() - }; - feature.set_property("name", item.name); - feature.set_property("id", item.id); - feature - }) - .collect(); - - println!("[PUBLIC_API] Returning {} instances\n", features.len()); + + println!("[PUBLIC_API] Returning {} instances\n", fc.features.len()); Ok(HttpResponse::Ok().json(Response { - data: Some(json!(features.to_collection(None, None))), + data: Some(json!(fc)), message: "Success".to_string(), status: "ok".to_string(), stats: None, @@ -47,7 +35,7 @@ async fn get_area( area: actix_web::web::Path, ) -> Result { let area = area.into_inner(); - let feature = geofence::Query::route(&conn.koji_db, &area) + let feature = geofence::Query::feature_from_name(&conn.koji_db, &area) .await .map_err(actix_web::error::ErrorInternalServerError)?; @@ -71,9 +59,10 @@ async fn save_koji( ) -> Result { let ArgsUnwrapped { area, .. } = payload.into_inner().init(Some("geofence_save")); - let (inserts, updates) = geofence::Query::save(&conn.koji_db, area) - .await - .map_err(actix_web::error::ErrorInternalServerError)?; + let (inserts, updates) = + geofence::Query::upsert_from_geometry(&conn.koji_db, GeoFormats::FeatureCollection(area)) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; println!("Rows Updated: {}, Rows Inserted: {}", updates, inserts); @@ -96,12 +85,29 @@ async fn save_scanner( let ArgsUnwrapped { area, .. } = payload.into_inner().init(Some("geofence_save")); let (inserts, updates) = if scanner_type == "rdm" { - instance::Query::save(&conn.data_db, area).await + instance::Query::upsert_from_geometry( + &conn.data_db, + GeoFormats::FeatureCollection(area), + false, + ) + .await } else { - area::Query::save(&conn.unown_db.as_ref().unwrap(), area).await + area::Query::upsert_from_geometry( + &conn.unown_db.as_ref().unwrap(), + GeoFormats::FeatureCollection(area), + ) + .await } .map_err(actix_web::error::ErrorInternalServerError)?; + let project = project::Query::get_scanner_project(&conn.koji_db) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + if let Some(project) = project { + send_api_req(project, Some(scanner_type)) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + } println!("Rows Updated: {}, Rows Inserted: {}", updates, inserts); Ok(HttpResponse::Ok().json(Response { @@ -113,6 +119,50 @@ async fn save_scanner( })) } +#[get("/push/{id}")] +async fn push_to_prod( + conn: web::Data, + scanner_type: web::Data, + id: actix_web::web::Path, +) -> Result { + let id = id.into_inner(); + let scanner_type = scanner_type.as_ref(); + + let feature = geofence::Query::feature(&conn.koji_db, id) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + + let (inserts, updates) = if scanner_type == "rdm" { + instance::Query::upsert_from_geometry(&conn.data_db, GeoFormats::Feature(feature), false) + .await + } else { + area::Query::upsert_from_geometry( + &conn.unown_db.as_ref().unwrap(), + GeoFormats::Feature(feature), + ) + .await + } + .map_err(actix_web::error::ErrorInternalServerError)?; + + let project = project::Query::get_scanner_project(&conn.koji_db) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + if let Some(project) = project { + send_api_req(project, Some(scanner_type)) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + } + log::info!("Rows Updated: {}, Rows Inserted: {}", updates, inserts); + + Ok(HttpResponse::Ok().json(Response { + data: Some(json!({ "updates": updates, "inserts": inserts })), + message: "Success".to_string(), + status: "ok".to_string(), + stats: None, + status_code: 200, + })) +} + #[get("/{return_type}")] async fn specific_return_type( conn: web::Data, diff --git a/server/api/src/public/v1/mod.rs b/server/api/src/public/v1/mod.rs index 2d5cf596..62bffed9 100644 --- a/server/api/src/public/v1/mod.rs +++ b/server/api/src/public/v1/mod.rs @@ -3,3 +3,4 @@ use super::*; pub mod calculate; pub mod convert; pub mod geofence; +pub mod route; diff --git a/server/api/src/public/v1/route.rs b/server/api/src/public/v1/route.rs new file mode 100644 index 00000000..68073c84 --- /dev/null +++ b/server/api/src/public/v1/route.rs @@ -0,0 +1,163 @@ +use crate::utils::request::send_api_req; + +use super::*; + +use serde_json::json; + +use model::{ + api::{ + args::{get_return_type, Args, ArgsUnwrapped, Response, ReturnTypeArg}, + GeoFormats, ToCollection, + }, + db::{area, instance, project, route}, + KojiDb, +}; + +#[get("/all")] +async fn all(conn: web::Data) -> Result { + let fc = route::Query::as_collection(&conn.koji_db) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + + log::info!("[PUBLIC_API] Returning {} routes\n", fc.features.len()); + Ok(HttpResponse::Ok().json(Response { + data: Some(json!(fc)), + message: "Success".to_string(), + status: "ok".to_string(), + stats: None, + status_code: 200, + })) +} + +#[get("/area/{area_name}")] +async fn get_area( + conn: web::Data, + area: actix_web::web::Path, +) -> Result { + let area = area.into_inner(); + let feature = route::Query::feature_from_name(&conn.koji_db, area) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + + log::info!( + "[PUBLIC_API] Returning feature for {:?}\n", + feature.property("name") + ); + Ok(HttpResponse::Ok().json(Response { + data: Some(json!(feature)), + message: "Success".to_string(), + status: "ok".to_string(), + stats: None, + status_code: 200, + })) +} + +#[post("/save-koji")] +async fn save_koji( + conn: web::Data, + payload: web::Json, +) -> Result { + let ArgsUnwrapped { area, .. } = payload.into_inner().init(Some("geofence_save")); + + let (inserts, updates) = + route::Query::upsert_from_geometry(&conn.koji_db, GeoFormats::FeatureCollection(area)) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + + log::info!("Rows Updated: {}, Rows Inserted: {}", updates, inserts); + + Ok(HttpResponse::Ok().json(Response { + data: Some(json!({ "updates": updates, "inserts": inserts })), + message: "Success".to_string(), + status: "ok".to_string(), + stats: None, + status_code: 200, + })) +} + +#[get("/push/{id}")] +async fn push_to_prod( + conn: web::Data, + scanner_type: web::Data, + id: actix_web::web::Path, +) -> Result { + let id = id.into_inner(); + let scanner_type = scanner_type.as_ref(); + + let feature = route::Query::feature(&conn.koji_db, id) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + + let (inserts, updates) = if scanner_type == "rdm" { + instance::Query::upsert_from_geometry(&conn.data_db, GeoFormats::Feature(feature), false) + .await + } else { + area::Query::upsert_from_geometry( + &conn.unown_db.as_ref().unwrap(), + GeoFormats::Feature(feature), + ) + .await + } + .map_err(actix_web::error::ErrorInternalServerError)?; + + let project = project::Query::get_scanner_project(&conn.koji_db) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + if let Some(project) = project { + send_api_req(project, Some(scanner_type)) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + } + log::info!("Rows Updated: {}, Rows Inserted: {}", updates, inserts); + + Ok(HttpResponse::Ok().json(Response { + data: Some(json!({ "updates": updates, "inserts": inserts })), + message: "Success".to_string(), + status: "ok".to_string(), + stats: None, + status_code: 200, + })) +} + +#[get("/{return_type}")] +async fn specific_return_type( + conn: web::Data, + url: actix_web::web::Path, +) -> Result { + let return_type = url.into_inner(); + let return_type = get_return_type(return_type, &ReturnTypeArg::FeatureCollection); + + let fc = route::Query::as_collection(&conn.koji_db) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + + log::info!( + "[GEOFENCES_ALL] Returning {} instances\n", + fc.features.len() + ); + Ok(utils::response::send(fc, return_type, None, false, None)) +} + +#[get("/{return_type}/{project_name}")] +async fn specific_project( + conn: web::Data, + url: actix_web::web::Path<(String, String)>, +) -> Result { + let (return_type, project_name) = url.into_inner(); + let return_type = get_return_type(return_type, &ReturnTypeArg::FeatureCollection); + let features = route::Query::by_geofence(&conn.koji_db, project_name) + .await + .map_err(actix_web::error::ErrorInternalServerError)?; + + log::info!( + "[GEOFENCES_FC_ALL] Returning {} instances\n", + features.len() + ); + Ok(utils::response::send( + features.to_collection(None, None), + return_type, + None, + false, + None, + )) +} diff --git a/server/api/src/utils/error.rs b/server/api/src/utils/error.rs new file mode 100644 index 00000000..879ff834 --- /dev/null +++ b/server/api/src/utils/error.rs @@ -0,0 +1,37 @@ +use migration::DbErr; +use model::error::ModelError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("`{0}`")] + Model(ModelError), + #[error("Project API Error: `{0}`")] + ProjectApiError(String), + #[error("Database Error: {0}")] + Database(DbErr), + #[error("Request API Error: {0}")] + RequestError(String), + #[error("Not Implemented: {0}")] + NotImplemented(String), +} + +// pub type Result = std::result::Result; + +impl From for Error { + fn from(error: DbErr) -> Self { + Self::Database(error) + } +} + +impl From for Error { + fn from(error: reqwest::Error) -> Self { + Self::RequestError(error.to_string()) + } +} + +impl From for Error { + fn from(error: ModelError) -> Self { + Self::Model(error) + } +} diff --git a/server/api/src/utils/mod.rs b/server/api/src/utils/mod.rs index 70ba14bc..09d50bfb 100644 --- a/server/api/src/utils/mod.rs +++ b/server/api/src/utils/mod.rs @@ -5,10 +5,13 @@ use geojson::{Geometry, Value}; use model::{ api::{collection::Default, single_vec::SingleVec, BBox, ToCollection}, db::{area, geofence, gym, instance, pokestop, spawnpoint, GenericData}, + error::ModelError, KojiDb, }; pub mod auth; +pub mod error; +pub mod request; pub mod response; pub fn is_docker() -> io::Result { @@ -22,7 +25,7 @@ pub async fn load_collection( instance: &String, scanner_type: &String, conn: &KojiDb, -) -> Result { +) -> Result { match load_feature(instance, scanner_type, conn).await { Ok(feature) => Ok(feature.to_collection(None, None)), Err(err) => Err(err), @@ -33,17 +36,17 @@ pub async fn load_feature( instance: &String, scanner_type: &String, conn: &KojiDb, -) -> Result { - match geofence::Query::route(&conn.koji_db, &instance).await { +) -> Result { + match geofence::Query::feature_from_name(&conn.koji_db, &instance).await { Ok(area) => Ok(area), Err(_) => { if scanner_type.eq("rdm") { - instance::Query::route(&conn.data_db, &instance).await + instance::Query::feature_from_name(&conn.data_db, &instance).await } else { - area::Query::route( + area::Query::feature_from_name( &conn.unown_db.as_ref().unwrap(), &instance, - &model::db::sea_orm_active_enums::Type::AutoQuest, + "AutoQuest".to_string(), ) .await } @@ -57,7 +60,7 @@ pub async fn create_or_find_collection( conn: &KojiDb, area: FeatureCollection, data_points: &SingleVec, -) -> Result { +) -> Result { if !data_points.is_empty() { let bbox = BBox::new( &data_points @@ -76,7 +79,7 @@ pub async fn create_or_find_collection( }), ..Feature::default() }], - foreign_members: None, + ..FeatureCollection::default() }) } else if !area.features.is_empty() { Ok(area) diff --git a/server/api/src/utils/request.rs b/server/api/src/utils/request.rs new file mode 100644 index 00000000..aa54a494 --- /dev/null +++ b/server/api/src/utils/request.rs @@ -0,0 +1,57 @@ +use super::{error::Error, *}; + +use model::db::project; +use sea_orm::DatabaseConnection; + +pub async fn update_project_api( + db: &DatabaseConnection, + scanner_type: Option<&String>, +) -> Result { + let project = project::Query::get_scanner_project(&db).await?; + if let Some(project) = project { + send_api_req(project, scanner_type).await + } else { + Err(Error::ProjectApiError( + "No scanner project found".to_string(), + )) + } +} +pub async fn send_api_req( + project: project::Model, + scanner_type: Option<&String>, +) -> Result { + if let Some(endpoint) = project.api_endpoint.as_ref() { + let req = reqwest::ClientBuilder::new().build(); + if let Ok(req) = req { + if let Some(scanner_type) = scanner_type { + let req = if scanner_type.eq("rdm") { + if let Some(api_key) = project.api_key { + let split = api_key.split_once(":"); + if let Some((username, password)) = split { + req.get(endpoint).basic_auth(username, Some(password)) + } else { + req.get(endpoint) + } + } else { + req.get(endpoint) + } + } else { + req.get(endpoint) + }; + log::info!( + "Sending Scanner Request to {}", + project.api_endpoint.unwrap() + ); + Ok(req.send().await?) + } else { + Err(Error::NotImplemented("Scanner type not found".to_string())) + } + } else { + Err(Error::NotImplemented("Scanner type not found".to_string())) + } + } else { + let error = format!("API Endpoint not specified for project {}", project.name); + log::warn!("{}", error); + Err(Error::ProjectApiError(error)) + } +} diff --git a/server/api/src/utils/response.rs b/server/api/src/utils/response.rs index a6b5bc46..c9d6acae 100644 --- a/server/api/src/utils/response.rs +++ b/server/api/src/utils/response.rs @@ -1,6 +1,7 @@ use super::*; use actix_web::HttpResponse; +use model::api::ToGeometry; use serde_json::json; use crate::model::api::{ @@ -29,11 +30,21 @@ pub fn send( ReturnTypeArg::AltText => GeoFormats::Text(value.to_text(" ", ",", false)), ReturnTypeArg::SingleArray => GeoFormats::SingleArray(value.to_single_vec()), ReturnTypeArg::MultiArray => GeoFormats::MultiArray(value.to_multi_vec()), + ReturnTypeArg::Geometry => { + if value.features.len() == 1 { + GeoFormats::Geometry(value.features.first().unwrap().to_owned().to_geometry()) + } else { + log::info!("\"Geometry\" was requested as the return type but multiple features were found so a Vec of geometries is being returned"); + GeoFormats::GeometryVec(value.into_iter().map(|feat| feat.to_geometry()).collect()) + } + }, + ReturnTypeArg::GeometryVec => GeoFormats::GeometryVec(value.into_iter().map(|feat| feat.to_geometry()).collect()), ReturnTypeArg::Feature => { if value.features.len() == 1 { - GeoFormats::Feature(value.features.first().unwrap().clone()) + let feat = GeoFormats::Feature(value.features.first().unwrap().clone()); + feat } else { - println!("\"Feature\" was requested as the return type but multiple features were found so a Vec of features is being returned"); + log::info!("\"Feature\" was requested as the return type but multiple features were found so a Vec of features is being returned"); GeoFormats::FeatureVec(value.features) } } diff --git a/server/geo_repair/Cargo.toml b/server/geo_repair/Cargo.toml index 46d67501..3360881b 100644 --- a/server/geo_repair/Cargo.toml +++ b/server/geo_repair/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "geo_repair" -version = "0.1.0" +version = "0.4.0" edition = "2021" publish = false diff --git a/server/migration/Cargo.toml b/server/migration/Cargo.toml index b48619e8..7acc46e2 100644 --- a/server/migration/Cargo.toml +++ b/server/migration/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "migration" -version = "0.1.0" +version = "0.4.0" edition = "2021" publish = false diff --git a/server/migration/src/lib.rs b/server/migration/src/lib.rs index 80315169..0bf30942 100644 --- a/server/migration/src/lib.rs +++ b/server/migration/src/lib.rs @@ -6,6 +6,8 @@ mod m20221207_122501_create_geofence_project; mod m20221229_163230_change_fks; mod m20230108_204408_add_type_column; mod m20230117_010422_routes_table; +mod m20230121_184556_add_project_api; +mod m20230122_134517_route_description; pub struct Migrator; @@ -19,6 +21,8 @@ impl MigratorTrait for Migrator { Box::new(m20221229_163230_change_fks::Migration), Box::new(m20230108_204408_add_type_column::Migration), Box::new(m20230117_010422_routes_table::Migration), + Box::new(m20230121_184556_add_project_api::Migration), + Box::new(m20230122_134517_route_description::Migration), ] } } diff --git a/server/migration/src/m20221207_122452_create_project.rs b/server/migration/src/m20221207_122452_create_project.rs index dc992de1..e437a45f 100644 --- a/server/migration/src/m20221207_122452_create_project.rs +++ b/server/migration/src/m20221207_122452_create_project.rs @@ -40,6 +40,9 @@ pub enum Project { Table, Id, Name, + ApiEndpoint, + ApiKey, + Scanner, CreatedAt, UpdatedAt, } diff --git a/server/migration/src/m20230117_010422_routes_table.rs b/server/migration/src/m20230117_010422_routes_table.rs index 76631e33..be0e560a 100644 --- a/server/migration/src/m20230117_010422_routes_table.rs +++ b/server/migration/src/m20230117_010422_routes_table.rs @@ -55,6 +55,7 @@ pub enum Route { GeofenceId, Name, Mode, + Description, Geometry, CreatedAt, UpdatedAt, diff --git a/server/migration/src/m20230121_184556_add_project_api.rs b/server/migration/src/m20230121_184556_add_project_api.rs new file mode 100644 index 00000000..48a9c161 --- /dev/null +++ b/server/migration/src/m20230121_184556_add_project_api.rs @@ -0,0 +1,36 @@ +use sea_orm_migration::prelude::*; + +use crate::m20221207_122452_create_project::Project; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let table = Table::alter() + .table(Project::Table) + .add_column(ColumnDef::new(Project::ApiEndpoint).string()) + .add_column(ColumnDef::new(Project::ApiKey).string()) + .add_column( + ColumnDef::new(Project::Scanner) + .boolean() + .not_null() + .default(false), + ) + .to_owned(); + + manager.alter_table(table).await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let table = Table::alter() + .table(Project::Table) + .drop_column(Project::ApiEndpoint) + .drop_column(Project::ApiKey) + .drop_column(Project::Scanner) + .to_owned(); + + manager.alter_table(table).await + } +} diff --git a/server/migration/src/m20230122_134517_route_description.rs b/server/migration/src/m20230122_134517_route_description.rs new file mode 100644 index 00000000..bcc08ece --- /dev/null +++ b/server/migration/src/m20230122_134517_route_description.rs @@ -0,0 +1,27 @@ +use sea_orm_migration::prelude::*; + +use crate::m20230117_010422_routes_table::Route; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let table = Table::alter() + .table(Route::Table) + .add_column(ColumnDef::new(Route::Description).string()) + .to_owned(); + + manager.alter_table(table).await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let table = Table::alter() + .table(Route::Table) + .drop_column(Route::Description) + .to_owned(); + + manager.alter_table(table).await + } +} diff --git a/server/model/Cargo.toml b/server/model/Cargo.toml index 16ea78ea..88b9ea17 100644 --- a/server/model/Cargo.toml +++ b/server/model/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "model" -version = "0.1.0" +version = "0.4.0" edition = "2021" publish = false @@ -14,6 +14,7 @@ futures = "0.3.25" geo = "0.22.1" geojson = "0.24.0" geo_repair = { path = "../geo_repair" } +log = "0.4.17" num-traits = "0.2.15" sea-orm = { version = "0.9.3", features = [ "sqlx-mysql", @@ -23,3 +24,4 @@ sea-orm = { version = "0.9.3", features = [ serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0" serde_with = "2.1.0" +thiserror = "1.0.38" diff --git a/server/model/src/api/args.rs b/server/model/src/api/args.rs index dbb03d5b..98e29fc6 100644 --- a/server/model/src/api/args.rs +++ b/server/model/src/api/args.rs @@ -2,7 +2,10 @@ use super::*; use geojson::JsonValue; -use crate::api::{collection::Default, text::TextHelpers}; +use crate::{ + api::{collection::Default, text::TextHelpers}, + utils::get_enum_by_geometry_string, +}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Auth { @@ -26,6 +29,8 @@ pub enum ReturnTypeArg { MultiArray, SingleStruct, MultiStruct, + Geometry, + GeometryVec, Feature, FeatureVec, FeatureCollection, @@ -58,8 +63,10 @@ pub struct Args { pub only_unique: Option, pub last_seen: Option, pub save_to_db: Option, + pub save_to_scanner: Option, pub route_chunk_size: Option, pub simplify: Option, + pub geometry_type: Option, } pub struct ArgsUnwrapped { @@ -77,6 +84,7 @@ pub struct ArgsUnwrapped { pub only_unique: bool, pub last_seen: u32, pub save_to_db: bool, + pub save_to_scanner: bool, pub route_chunk_size: usize, pub simplify: bool, } @@ -98,12 +106,15 @@ impl Args { only_unique, last_seen, save_to_db, + save_to_scanner, route_chunk_size, simplify, + geometry_type, } = self; + let enum_type = get_enum_by_geometry_string(geometry_type); let (area, default_return_type) = if let Some(area) = area { ( - area.clone().to_collection(instance.clone(), None), + area.clone().to_collection(instance.clone(), enum_type), match area { GeoFormats::Text(area) => { if area.text_test() { @@ -116,6 +127,8 @@ impl Args { GeoFormats::MultiArray(_) => ReturnTypeArg::MultiArray, GeoFormats::SingleStruct(_) => ReturnTypeArg::SingleStruct, GeoFormats::MultiStruct(_) => ReturnTypeArg::MultiStruct, + GeoFormats::Geometry(_) => ReturnTypeArg::Geometry, + GeoFormats::GeometryVec(_) => ReturnTypeArg::GeometryVec, GeoFormats::Feature(_) => ReturnTypeArg::Feature, GeoFormats::FeatureVec(_) => ReturnTypeArg::FeatureVec, GeoFormats::FeatureCollection(_) => ReturnTypeArg::FeatureCollection, @@ -151,6 +164,7 @@ impl Args { let only_unique = only_unique.unwrap_or(false); let last_seen = last_seen.unwrap_or(0); let save_to_db = save_to_db.unwrap_or(false); + let save_to_scanner = save_to_scanner.unwrap_or(false); let route_chunk_size = route_chunk_size.unwrap_or(0); let simplify = simplify.unwrap_or(false); if let Some(mode) = mode { @@ -174,6 +188,7 @@ impl Args { only_unique, last_seen, save_to_db, + save_to_scanner, route_chunk_size, simplify, } @@ -181,29 +196,29 @@ impl Args { } pub fn get_return_type(return_type: String, default_return_type: &ReturnTypeArg) -> ReturnTypeArg { - match return_type.to_lowercase().as_str() { - "alttext" | "alt_text" | "alt-text" => ReturnTypeArg::AltText, + match return_type.to_lowercase().replace("-", "_").as_str() { + "alttext" | "alt_text" => ReturnTypeArg::AltText, "text" => ReturnTypeArg::Text, "array" => match *default_return_type { ReturnTypeArg::SingleArray => ReturnTypeArg::SingleArray, ReturnTypeArg::MultiArray => ReturnTypeArg::MultiArray, _ => ReturnTypeArg::SingleArray, }, - "singlearray" | "single_array" | "single-array" => ReturnTypeArg::SingleArray, - "multiarray" | "multi_array" | "multi-array" => ReturnTypeArg::MultiArray, + "singlearray" | "single_array" => ReturnTypeArg::SingleArray, + "multiarray" | "multi_array" => ReturnTypeArg::MultiArray, "struct" => match *default_return_type { ReturnTypeArg::SingleStruct => ReturnTypeArg::SingleStruct, ReturnTypeArg::MultiStruct => ReturnTypeArg::MultiStruct, _ => ReturnTypeArg::SingleStruct, }, - "singlestruct" | "single_struct" | "single-struct" => ReturnTypeArg::SingleStruct, - "multistruct" | "multi_struct" | "multi-struct" => ReturnTypeArg::MultiStruct, + "geometry" => ReturnTypeArg::Geometry, + "geometryvec" | "geometry_vec" | "geometries" => ReturnTypeArg::GeometryVec, + "singlestruct" | "single_struct" => ReturnTypeArg::SingleStruct, + "multistruct" | "multi_struct" => ReturnTypeArg::MultiStruct, "feature" => ReturnTypeArg::Feature, - "featurevec" | "feature_vec" | "feature-vec" => ReturnTypeArg::FeatureVec, + "featurevec" | "feature_vec" => ReturnTypeArg::FeatureVec, "poracle" => ReturnTypeArg::Poracle, - "featurecollection" | "feature_collection" | "feature-collection" => { - ReturnTypeArg::FeatureCollection - } + "featurecollection" | "feature_collection" => ReturnTypeArg::FeatureCollection, _ => default_return_type.clone(), } } diff --git a/server/model/src/api/collection.rs b/server/model/src/api/collection.rs index 1f56a795..747757e4 100644 --- a/server/model/src/api/collection.rs +++ b/server/model/src/api/collection.rs @@ -40,7 +40,7 @@ impl GeometryHelpers for FeatureCollection { } impl EnsureProperties for FeatureCollection { - fn ensure_properties(self, name: Option, enum_type: Option<&Type>) -> Self { + fn ensure_properties(self, name: Option, enum_type: Option) -> Self { let name = if let Some(n) = name { n } else { @@ -56,13 +56,23 @@ impl EnsureProperties for FeatureCollection { } else { name.clone() }), - enum_type, + enum_type.clone(), ) }) .collect() } } +impl GetBbox for FeatureCollection { + fn get_bbox(&self) -> Option { + self.clone() + .into_iter() + .flat_map(|x| x.to_single_vec()) + .collect::() + .get_bbox() + } +} + impl ToSingleVec for FeatureCollection { fn to_single_vec(self) -> single_vec::SingleVec { self.to_multi_vec().into_iter().flatten().collect() @@ -94,16 +104,12 @@ impl ToText for FeatureCollection { } impl ToCollection for FeatureCollection { - fn to_collection(self, _name: Option, _enum_type: Option<&Type>) -> FeatureCollection { + fn to_collection(self, _name: Option, _enum_type: Option) -> FeatureCollection { FeatureCollection { bbox: if self.bbox.is_some() { self.bbox } else { - self.clone() - .into_iter() - .flat_map(|x| x.to_single_vec()) - .collect::() - .get_bbox() + self.get_bbox() }, features: self .features @@ -112,7 +118,7 @@ impl ToCollection for FeatureCollection { bbox: if feat.bbox.is_some() { feat.bbox } else { - feat.clone().to_single_vec().get_bbox() + feat.get_bbox() }, ..feat }) @@ -223,7 +229,7 @@ impl ToPoracleVec for FeatureCollection { }), Value::Polygon(_) => poracle_feat.path = Some(geometry.to_single_vec()), _ => { - println!( + log::warn!( "Poracle format does not support: {:?}", geometry.value.type_name() ); diff --git a/server/model/src/api/feature.rs b/server/model/src/api/feature.rs index 4e3c0654..3f752596 100644 --- a/server/model/src/api/feature.rs +++ b/server/model/src/api/feature.rs @@ -14,16 +14,30 @@ impl EnsurePoints for Feature { } } +impl ToGeometry for Feature { + fn to_geometry(self) -> Geometry { + if let Some(geometry) = self.geometry { + geometry + } else { + Geometry { + bbox: None, + foreign_members: None, + value: Value::Point(vec![0., 0.]), + } + } + } +} + impl FeatureHelpers for Feature { - fn add_instance_properties(&mut self, name: Option, enum_type: Option<&Type>) { + fn add_instance_properties(&mut self, name: Option, enum_type: Option) { if !self.contains_property("__name") { if let Some(name) = name { self.set_property("__name", name) } } - if !self.contains_property("__type") { + if !self.contains_property("__mode") { if let Some(enum_type) = enum_type { - self.set_property("__type", enum_type.to_string()); + self.set_property("__mode", enum_type.to_string()); // match enum_type { // Type::CirclePokemon | Type::CircleSmartPokemon => { // self.set_property("radius", 70); @@ -39,10 +53,10 @@ impl FeatureHelpers for Feature { } else if let Some(geometry) = self.geometry.as_ref() { match geometry.value { Value::Point(_) | Value::MultiPoint(_) => { - self.set_property("__type", "CirclePokemon"); + self.set_property("__mode", "CirclePokemon"); } Value::Polygon(_) | Value::MultiPolygon(_) => { - self.set_property("__type", "AutoQuest"); + self.set_property("__mode", "AutoQuest"); } _ => {} } @@ -94,13 +108,23 @@ impl FeatureHelpers for Feature { } impl EnsureProperties for Feature { - fn ensure_properties(self, name: Option, enum_type: Option<&Type>) -> Self { + fn ensure_properties(self, name: Option, enum_type: Option) -> Self { let mut mutable_self = self; mutable_self.add_instance_properties(name, enum_type); mutable_self } } +impl GetBbox for Feature { + fn get_bbox(&self) -> Option { + if let Some(geometry) = self.geometry.clone() { + geometry.to_single_vec().get_bbox() + } else { + None + } + } +} + impl ToSingleVec for Feature { fn to_single_vec(self) -> single_vec::SingleVec { self.to_multi_vec().into_iter().flatten().collect() @@ -160,12 +184,8 @@ impl ToFeatureVec for Feature { } impl ToCollection for Feature { - fn to_collection(self, _name: Option, _enum_type: Option<&Type>) -> FeatureCollection { - let bbox = if self.bbox.is_some() { - self.bbox - } else { - self.clone().to_single_vec().get_bbox() - }; + fn to_collection(self, _name: Option, _enum_type: Option) -> FeatureCollection { + let bbox = self.get_bbox(); FeatureCollection { bbox: bbox.clone(), features: vec![Feature { bbox, ..self }.ensure_first_last()], @@ -174,8 +194,18 @@ impl ToCollection for Feature { } } +impl GetBbox for Vec { + fn get_bbox(&self) -> Option { + self.clone() + .into_iter() + .flat_map(|f| f.to_single_vec()) + .collect::() + .get_bbox() + } +} + impl ToCollection for Vec { - fn to_collection(self, _name: Option, _enum_type: Option<&Type>) -> FeatureCollection { + fn to_collection(self, _name: Option, _enum_type: Option) -> FeatureCollection { // let name = if let Some(name) = name { // name // } else { @@ -183,25 +213,12 @@ impl ToCollection for Vec { // }; // let length = self.len(); FeatureCollection { - bbox: self - .clone() - .into_iter() - .flat_map(|feat| feat.to_single_vec()) - .collect::() - .get_bbox(), + bbox: self.get_bbox(), features: self .into_iter() - .enumerate() - .map(|(_i, feat)| Feature { - bbox: feat.clone().to_single_vec().get_bbox(), - ..feat.ensure_first_last() // .ensure_properties( - // Some(if length > 1 { - // format!("{}_{}", name, i) - // } else { - // name.clone() - // }), - // enum_type, - // ) + .map(|feat| Feature { + bbox: feat.get_bbox(), + ..feat.ensure_first_last() }) .collect(), foreign_members: None, diff --git a/server/model/src/api/geometry.rs b/server/model/src/api/geometry.rs index a0a72281..d46c9f33 100644 --- a/server/model/src/api/geometry.rs +++ b/server/model/src/api/geometry.rs @@ -33,7 +33,7 @@ impl EnsurePoints for Geometry { impl GeometryHelpers for Geometry { fn simplify(self) -> Self { - match self.value { + let mut geometry = match self.value { Value::Polygon(_) => { Geometry::from(&Polygon::::try_from(self).unwrap().simplify(&0.0001)) } @@ -43,7 +43,15 @@ impl GeometryHelpers for Geometry { .simplify(&0.0001), ), _ => self, - } + }; + geometry.bbox = geometry.get_bbox(); + geometry + } +} + +impl GetBbox for Geometry { + fn get_bbox(&self) -> Option { + self.clone().to_single_vec().get_bbox() } } @@ -85,7 +93,7 @@ impl ToSingleVec for Geometry { } } _ => { - println!("Unsupported Geometry: {:?}", self.value.type_name()) + log::warn!("Unsupported Geometry: {:?}", self.value.type_name()) } } return_value @@ -93,10 +101,53 @@ impl ToSingleVec for Geometry { } impl ToFeature for Geometry { - fn to_feature(self, _enum_type: Option<&Type>) -> Feature { + fn to_feature(self, enum_type: Option) -> Feature { + let bbox = self.get_bbox(); Feature { - bbox: self.clone().to_single_vec().get_bbox(), - geometry: Some(self), + bbox: bbox.clone(), + geometry: Some(Self { + bbox, + foreign_members: None, + value: if let Some(enum_type) = enum_type { + match enum_type { + Type::Leveling => { + Value::Point(self.to_single_vec().to_point_array().to_vec()) + } + Type::CirclePokemon => Value::MultiPoint( + self.to_single_vec() + .into_iter() + .map(|s_vec| vec![s_vec[1], s_vec[0]]) + .collect(), + ), + Type::AutoQuest => Value::MultiPolygon(match self.value { + Value::Polygon(geometry) => vec![geometry], + Value::MultiPolygon(polygons) => polygons, + Value::Point(point) => vec![vec![vec![point]]], + Value::MultiPoint(points) => vec![vec![points]], + Value::LineString(line) => vec![vec![line]], + Value::MultiLineString(lines) => vec![lines], + Value::GeometryCollection(_) => { + log::error!("Geometry Collections are not currently supported"); + vec![vec![vec![vec![]]]] + } + }), + _ => self.value, + } + } else { + Value::Polygon(match self.value { + Value::Polygon(geometry) => geometry, + Value::MultiPolygon(polygons) => polygons.into_iter().flatten().collect(), + Value::Point(point) => vec![vec![point]], + Value::MultiPoint(points) => vec![points], + Value::LineString(line) => vec![line], + Value::MultiLineString(lines) => lines, + Value::GeometryCollection(_) => { + log::error!("Geometry Collections are not currently supported"); + vec![vec![vec![]]] + } + }) + }, + }), ..Default::default() } } @@ -107,8 +158,8 @@ impl ToFeatureVec for Geometry { match self.value { Value::MultiPolygon(val) => val .into_iter() - .map(|polygon| Feature { - bbox: polygon + .map(|polygon| { + let bbox = polygon .clone() .into_iter() .flat_map(|x| { @@ -117,13 +168,16 @@ impl ToFeatureVec for Geometry { .collect::() }) .collect::() - .get_bbox(), - geometry: Some(Geometry { - bbox: None, - value: Value::Polygon(polygon), - foreign_members: None, - }), - ..Default::default() + .get_bbox(); + Feature { + bbox: bbox.clone(), + geometry: Some(Geometry { + bbox, + value: Value::Polygon(polygon), + foreign_members: None, + }), + ..Default::default() + } }) .collect(), Value::GeometryCollection(val) => val @@ -134,3 +188,22 @@ impl ToFeatureVec for Geometry { } } } + +impl ToCollection for Vec { + fn to_collection(self, _name: Option, enum_type: Option) -> FeatureCollection { + FeatureCollection { + bbox: self + .clone() + .into_iter() + .map(|geom| geom.to_single_vec()) + .flatten() + .collect::() + .get_bbox(), + foreign_members: None, + features: self + .into_iter() + .map(|geometry| geometry.to_feature(enum_type.clone())) + .collect(), + } + } +} diff --git a/server/model/src/api/mod.rs b/server/model/src/api/mod.rs index 900367b4..00982c84 100644 --- a/server/model/src/api/mod.rs +++ b/server/model/src/api/mod.rs @@ -27,7 +27,7 @@ pub trait EnsurePoints { } pub trait EnsureProperties { - fn ensure_properties(self, name: Option, enum_type: Option<&Type>) -> Self; + fn ensure_properties(self, name: Option, enum_type: Option) -> Self; } /// [min_lon, min_lat, max_lon, max_lat] @@ -36,7 +36,7 @@ pub trait GetBbox { } pub trait ValueHelpers { - fn get_geojson_value(self, enum_type: &Type) -> Value; + fn get_geojson_value(self, enum_type: Type) -> Value; fn point(self) -> Value; fn multi_point(self) -> Value; fn polygon(self) -> Value; @@ -48,7 +48,7 @@ pub trait GeometryHelpers { } pub trait FeatureHelpers { - fn add_instance_properties(&mut self, name: Option, enum_type: Option<&Type>); + fn add_instance_properties(&mut self, name: Option, enum_type: Option); fn remove_last_coord(self) -> Self; fn remove_internal_props(&mut self); } @@ -78,7 +78,7 @@ pub trait ToMultiStruct { } pub trait ToFeature { - fn to_feature(self, enum_type: Option<&Type>) -> Feature; + fn to_feature(self, enum_type: Option) -> Feature; } pub trait ToFeatureVec { @@ -86,7 +86,7 @@ pub trait ToFeatureVec { } pub trait ToCollection { - fn to_collection(self, name: Option, enum_type: Option<&Type>) -> FeatureCollection; + fn to_collection(self, name: Option, enum_type: Option) -> FeatureCollection; } pub trait ToPoracle { @@ -101,6 +101,10 @@ pub trait ToText { fn to_text(self, sep_1: &str, sep_2: &str, poly_sep: bool) -> String; } +pub trait ToGeometry { + fn to_geometry(self) -> Geometry; +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(untagged)] pub enum GeoFormats { @@ -109,6 +113,8 @@ pub enum GeoFormats { MultiArray(multi_vec::MultiVec), SingleStruct(single_struct::SingleStruct), MultiStruct(multi_struct::MultiStruct), + Geometry(Geometry), + GeometryVec(Vec), Feature(Feature), FeatureVec(Vec), FeatureCollection(FeatureCollection), @@ -118,7 +124,7 @@ pub enum GeoFormats { } impl ToCollection for GeoFormats { - fn to_collection(self, name: Option, enum_type: Option<&Type>) -> FeatureCollection { + fn to_collection(self, name: Option, enum_type: Option) -> FeatureCollection { // let name_clone = name.clone(); match self { GeoFormats::Text(area) => area.to_collection(name, enum_type), @@ -126,6 +132,8 @@ impl ToCollection for GeoFormats { GeoFormats::MultiArray(area) => area.to_collection(name, enum_type), GeoFormats::SingleStruct(area) => area.to_collection(name, enum_type), GeoFormats::MultiStruct(area) => area.to_collection(name, enum_type), + GeoFormats::Geometry(area) => area.to_feature(enum_type).to_collection(name, None), + GeoFormats::GeometryVec(area) => area.to_collection(name, enum_type), GeoFormats::Feature(area) => area.to_collection(name, enum_type), GeoFormats::FeatureVec(area) => area.to_collection(name, enum_type), GeoFormats::FeatureCollection(area) => area.to_collection(name, enum_type), diff --git a/server/model/src/api/multi_struct.rs b/server/model/src/api/multi_struct.rs index 3f468708..aae9e962 100644 --- a/server/model/src/api/multi_struct.rs +++ b/server/model/src/api/multi_struct.rs @@ -2,6 +2,12 @@ use super::*; pub type MultiStruct = Vec>>; +impl GetBbox for MultiStruct { + fn get_bbox(&self) -> Option { + self.clone().to_single_vec().get_bbox() + } +} + impl ToPointArray for MultiStruct { fn to_point_array(self) -> point_array::PointArray { [self[0][0].lat, self[0][0].lon] @@ -10,9 +16,7 @@ impl ToPointArray for MultiStruct { impl ToSingleVec for MultiStruct { fn to_single_vec(self) -> single_vec::SingleVec { - self.into_iter() - .map(|point| point.to_point_array()) - .collect() + self.to_multi_vec().into_iter().flatten().collect() } } @@ -47,10 +51,12 @@ impl ToMultiStruct for MultiStruct { } impl ToFeature for MultiStruct { - fn to_feature(self, enum_type: Option<&Type>) -> Feature { + fn to_feature(self, enum_type: Option) -> Feature { + let bbox = self.clone().to_single_vec().get_bbox(); Feature { + bbox: bbox.clone(), geometry: Some(Geometry { - bbox: self.clone().to_single_vec().get_bbox(), + bbox, foreign_members: None, value: if let Some(enum_type) = enum_type { self.to_multi_vec().get_geojson_value(enum_type) @@ -64,7 +70,7 @@ impl ToFeature for MultiStruct { } impl ToCollection for MultiStruct { - fn to_collection(self, _name: Option, enum_type: Option<&Type>) -> FeatureCollection { + fn to_collection(self, _name: Option, enum_type: Option) -> FeatureCollection { let feature = self.to_feature(enum_type) // .ensure_properties(name, enum_type) ; diff --git a/server/model/src/api/multi_vec.rs b/server/model/src/api/multi_vec.rs index c3f05acc..78480daf 100644 --- a/server/model/src/api/multi_vec.rs +++ b/server/model/src/api/multi_vec.rs @@ -3,7 +3,7 @@ use super::*; pub type MultiVec = Vec>>; impl ValueHelpers for MultiVec { - fn get_geojson_value(self, enum_type: &Type) -> Value { + fn get_geojson_value(self, enum_type: Type) -> Value { match enum_type { Type::AutoQuest | Type::PokemonIv | Type::AutoPokemon | Type::AutoTth => { self.multi_polygon() @@ -46,6 +46,12 @@ impl ValueHelpers for MultiVec { } } +impl GetBbox for MultiVec { + fn get_bbox(&self) -> Option { + self.clone().to_single_vec().get_bbox() + } +} + impl ToPointArray for MultiVec { fn to_point_array(self) -> point_array::PointArray { self[0][0] @@ -93,10 +99,12 @@ impl ToMultiStruct for MultiVec { } impl ToFeature for MultiVec { - fn to_feature(self, enum_type: Option<&Type>) -> Feature { + fn to_feature(self, enum_type: Option) -> Feature { + let bbox = self.get_bbox(); Feature { + bbox: bbox.clone(), geometry: Some(Geometry { - bbox: self.clone().to_single_vec().get_bbox(), + bbox, foreign_members: None, value: if let Some(enum_type) = enum_type { self.get_geojson_value(enum_type) @@ -110,7 +118,7 @@ impl ToFeature for MultiVec { } impl ToCollection for MultiVec { - fn to_collection(self, _name: Option, enum_type: Option<&Type>) -> FeatureCollection { + fn to_collection(self, _name: Option, enum_type: Option) -> FeatureCollection { let feature = self .to_feature(enum_type) // .ensure_properties(name, enum_type) diff --git a/server/model/src/api/point_array.rs b/server/model/src/api/point_array.rs index 78d79738..4fef33db 100644 --- a/server/model/src/api/point_array.rs +++ b/server/model/src/api/point_array.rs @@ -42,11 +42,12 @@ impl ToMultiStruct for PointArray { } impl ToFeature for PointArray { - fn to_feature(self, enum_type: Option<&Type>) -> Feature { + fn to_feature(self, enum_type: Option) -> Feature { + let bbox = self.clone().to_single_vec().get_bbox(); Feature { - bbox: self.clone().to_single_vec().get_bbox(), + bbox: bbox.clone(), geometry: Some(Geometry { - bbox: None, + bbox, foreign_members: None, value: if let Some(enum_type) = enum_type { self.to_multi_vec().get_geojson_value(enum_type) @@ -60,7 +61,7 @@ impl ToFeature for PointArray { } impl ToCollection for PointArray { - fn to_collection(self, _name: Option, enum_type: Option<&Type>) -> FeatureCollection { + fn to_collection(self, _name: Option, enum_type: Option) -> FeatureCollection { let feature = self .to_feature(enum_type) // .ensure_properties(name, enum_type) diff --git a/server/model/src/api/point_struct.rs b/server/model/src/api/point_struct.rs index 07cbeae4..d2334673 100644 --- a/server/model/src/api/point_struct.rs +++ b/server/model/src/api/point_struct.rs @@ -48,11 +48,12 @@ impl ToMultiStruct for PointStruct { } impl ToFeature for PointStruct { - fn to_feature(self, enum_type: Option<&Type>) -> Feature { + fn to_feature(self, enum_type: Option) -> Feature { + let bbox = self.clone().to_single_vec().get_bbox(); Feature { - bbox: self.clone().to_single_vec().get_bbox(), + bbox: bbox.clone(), geometry: Some(Geometry { - bbox: None, + bbox, foreign_members: None, value: if let Some(enum_type) = enum_type { self.to_multi_vec().get_geojson_value(enum_type) @@ -66,7 +67,7 @@ impl ToFeature for PointStruct { } impl ToCollection for PointStruct { - fn to_collection(self, _name: Option, enum_type: Option<&Type>) -> FeatureCollection { + fn to_collection(self, _name: Option, enum_type: Option) -> FeatureCollection { let feature = self .to_feature(enum_type) // .ensure_properties(name, enum_type) diff --git a/server/model/src/api/poracle.rs b/server/model/src/api/poracle.rs index 5de1c4fd..653843f4 100644 --- a/server/model/src/api/poracle.rs +++ b/server/model/src/api/poracle.rs @@ -40,14 +40,11 @@ impl Default for Poracle { } } -// impl Poracle { -// fn multipath_to_path(&mut self) { -// self.path = Some(self.clone().to_single_vec()); -// } -// fn path_to_multipath(&mut self) { -// self.multipath = Some(self.clone().to_multi_vec()) -// } -// } +impl GetBbox for Poracle { + fn get_bbox(&self) -> Option { + self.clone().to_single_vec().get_bbox() + } +} impl ToPointArray for Poracle { fn to_point_array(self) -> point_array::PointArray { @@ -134,11 +131,12 @@ impl ToMultiStruct for Poracle { } impl ToFeature for Poracle { - fn to_feature(self, enum_type: Option<&Type>) -> Feature { + fn to_feature(self, enum_type: Option) -> Feature { + let bbox = self.get_bbox(); let mut feature = Feature { - bbox: self.clone().to_single_vec().get_bbox(), + bbox: bbox.clone(), geometry: Some(Geometry { - bbox: None, + bbox, foreign_members: None, value: if let Some(enum_type) = enum_type { self.clone().to_multi_vec().get_geojson_value(enum_type) @@ -190,7 +188,7 @@ impl ToFeature for Poracle { } impl ToCollection for Poracle { - fn to_collection(self, _name: Option, enum_type: Option<&Type>) -> FeatureCollection { + fn to_collection(self, _name: Option, enum_type: Option) -> FeatureCollection { let feature = self .to_feature(enum_type) // .ensure_properties(name, enum_type) @@ -204,7 +202,7 @@ impl ToCollection for Poracle { } impl ToCollection for Vec { - fn to_collection(self, _name: Option, enum_type: Option<&Type>) -> FeatureCollection { + fn to_collection(self, _name: Option, enum_type: Option) -> FeatureCollection { // let name = if let Some(name) = name { // name // } else { @@ -222,7 +220,7 @@ impl ToCollection for Vec { .into_iter() .enumerate() .map(|(_i, poracle_feat)| { - poracle_feat.to_feature(enum_type) + poracle_feat.to_feature(enum_type.to_owned()) // .ensure_properties( // Some(if length > 1 { // format!("{}_{}", name, i) diff --git a/server/model/src/api/single_struct.rs b/server/model/src/api/single_struct.rs index 6c68182d..68dc4e30 100644 --- a/server/model/src/api/single_struct.rs +++ b/server/model/src/api/single_struct.rs @@ -2,6 +2,12 @@ use super::*; pub type SingleStruct = Vec>; +impl GetBbox for SingleStruct { + fn get_bbox(&self) -> Option { + self.clone().to_single_vec().get_bbox() + } +} + impl ToPointArray for SingleStruct { fn to_point_array(self) -> point_array::PointArray { [self[0].lat, self[0].lon] @@ -46,11 +52,12 @@ impl ToMultiStruct for SingleStruct { } impl ToFeature for SingleStruct { - fn to_feature(self, enum_type: Option<&Type>) -> Feature { + fn to_feature(self, enum_type: Option) -> Feature { + let bbox = self.get_bbox(); Feature { - bbox: self.clone().to_single_vec().get_bbox(), + bbox: bbox.clone(), geometry: Some(Geometry { - bbox: None, + bbox, foreign_members: None, value: if let Some(enum_type) = enum_type { self.to_multi_vec().get_geojson_value(enum_type) @@ -64,7 +71,7 @@ impl ToFeature for SingleStruct { } impl ToCollection for SingleStruct { - fn to_collection(self, _name: Option, enum_type: Option<&Type>) -> FeatureCollection { + fn to_collection(self, _name: Option, enum_type: Option) -> FeatureCollection { let feature = self .to_feature(enum_type) // .ensure_properties(name, enum_type) diff --git a/server/model/src/api/single_vec.rs b/server/model/src/api/single_vec.rs index 6aa53c61..8f02f5de 100644 --- a/server/model/src/api/single_vec.rs +++ b/server/model/src/api/single_vec.rs @@ -93,11 +93,12 @@ impl ToMultiStruct for SingleVec { } impl ToFeature for SingleVec { - fn to_feature(self, enum_type: Option<&Type>) -> Feature { + fn to_feature(self, enum_type: Option) -> Feature { + let bbox = self.get_bbox(); Feature { - bbox: self.get_bbox(), + bbox: bbox.clone(), geometry: Some(Geometry { - bbox: None, + bbox, foreign_members: None, value: if let Some(enum_type) = enum_type { self.to_multi_vec().get_geojson_value(enum_type) @@ -111,7 +112,7 @@ impl ToFeature for SingleVec { } impl ToCollection for SingleVec { - fn to_collection(self, _name: Option, enum_type: Option<&Type>) -> FeatureCollection { + fn to_collection(self, _name: Option, enum_type: Option) -> FeatureCollection { FeatureCollection { bbox: self.get_bbox(), features: vec![ diff --git a/server/model/src/api/text.rs b/server/model/src/api/text.rs index 9a8722cf..3d505773 100644 --- a/server/model/src/api/text.rs +++ b/server/model/src/api/text.rs @@ -2,7 +2,7 @@ use super::*; pub trait TextHelpers { fn text_test(&self) -> bool; - fn parse_scanner_instance(self, name: Option, enum_type: Option<&Type>) -> Feature; + fn parse_scanner_instance(self, name: Option, enum_type: Option) -> Feature; } impl TextHelpers for String { @@ -13,8 +13,8 @@ impl TextHelpers for String { Err(_) => false, } } - fn parse_scanner_instance(self, name: Option, enum_type: Option<&Type>) -> Feature { - let mut parsed = if self.starts_with("{") { + fn parse_scanner_instance(self, name: Option, enum_type: Option) -> Feature { + let parsed = if self.starts_with("{") { match serde_json::from_str::(&self) { Ok(result) => match result { InstanceParsing::Feature(feat) => feat, @@ -31,7 +31,7 @@ impl TextHelpers for String { } }, Err(err) => { - println!( + log::error!( "Error Parsing Instance: {}\n{}", name.clone().unwrap_or("".to_string()), err @@ -42,7 +42,6 @@ impl TextHelpers for String { } else { self.to_feature(enum_type) }; - parsed.add_instance_properties(name, enum_type); parsed } } @@ -111,12 +110,13 @@ impl ToMultiStruct for String { } impl ToFeature for String { - fn to_feature(self, enum_type: Option<&Type>) -> Feature { + fn to_feature(self, enum_type: Option) -> Feature { let multi_vec = self.to_multi_vec(); + let bbox = multi_vec.get_bbox(); Feature { - bbox: multi_vec.clone().to_single_vec().get_bbox(), + bbox: bbox.clone(), geometry: Some(Geometry { - bbox: None, + bbox, foreign_members: None, value: if let Some(enum_type) = enum_type { multi_vec.get_geojson_value(enum_type) @@ -130,7 +130,7 @@ impl ToFeature for String { } impl ToCollection for String { - fn to_collection(self, _name: Option, enum_type: Option<&Type>) -> FeatureCollection { + fn to_collection(self, _name: Option, enum_type: Option) -> FeatureCollection { let feature = self .to_feature(enum_type) // .ensure_properties(name, enum_type) diff --git a/server/model/src/db/area.rs b/server/model/src/db/area.rs index e2d520f2..2035b0d1 100644 --- a/server/model/src/db/area.rs +++ b/server/model/src/db/area.rs @@ -1,9 +1,14 @@ //! SeaORM Entity. Generated by sea-orm-codegen 0.10.1 -use super::{sea_orm_active_enums::Type, *}; +use std::collections::HashMap; + +use super::*; use sea_orm::entity::prelude::*; -use crate::api::{text::TextHelpers, ToText}; +use crate::{ + api::{text::TextHelpers, GeoFormats, ToCollection, ToText}, + utils::get_enum, +}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "area")] @@ -34,10 +39,40 @@ pub enum Relation {} impl ActiveModelBehavior for ActiveModel {} +impl Model { + fn to_feature(self, mode: String) -> Result { + if let Some(area_type) = get_enum(Some(mode.to_string())) { + let coords = match mode.as_str() { + "AutoQuest" | "AutoPokemon" | "AutoTth" | "PokemonIv" => self.geofence, + "CirclePokemon" | "CircleSmartPokemon" => self.pokemon_mode_route, + "CircleRaid" | "CircleSmartRaid" => self.fort_mode_route, + "ManualQuest" => self.quest_mode_route, + _ => None, + }; + if let Some(coords) = coords { + let mut feature = + coords.parse_scanner_instance(Some(self.name.clone()), Some(area_type.clone())); + feature.id = Some(geojson::feature::Id::String(format!( + "{}__{}__SCANNER", + self.id, area_type + ))); + feature.set_property("__id", self.id); + feature.set_property("__name", self.name); + feature.set_property("__mode", area_type.to_string()); + Ok(feature) + } else { + Err(ModelError::Custom("Unable to determine route".to_string())) + } + } else { + Err(ModelError::Custom("Area not found".to_string())) + } + } +} + pub struct Query; impl Query { - pub async fn all(conn: &DatabaseConnection) -> Result, DbErr> { + pub async fn all(conn: &DatabaseConnection) -> Result, DbErr> { let items = area::Entity::find() .select_only() .column(Column::Id) @@ -71,103 +106,169 @@ impl Query { Ok(utils::normalize::area_ref(items)) } - pub async fn route( + pub async fn feature_from_name( conn: &DatabaseConnection, area_name: &String, - area_type: &Type, - ) -> Result { + area_type: String, + ) -> Result { let item = area::Entity::find() .filter(Column::Name.eq(Value::String(Some(Box::new(area_name.to_string()))))) .one(conn) .await?; if let Some(item) = item { - let coords = match area_type { - Type::AutoQuest | Type::AutoPokemon | Type::AutoTth | Type::PokemonIv => { - item.geofence - } - Type::CirclePokemon | Type::CircleSmartPokemon => item.pokemon_mode_route, - Type::CircleRaid | Type::CircleSmartRaid => item.fort_mode_route, - Type::ManualQuest => item.quest_mode_route, - Type::Leveling => Some("".to_string()), - }; - if let Some(coords) = coords { - Ok(coords.parse_scanner_instance(Some(item.name), Some(area_type))) - } else { - Err(DbErr::Custom("No route found".to_string())) - } + item.to_feature(area_type) } else { - Err(DbErr::Custom("Area not found".to_string())) + Err(ModelError::Custom("Area not found".to_string())) } } - pub async fn save( + pub async fn feature( conn: &DatabaseConnection, - area: FeatureCollection, - ) -> Result<(usize, usize), DbErr> { - let existing = area::Entity::find() - .select_only() - .column(area::Column::Id) - .column(area::Column::Name) - .into_model::() - .all(conn) - .await?; - - let mut inserts: Vec = vec![]; - let mut update_len = 0; + id: u32, + area_type: String, + ) -> Result { + let item = area::Entity::find_by_id(id).one(conn).await?; + if let Some(item) = item { + item.to_feature(area_type) + } else { + Err(ModelError::Custom("Area not found".to_string())) + } + } - for feat in area.into_iter() { - if let Some(name) = feat.property("__name") { - if let Some(name) = name.clone().as_str() { - let name = name.to_string(); - let column = if let Some(r#type) = feat.property("__type").clone() { - if let Some(r#type) = r#type.as_str() { - println!("Instance Type: {}", r#type); - match r#type.to_lowercase().as_str() { - "circlepokemon" - | "circle_pokemon" - | "circlesmartpokemon" - | "circle_smart_pokemon" => area::Column::PokemonModeRoute, - "circleraid" | "circle_raid" | "circlesmartraid" - | "circle_smart_raid" => area::Column::FortModeRoute, - "manualquest" | "manual_quest" => area::Column::QuestModeRoute, - _ => area::Column::Geofence, - } - } else { - area::Column::Geofence + async fn upsert_feature( + conn: &DatabaseConnection, + feat: Feature, + existing: &HashMap, + inserts_updates: &mut InsertsUpdates, + ) -> Result<(), DbErr> { + if let Some(name) = feat.property("__name") { + if let Some(name) = name.as_str() { + let column = if let Some(r#type) = feat.property("__mode").clone() { + if let Some(r#type) = r#type.as_str() { + match r#type.to_lowercase().as_str() { + "circlepokemon" + | "circle_pokemon" + | "circlesmartpokemon" + | "circle_smart_pokemon" => Some(area::Column::PokemonModeRoute), + "circleraid" | "circle_raid" | "circlesmartraid" + | "circle_smart_raid" => Some(area::Column::FortModeRoute), + "manualquest" | "manual_quest" => Some(area::Column::QuestModeRoute), + "autoquest" | "auto_quest" => Some(area::Column::Geofence), + _ => None, } } else { - area::Column::Geofence - }; + None + } + } else { + None + }; + if let Some(column) = column { + let name = name.to_string(); let area = feat.to_text(" ", ",", false); + let is_update = existing.get(&name); - let is_update = existing.iter().find(|entry| entry.name == name); - if let Some(entry) = is_update { + if let Some(id) = is_update { area::Entity::update_many() .col_expr(column, Expr::value(area)) - .filter(area::Column::Id.eq(entry.id)) + .filter(area::Column::Id.eq(id.to_owned())) .exec(conn) .await?; - println!("[DB] {}.{:?} Area Updated!", name, column); - update_len += 1; + log::info!("[DB] {}.{:?} Area Updated!", name, column); + inserts_updates.updates += 1; + Ok(()) } else { - inserts.push(area::ActiveModel { - name: Set(name.to_string()), - geofence: Set(Some(area)), + log::info!("[AREA] Adding new area {}", name); + let mut new_model = ActiveModel { + name: Set(name), ..Default::default() - }) + }; + let default_model = Entity::find() + .filter(Column::Name.eq("Default")) + .one(conn) + .await?; + if let Some(default_model) = default_model { + new_model.pokemon_mode_workers = + Set(default_model.pokemon_mode_workers); + new_model.pokemon_mode_route = Set(default_model.pokemon_mode_route); + new_model.fort_mode_workers = Set(default_model.fort_mode_workers); + new_model.fort_mode_route = Set(default_model.fort_mode_route); + new_model.quest_mode_workers = Set(default_model.quest_mode_workers); + new_model.quest_mode_hours = Set(default_model.quest_mode_hours); + new_model.quest_mode_max_login_queue = + Set(default_model.quest_mode_max_login_queue); + new_model.geofence = Set(default_model.geofence); + new_model.enable_quests = Set(default_model.enable_quests); + }; + match column { + Column::Geofence => new_model.geofence = Set(Some(area)), + Column::FortModeRoute => new_model.fort_mode_route = Set(Some(area)), + Column::QuestModeRoute => new_model.quest_mode_route = Set(Some(area)), + Column::PokemonModeRoute => { + new_model.pokemon_mode_route = Set(Some(area)) + } + _ => {} + } + inserts_updates.to_insert.push(new_model); + Ok(()) } } else { - println!("[DB] Couldn't save area, name property is malformed"); + let error = format!("[AREA] Couldn't determine column for {}", name); + log::warn!("{}", error); + Err(DbErr::Custom(error)) } } else { - println!("[DB] Couldn't save area, name not found in GeoJson!"); + let error = "[AREA] Couldn't save area, name property is malformed"; + log::warn!("{}", error); + Err(DbErr::Custom(error.to_string())) + } + } else { + let error = "[AREA] Couldn't save area, name not found in GeoJson!"; + log::warn!("{}", error); + Err(DbErr::Custom(error.to_string())) + } + } + + pub async fn upsert_from_geometry( + conn: &DatabaseConnection, + area: GeoFormats, + ) -> Result<(usize, usize), DbErr> { + let existing: HashMap = area::Entity::find() + .select_only() + .column(area::Column::Id) + .column(area::Column::Name) + .into_model::() + .all(conn) + .await? + .into_iter() + .map(|model| (model.name, model.id)) + .collect(); + + let mut insert_update = InsertsUpdates:: { + to_insert: vec![], + updates: 0, + inserts: 0, + }; + match area { + GeoFormats::Feature(feat) => { + Query::upsert_feature(conn, feat, &existing, &mut insert_update).await? + } + feat => { + let fc = match feat { + GeoFormats::FeatureCollection(fc) => fc, + geometry => geometry.to_collection(None, None), + }; + for feat in fc.into_iter() { + Query::upsert_feature(conn, feat, &existing, &mut insert_update).await? + } } } - let insert_len = inserts.len(); - if !inserts.is_empty() { - area::Entity::insert_many(inserts).exec(conn).await?; - println!("Updated {} Areas", insert_len); + let insert_len = insert_update.to_insert.len(); + if !insert_update.to_insert.is_empty() { + area::Entity::insert_many(insert_update.to_insert) + .exec(conn) + .await?; + log::info!("Updated {} Areas", insert_len); } - Ok((insert_len, update_len)) + Ok((insert_len, insert_update.updates)) } } diff --git a/server/model/src/db/device.rs b/server/model/src/db/device.rs deleted file mode 100644 index 2e71eca7..00000000 --- a/server/model/src/db/device.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.10.1 - -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "device")] -pub struct Model { - #[sea_orm(primary_key, auto_increment = false)] - pub uuid: String, - pub instance_name: Option, - pub last_host: Option, - pub last_seen: u32, - #[sea_orm(unique)] - pub account_username: Option, - pub last_lat: Option, - pub last_lon: Option, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::instance::Entity", - from = "Column::InstanceName", - to = "super::instance::Column::Name", - on_update = "Cascade", - on_delete = "SetNull" - )] - Instance, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Instance.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/server/model/src/db/geofence.rs b/server/model/src/db/geofence.rs index 2430ce79..64d02f9e 100644 --- a/server/model/src/db/geofence.rs +++ b/server/model/src/db/geofence.rs @@ -1,11 +1,16 @@ //! SeaORM Entity. Generated by sea-orm-codegen 0.10.1 -use crate::api::{FeatureHelpers, ToCollection, ToFeature}; +use std::collections::HashMap; -use super::{sea_orm_active_enums::Type, *}; +use crate::{ + api::{FeatureHelpers, GeoFormats, ToCollection}, + error::ModelError, +}; + +use super::*; use geojson::GeoJson; -use sea_orm::{entity::prelude::*, InsertResult}; +use sea_orm::{entity::prelude::*, InsertResult, UpdateResult}; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] @@ -54,30 +59,25 @@ pub struct GeofenceNoGeometry { pub updated_at: DateTimeUtc, } -impl ToFeature for Model { - /// Turns a Geofence model into a feature, - /// with the rest of its columns turned into properties (starting with `__`) on that feature - fn to_feature(self, enum_type: Option<&Type>) -> Feature { - let mut feature = match Feature::from_json_value(self.area) { - Ok(feat) => feat, - Err(err) => { - println!("[ERROR] An error occurred while parsing the feature, this likely means that the `area` value is not formatted correctly for {}\n\n{:?}", self.name, err); - Feature::default() - } - }; - if let Some(enum_type) = enum_type { - feature.set_property("__koji_id", self.id); - feature.set_property("__name", self.name); - feature.set_property( - "__type", - if let Some(mode) = self.mode { - mode - } else { - enum_type.to_string() - }, - ); - } - feature +impl ToFeatureFromModel for Model { + fn to_feature(self) -> Result { + let Self { + area, + name, + id, + mode, + .. + } = self; + let mut feature = Feature::from_json_value(area)?; + feature.id = Some(geojson::feature::Id::String(format!( + "{}__{}__KOJI", + id, + mode.as_ref().unwrap_or(&"Null".to_string()) + ))); + feature.set_property("__name", name); + feature.set_property("__id", id); + feature.set_property("__mode", mode); + Ok(feature) } } @@ -99,20 +99,14 @@ impl Query { .paginate(db, posts_per_page); let total = paginator.num_items_and_pages().await?; - let results: Vec = match paginator.fetch_page(page).await { - Ok(results) => results, - Err(err) => { - println!("[Geofence] Error paginating, {:?}", err); - vec![] - } - }; + let results = paginator.fetch_page(page).await?; + let results = future::try_join_all( results .into_iter() .map(|result| Query::get_related_projects(db, result)), ) - .await - .unwrap(); + .await?; Ok(PaginateResults { results: results @@ -141,6 +135,18 @@ impl Query { Entity::find().all(db).await } + pub async fn get_json_cache(db: &DatabaseConnection) -> Result, DbErr> { + Entity::find() + .from_raw_sql(Statement::from_sql_and_values( + DbBackend::MySql, + r#"SELECT id, name, mode, JSON_EXTRACT(area, '$.geometry.type') AS geo_type FROM geofence ORDER BY name"#, + vec![], + )) + .into_json() + .all(db) + .await + } + /// Returns all Geofence models in the db without their features pub async fn get_all_no_fences( db: &DatabaseConnection, @@ -206,37 +212,58 @@ impl Query { Query::get_related_projects(db, record).await } + pub async fn update_related( + conn: &DatabaseConnection, + model: &Model, + new_name: String, + ) -> Result { + route::Entity::update_many() + .col_expr(route::Column::Name, Expr::value(new_name)) + .filter(route::Column::GeofenceId.eq(model.id.to_owned())) + .filter(route::Column::Name.eq(model.name.to_owned())) + .exec(conn) + .await + } + // Updates a Geofence model, removes internally used props pub async fn update( db: &DatabaseConnection, id: u32, new_model: Model, ) -> Result { - let old_model: Option = Entity::find_by_id(id).one(db).await?; + let old_model = Entity::find_by_id(id).one(db).await?; let new_fence = Feature::from_json_value(new_model.area); - if let Ok(mut new_feature) = new_fence { - new_feature.remove_internal_props(); - let value = GeoJson::Feature(new_feature).to_json_value(); - let mut old_model: ActiveModel = old_model.unwrap().into(); - old_model.name = Set(new_model.name.to_owned()); - old_model.area = Set(value); - old_model.mode = Set(new_model.mode); - old_model.updated_at = Set(Utc::now()); - old_model.update(db).await + + if let Some(old_model) = old_model { + if let Ok(mut new_feature) = new_fence { + new_feature.remove_internal_props(); + let value = GeoJson::Feature(new_feature).to_json_value(); + if old_model.name.ne(&new_model.name) { + Query::update_related(db, &old_model, new_model.name.clone()).await?; + }; + let mut old_model: ActiveModel = old_model.into(); + old_model.name = Set(new_model.name.to_owned()); + old_model.area = Set(value); + old_model.mode = Set(new_model.mode); + old_model.updated_at = Set(Utc::now()); + old_model.update(db).await + } else { + Err(DbErr::Custom( + "New area was not a GeoJSON Feature".to_string(), + )) + } } else { - Err(DbErr::Custom( - "New area was not a GeoJSON Feature".to_string(), - )) + Err(DbErr::Custom("Could not find geofence model".to_string())) } } - // Deletes a Geofence model from db + /// Deletes a Geofence model from db pub async fn delete(db: &DatabaseConnection, id: u32) -> Result { let record = Entity::delete_by_id(id).exec(db).await?; Ok(record) } - // Returns all geofence models as a FeatureCollection, + /// Returns all geofence models as a FeatureCollection, pub async fn as_collection(conn: &DatabaseConnection) -> Result { let items = Entity::find() .order_by(Column::Name, Order::Asc) @@ -244,25 +271,39 @@ impl Query { .await?; let items: Vec = items .into_iter() - .map(|item| item.to_feature(Some(&Type::AutoQuest))) + .filter_map(|item| item.to_feature().ok()) .collect(); Ok(items.to_collection(None, None)) } /// Returns a feature for a route queried by name - pub async fn route( + pub async fn feature_from_name( conn: &DatabaseConnection, - instance_name: &String, - ) -> Result { + name: &String, + ) -> Result { let item = Entity::find() - .filter(Column::Name.eq(Value::String(Some(Box::new(instance_name.to_string()))))) + .filter(Column::Name.eq(Value::String(Some(Box::new(name.to_string()))))) .one(conn) .await?; if let Some(item) = item { - Ok(item.to_feature(Some(&Type::AutoQuest))) + item.to_feature() + } else { + Err(ModelError::Database(DbErr::Custom( + "Geofence not found".to_string(), + ))) + } + } + + /// Returns a feature for a route queried by name + pub async fn feature(conn: &DatabaseConnection, id: u32) -> Result { + let item = Entity::find_by_id(id).one(conn).await?; + if let Some(item) = item { + item.to_feature() } else { - Err(DbErr::Custom("Instance not found".to_string())) + Err(ModelError::Database(DbErr::Custom( + "Geofence not found".to_string(), + ))) } } @@ -284,9 +325,106 @@ impl Query { .await } - pub async fn save( + async fn upsert_feature( + conn: &DatabaseConnection, + feat: Feature, + existing: &HashMap, + inserts_updates: &mut InsertsUpdates, + ) -> Result<(), DbErr> { + if let Some(name) = feat.property("__name") { + if let Some(name) = name.as_str() { + let mut feat = feat.clone(); + feat.id = None; + let mode = if let Some(r#type) = feat.property("__mode") { + if let Some(r#type) = r#type.as_str() { + Some(r#type.to_string()) + } else { + None + } + } else { + None + }; + let projects: Option> = if let Some(projects) = feat.property("__projects") + { + if let Some(projects) = projects.as_array() { + Some( + projects + .iter() + .filter_map(|project| { + if let Some(project) = project.as_u64() { + Some(project) + } else { + None + } + }) + .collect(), + ) + } else { + None + } + } else { + None + }; + feat.remove_internal_props(); + feat.id = None; + let area = GeoJson::Feature(feat).to_json_value(); + if let Some(area) = area.as_object() { + let area = sea_orm::JsonValue::Object(area.to_owned()); + let name = name.to_string(); + let is_update = existing.get(&name); + + if let Some(entry) = is_update { + let old_model: Option = + Entity::find_by_id(entry.clone()).one(conn).await?; + let mut old_model: ActiveModel = old_model.unwrap().into(); + old_model.area = Set(area); + old_model.mode = Set(mode); + old_model.updated_at = Set(Utc::now()); + let model = old_model.update(conn).await?; + + if let Some(projects) = projects { + Query::insert_related_projects(conn, projects, model.id).await?; + }; + inserts_updates.updates += 1; + Ok(()) + } else { + let model = ActiveModel { + name: Set(name.to_string()), + area: Set(area), + mode: Set(mode), + created_at: Set(Utc::now()), + updated_at: Set(Utc::now()), + ..Default::default() + } + .insert(conn) + .await?; + + if let Some(projects) = projects { + Query::insert_related_projects(conn, projects, model.id).await?; + }; + inserts_updates.inserts += 1; + Ok(()) + } + } else { + let error = format!("[AREA] unable to serialize the feature: {}", name); + log::warn!("{}", error); + Err(DbErr::Custom(error)) + } + } else { + let error = "[AREA] Couldn't save area, name property is malformed"; + log::warn!("{}", error); + Err(DbErr::Custom(error.to_string())) + } + } else { + let error = "[AREA] Couldn't save area, name not found in GeoJson!"; + log::warn!("{}", error); + Err(DbErr::Custom(error.to_string())) + } + } + + pub async fn upsert_from_geometry( conn: &DatabaseConnection, - area: FeatureCollection, + area: GeoFormats, ) -> Result<(usize, usize), DbErr> { let existing = Entity::find() .select_only() @@ -294,100 +432,42 @@ impl Query { .column(Column::Name) .into_model::() .all(conn) - .await?; + .await? + .into_iter() + .map(|model| (model.name, model.id)) + .collect(); - let mut inserts = 0; - let mut update_len = 0; - - for feat in area.into_iter() { - if let Some(name) = feat.property("__name") { - if let Some(name) = name.as_str() { - let mut feat = feat.clone(); - feat.id = None; - let mode = if let Some(r#type) = feat.property("__type") { - println!("Type? {:?}", r#type); - if let Some(r#type) = r#type.as_str() { - Some(r#type.to_string()) - } else { - None - } - } else { - None - }; - let projects: Option> = - if let Some(projects) = feat.property("__projects") { - if let Some(projects) = projects.as_array() { - Some( - projects - .iter() - .filter_map(|project| { - if let Some(project) = project.as_u64() { - Some(project) - } else { - None - } - }) - .collect(), - ) - } else { - None - } - } else { - None - }; - feat.remove_internal_props(); - let area = GeoJson::Feature(feat).to_json_value(); - if let Some(area) = area.as_object() { - let area = sea_orm::JsonValue::Object(area.to_owned()); - let name = name.to_string(); - let is_update = existing.iter().find(|entry| entry.name == name); - - if let Some(entry) = is_update { - let old_model: Option = - Entity::find_by_id(entry.id).one(conn).await?; - let mut old_model: ActiveModel = old_model.unwrap().into(); - old_model.area = Set(area); - old_model.mode = Set(mode); - old_model.updated_at = Set(Utc::now()); - let model = old_model.update(conn).await?; - - if let Some(projects) = projects { - Query::insert_related_projects(conn, projects, model.id).await?; - }; - update_len += 1; - } else { - let model = ActiveModel { - name: Set(name.to_string()), - area: Set(area), - mode: Set(mode), - created_at: Set(Utc::now()), - updated_at: Set(Utc::now()), - ..Default::default() - } - .insert(conn) - .await?; - - if let Some(projects) = projects { - Query::insert_related_projects(conn, projects, model.id).await?; - }; - inserts += 1; - } - } + let mut inserts_updates = InsertsUpdates:: { + to_insert: vec![], + updates: 0, + inserts: 0, + }; + match area { + GeoFormats::Feature(feat) => { + Query::upsert_feature(conn, feat, &existing, &mut inserts_updates).await? + } + feat => { + let fc = match feat { + GeoFormats::FeatureCollection(fc) => fc, + geometry => geometry.to_collection(None, None), + }; + for feat in fc.into_iter() { + Query::upsert_feature(conn, feat, &existing, &mut inserts_updates).await? } } } - Ok((inserts, update_len)) + Ok((inserts_updates.inserts, inserts_updates.updates)) } /// Returns all geofence models, as features, that are related to the specified project pub async fn by_project( conn: &DatabaseConnection, - project_id: String, + project_name: String, ) -> Result, DbErr> { let items = Entity::find() .order_by(Column::Name, Order::Asc) .left_join(project::Entity) - .filter(project::Column::Name.eq(project_id)) + .filter(project::Column::Name.eq(project_name)) .all(conn) .await?; diff --git a/server/model/src/db/instance.rs b/server/model/src/db/instance.rs index bedf3982..1a1a583a 100644 --- a/server/model/src/db/instance.rs +++ b/server/model/src/db/instance.rs @@ -2,21 +2,33 @@ use std::collections::HashMap; -use crate::api::{ToMultiStruct, ToMultiVec, ToPointStruct, ToSingleStruct, ToSingleVec}; +use crate::{ + api::{ + text::TextHelpers, GeoFormats, ToCollection, ToMultiStruct, ToMultiVec, ToPointStruct, + ToSingleStruct, ToSingleVec, + }, + error::ModelError, + utils::get_mode_acronym, +}; use super::{ - sea_orm_active_enums::Type, utils, Feature, FeatureCollection, NameType, NameTypeId, Order, - QueryOrder, RdmInstanceArea, + sea_orm_active_enums::Type, utils, Feature, InsertsUpdates, NameTypeId, Order, QueryOrder, + RdmInstanceArea, ToFeatureFromModel, }; -use sea_orm::{entity::prelude::*, sea_query::Expr, QuerySelect, Set}; +use sea_orm::{ + entity::prelude::*, sea_query::Expr, DbBackend, FromQueryResult, QuerySelect, Set, Statement, +}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] #[sea_orm(table_name = "instance")] pub struct Model { - #[sea_orm(primary_key, auto_increment = false)] + #[sea_orm(primary_key)] + #[serde(skip_deserializing)] + pub id: u32, + #[sea_orm(unique)] pub name: String, pub r#type: Type, #[sea_orm(column_type = "Custom(\"LONGTEXT\".to_owned())")] @@ -24,23 +36,37 @@ pub struct Model { } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::device::Entity")] - Device, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Device.def() - } -} +pub enum Relation {} impl ActiveModelBehavior for ActiveModel {} -struct Instance { - name: String, - // r#type: Type, - data: HashMap, +#[derive(Debug, Serialize, FromQueryResult)] +pub struct WithGeoType { + pub id: u32, + pub name: String, + pub mode: Type, + pub geo_type: String, +} + +impl ToFeatureFromModel for Model { + fn to_feature(self) -> Result { + let Self { + id, + name, + r#type, + data, + .. + } = self; + let mut feature = data.parse_scanner_instance(Some(name.clone()), Some(r#type.clone())); + feature.id = Some(geojson::feature::Id::String(format!( + "{}__{}__SCANNER", + id, r#type + ))); + feature.set_property("__name", name); + feature.set_property("__mode", r#type.to_string()); + feature.set_property("__id", id); + Ok(feature) + } } pub struct Query; @@ -51,138 +77,310 @@ impl Query { instance_type: Option, ) -> Result, DbErr> { let instance_type = utils::get_enum(instance_type); - let items = if let Some(instance_type) = instance_type { + if let Some(instance_type) = instance_type { Entity::find() .filter(Column::Type.eq(instance_type)) .select_only() .column(Column::Name) - .column_as(Column::Type, "instance_type") + .column_as(Column::Type, "mode") .order_by(Column::Name, Order::Asc) - .into_model::() + .into_model::() .all(conn) - .await? + .await } else { Entity::find() .select_only() + .column(Column::Id) .column(Column::Name) - .column_as(Column::Type, "instance_type") + .column_as(Column::Type, "mode") .order_by(Column::Name, Order::Asc) - .into_model::() + .into_model::() .all(conn) - .await? - }; - Ok(items - .into_iter() - .enumerate() - .map(|(i, item)| NameTypeId { - id: i as u32, - name: item.name, - r#type: Some(item.instance_type), - }) - .collect()) + .await + } } - pub async fn route( + pub async fn get_json_cache(db: &DatabaseConnection) -> Result, DbErr> { + let entries = Entity::find() + .from_raw_sql(Statement::from_sql_and_values( + DbBackend::MySql, + r#"SELECT id, name, type AS mode, + CASE + WHEN type = 'leveling' THEN 'Point' + WHEN type LIKE 'circle_%' THEN 'MultiPoint' + ELSE 'MultiPolygon' + END AS geo_type + FROM instance + ORDER BY name + "#, + vec![], + )) + .into_model::() + .all(db) + .await?; + Ok(entries.into_iter().map(|entry| json!(entry)).collect()) + } + + pub async fn feature_from_name( conn: &DatabaseConnection, - instance_name: &String, - ) -> Result { - let items = Entity::find() - .filter(Column::Name.eq(instance_name.trim().to_string())) + name: &String, + ) -> Result { + let item = Entity::find() + .filter(Column::Name.eq(name.trim().to_string())) .one(conn) .await?; - if let Some(items) = items { - Ok(utils::normalize::instance(items)) + if let Some(item) = item { + item.to_feature() } else { - Err(DbErr::Custom("Instance not found".to_string())) + Err(ModelError::Custom("Instance not found".to_string())) } } - pub async fn save( - conn: &DatabaseConnection, - area: FeatureCollection, - ) -> Result<(usize, usize), DbErr> { - let existing = Entity::find().all(conn).await?; - let mut existing: Vec = existing - .into_iter() - .map(|x| Instance { - name: x.name, - // r#type: x.r#type, - data: serde_json::from_str(&x.data).unwrap(), - }) - .collect(); + pub async fn feature(conn: &DatabaseConnection, id: u32) -> Result { + let item = Entity::find_by_id(id).one(conn).await?; + if let Some(item) = item { + item.to_feature() + } else { + Err(ModelError::Custom("Instance not found".to_string())) + } + } - let mut inserts: Vec = vec![]; - let mut update_len = 0; + async fn get_by_name(conn: &DatabaseConnection, name: String) -> Result, DbErr> { + Entity::find().filter(Column::Name.eq(name)).one(conn).await + } - for feat in area.into_iter() { - if let Some(name) = feat.property("__name") { - if let Some(name) = name.as_str() { - let r#type = if let Some(instance_type) = feat.property("__type") { - if let Some(instance_type) = instance_type.as_str() { - utils::get_enum(Some(instance_type.to_string())) - } else { - utils::get_enum_by_geometry(&feat.geometry.as_ref().unwrap().value) - } + async fn get_default( + conn: &DatabaseConnection, + short: &String, + mode: &Type, + ) -> Result, DbErr> { + if let Some(default_model) = Query::get_by_name(conn, format!("Default_{}", short)).await? { + log::info!("Found default for Default_{}", short); + match serde_json::from_str(&default_model.data) { + Ok(defaults) => Ok(defaults), + Err(err) => Err(DbErr::Custom(err.to_string())), + } + } else if let Some(default_model) = + Query::get_by_name(conn, format!("Default_{}", mode)).await? + { + log::info!("Found default for Default_{}", mode); + match serde_json::from_str(&default_model.data) { + Ok(defaults) => Ok(defaults), + Err(err) => Err(DbErr::Custom(err.to_string())), + } + } else { + Ok(HashMap::new()) + } + } + + async fn upsert_feature( + conn: &DatabaseConnection, + feat: Feature, + existing: &HashMap, + inserts_updates: &mut InsertsUpdates, + ) -> Result<(), DbErr> { + if let Some(name) = feat.property("__name") { + if let Some(name) = name.as_str() { + let mode = if let Some(instance_type) = feat.property("__mode") { + if let Some(instance_type) = instance_type.as_str() { + utils::get_enum(Some(instance_type.to_string())) } else { utils::get_enum_by_geometry(&feat.geometry.as_ref().unwrap().value) + } + } else { + utils::get_enum_by_geometry(&feat.geometry.as_ref().unwrap().value) + }; + if let Some(mode) = mode { + let area = match mode { + Type::CirclePokemon + | Type::CircleSmartPokemon + | Type::CircleRaid + | Type::CircleSmartRaid + | Type::ManualQuest => { + RdmInstanceArea::Single(feat.clone().to_single_vec().to_single_struct()) + } + Type::Leveling => { + RdmInstanceArea::Leveling(feat.clone().to_single_vec().to_struct()) + } + Type::AutoQuest | Type::PokemonIv | Type::AutoPokemon | Type::AutoTth => { + RdmInstanceArea::Multi(feat.clone().to_multi_vec().to_multi_struct()) + } + }; + let new_area = json!(area); + let id = if let Some(id) = feat.property("__id") { + id.as_u64() + } else { + log::info!( + "ID not found, attempting to save by name ({}) and mode ({})", + name, + mode + ); + None }; - if let Some(r#type) = r#type { - let area = match r#type { - Type::CirclePokemon - | Type::CircleSmartPokemon - | Type::CircleRaid - | Type::CircleSmartRaid - | Type::ManualQuest => RdmInstanceArea::Single( - feat.clone().to_single_vec().to_single_struct(), - ), - Type::Leveling => { - RdmInstanceArea::Leveling(feat.clone().to_single_vec().to_struct()) + if let Some(id) = id { + let model = Entity::find_by_id(id as u32).one(conn).await?; + if let Some(model) = model { + let mut data: HashMap = + serde_json::from_str(&model.data).unwrap(); + data.insert("area".to_string(), new_area); + + if let Ok(data) = serde_json::to_string(&data) { + let mut model: ActiveModel = model.into(); + model.data = Set(data); + match model.update(conn).await { + Ok(_) => { + log::info!("Successfully updated {}", id); + Ok(()) + } + Err(err) => { + let error = format!("Unable to update {}: {:?}", id, err); + log::error!("{}", error); + Err(DbErr::Custom(error)) + } + } + } else { + let error = format!("Unable to serialize json: {:?}", data); + log::error!("{}", error); + Err(DbErr::Custom(error)) } - Type::AutoQuest - | Type::PokemonIv - | Type::AutoPokemon - | Type::AutoTth => RdmInstanceArea::Multi( - feat.clone().to_multi_vec().to_multi_struct(), - ), - }; - let new_area = json!(area); + } else { + let error = format!( + "Found an ID but was unable to find the record in the db: {}", + id + ); + log::error!("{}", error); + Err(DbErr::Custom(error)) + } + } else { let name = name.to_string(); - let is_update = existing.iter_mut().find(|entry| entry.name == name); - + let is_update = existing.get(&name); + let short = get_mode_acronym(Some(&mode.to_string())); if let Some(entry) = is_update { - entry.data.insert("area".to_string(), new_area); - Entity::update_many() - .col_expr(Column::Data, Expr::value(json!(entry.data).to_string())) - .col_expr(Column::Type, Expr::value(r#type)) - .filter(Column::Name.eq(entry.name.to_string())) - .exec(conn) - .await?; - update_len += 1; + if entry.r#type == mode { + let mut data: HashMap = + serde_json::from_str(&entry.data).unwrap(); + data.insert("area".to_string(), new_area); + + Entity::update_many() + .col_expr(Column::Data, Expr::value(json!(data).to_string())) + .filter(Column::Id.eq(entry.id)) + .exec(conn) + .await?; + inserts_updates.updates += 1; + Ok(()) + } else if let Some(actual_entry) = + existing.get(&format!("{}_{}", name, short)) + { + let mut data: HashMap = + serde_json::from_str(&actual_entry.data).unwrap(); + data.insert("area".to_string(), new_area); + + Entity::update_many() + .col_expr(Column::Data, Expr::value(json!(data).to_string())) + .filter(Column::Name.eq(actual_entry.name.to_string())) + .exec(conn) + .await?; + inserts_updates.updates += 1; + Ok(()) + } else { + let mut active_model = ActiveModel { + name: Set(name.to_string()), + ..Default::default() + }; + let mut data = Query::get_default(conn, &short, &mode).await?; + data.insert("area".to_string(), new_area); + + active_model + .set_from_json(json!({ + "name": format!("{}_{}", name, short), + "type": mode, + "data": json!(data).to_string(), + })) + .unwrap(); + + inserts_updates.inserts += 1; + inserts_updates.to_insert.push(active_model); + Ok(()) + } } else { let mut active_model = ActiveModel { name: Set(name.to_string()), - // r#type: Set(r#type), - // data: Set(json!({ "area": new_area }).to_string()), ..Default::default() }; + let mut data = Query::get_default(conn, &short, &mode).await?; + data.insert("area".to_string(), new_area); + active_model .set_from_json(json!({ "name": name, - "type": r#type, - "data": json!({ "area": new_area }).to_string(), + "type": mode, + "data": json!(data).to_string(), })) .unwrap(); - - inserts.push(active_model) + inserts_updates.inserts += 1; + inserts_updates.to_insert.push(active_model); + Ok(()) } } + } else { + let error = format!("Unable to determine mode | {:?}", mode); + log::warn!("{}", error); + Err(DbErr::Custom(error)) + } + } else { + let error = format!( + "Name property is not a properly formatted string | {}", + name + ); + log::warn!("{}", error); + Err(DbErr::Custom(error)) + } + } else { + let error = format!( + "Name not found, unable to save feature {:?}", + feat.properties + ); + log::warn!("{}", error); + Err(DbErr::Custom(error)) + } + } + + pub async fn upsert_from_geometry( + conn: &DatabaseConnection, + area: GeoFormats, + _auto_mode: bool, + ) -> Result<(usize, usize), DbErr> { + let existing: HashMap = Entity::find() + .all(conn) + .await? + .into_iter() + .map(|model| (model.name.to_string(), model)) + .collect(); + let mut inserts_updates = InsertsUpdates:: { + inserts: 0, + updates: 0, + to_insert: vec![], + }; + + match area { + GeoFormats::Feature(feat) => { + Query::upsert_feature(conn, feat, &existing, &mut inserts_updates).await? + } + feat => { + let fc = match feat { + GeoFormats::FeatureCollection(fc) => fc, + geometry => geometry.to_collection(None, None), + }; + for feat in fc.into_iter() { + Query::upsert_feature(conn, feat, &existing, &mut inserts_updates).await? } } } - let insert_len = inserts.len(); - if !inserts.is_empty() { - Entity::insert_many(inserts).exec(conn).await?; + if !inserts_updates.to_insert.is_empty() { + Entity::insert_many(inserts_updates.to_insert) + .exec(conn) + .await?; } - Ok((insert_len, update_len)) + Ok((inserts_updates.inserts, inserts_updates.updates)) } } diff --git a/server/model/src/db/mod.rs b/server/model/src/db/mod.rs index f968fcd0..5cac5095 100644 --- a/server/model/src/db/mod.rs +++ b/server/model/src/db/mod.rs @@ -1,5 +1,7 @@ //! SeaORM Entity. Generated by sea-orm-codegen 0.10.1 +use crate::error::ModelError; + use super::*; use chrono::Utc; @@ -10,7 +12,6 @@ use sea_orm::{ }; pub mod area; -pub mod device; pub mod geofence; pub mod geofence_project; pub mod gym; @@ -22,6 +23,10 @@ pub mod route; pub mod sea_orm_active_enums; pub mod spawnpoint; +trait ToFeatureFromModel { + fn to_feature(self) -> Result; +} + #[derive(Debug, Serialize, FromQueryResult)] pub struct NameId { id: u32, @@ -34,11 +39,12 @@ pub struct NameType { pub instance_type: self::sea_orm_active_enums::Type, } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Clone, FromQueryResult, Serialize, Deserialize)] pub struct NameTypeId { pub id: u32, pub name: String, - pub r#type: Option, + pub mode: Option, + pub geo_type: Option, } #[derive(Debug, FromQueryResult)] @@ -134,3 +140,9 @@ pub enum InstanceParsing { Feature(Feature), Rdm(RdmInstance), } + +pub struct InsertsUpdates { + to_insert: Vec, + updates: usize, + inserts: usize, +} diff --git a/server/model/src/db/prelude.rs b/server/model/src/db/prelude.rs index 9852716a..dc15fffb 100644 --- a/server/model/src/db/prelude.rs +++ b/server/model/src/db/prelude.rs @@ -1,6 +1,5 @@ //! SeaORM Entity. Generated by sea-orm-codegen 0.10.1 -pub use super::device::Entity as Device; pub use super::geofence::Entity as Geofence; pub use super::geofence_project::Entity as GeofenceProject; pub use super::gym::Entity as Gym; diff --git a/server/model/src/db/project.rs b/server/model/src/db/project.rs index e8eaa9ad..3727b543 100644 --- a/server/model/src/db/project.rs +++ b/server/model/src/db/project.rs @@ -9,6 +9,9 @@ pub struct Model { #[sea_orm(primary_key)] pub id: u32, pub name: String, + pub api_endpoint: Option, + pub api_key: Option, + pub scanner: bool, pub created_at: DateTimeUtc, pub updated_at: DateTimeUtc, } @@ -74,6 +77,18 @@ impl Query { project::Entity::find().all(db).await } + pub async fn get_json_cache(db: &DatabaseConnection) -> Result, DbErr> { + Entity::find() + .from_raw_sql(Statement::from_sql_and_values( + DbBackend::MySql, + r#"SELECT id, name FROM project ORDER BY name"#, + vec![], + )) + .into_json() + .all(db) + .await + } + pub async fn get_related_fences( db: &DatabaseConnection, model: Model, @@ -92,6 +107,9 @@ impl Query { pub async fn create(db: &DatabaseConnection, new_project: Model) -> Result { ActiveModel { name: Set(new_project.name.to_owned()), + api_endpoint: Set(new_project.api_endpoint.to_owned()), + api_key: Set(new_project.api_key.to_owned()), + scanner: Set(new_project.scanner.to_owned()), created_at: Set(Utc::now()), updated_at: Set(Utc::now()), ..Default::default() @@ -106,6 +124,14 @@ impl Query { Query::get_related_fences(db, record).await } + pub async fn get_scanner_project(db: &DatabaseConnection) -> Result, DbErr> { + project::Entity::find() + .filter(Column::Scanner.eq(true)) + .filter(Column::ApiEndpoint.is_not_null()) + .one(db) + .await + } + pub async fn update( db: &DatabaseConnection, id: u32, @@ -113,7 +139,11 @@ impl Query { ) -> Result { let old_model: Option = project::Entity::find_by_id(id).one(db).await?; let mut old_model: ActiveModel = old_model.unwrap().into(); + old_model.name = Set(new_model.name.to_owned()); + old_model.api_endpoint = Set(new_model.api_endpoint.to_owned()); + old_model.api_key = Set(new_model.api_key.to_owned()); + old_model.scanner = Set(new_model.scanner.to_owned()); old_model.updated_at = Set(Utc::now()); old_model.update(db).await } diff --git a/server/model/src/db/route.rs b/server/model/src/db/route.rs index c012f8c8..338e3c62 100644 --- a/server/model/src/db/route.rs +++ b/server/model/src/db/route.rs @@ -3,8 +3,9 @@ use std::collections::HashMap; use crate::{ - api::FeatureHelpers, - utils::{get_enum, get_mode_acronym}, + api::{GeoFormats, ToCollection, ToFeature}, + db::sea_orm_active_enums::Type, + error::ModelError, }; use super::*; @@ -20,6 +21,7 @@ pub struct Model { pub id: u32, pub geofence_id: u32, pub name: String, + pub description: Option, pub mode: String, pub geometry: Json, pub created_at: DateTimeUtc, @@ -51,6 +53,7 @@ pub struct RouteNoGeometry { pub id: u32, pub geofence_id: u32, pub name: String, + pub description: Option, pub mode: String, pub created_at: DateTimeUtc, pub updated_at: DateTimeUtc, @@ -61,10 +64,37 @@ pub struct Paginated { pub id: u32, pub geofence_id: u32, pub name: String, + pub description: Option, pub mode: String, pub hops: usize, } +impl ToFeatureFromModel for Model { + fn to_feature(self) -> Result { + let Self { + geometry, + name, + geofence_id, + id, + mode, + .. + } = self; + + let geometry = Geometry::from_json_value(geometry)?; + let mut feature = geometry.to_feature(Some(Type::CirclePokemon)); + + feature.id = Some(geojson::feature::Id::String(format!( + "{}__{}__KOJI", + id, mode + ))); + feature.set_property("__name", name); + feature.set_property("__id", id); + feature.set_property("__geofence_id", geofence_id); + feature.set_property("__mode", mode); + + Ok(feature) + } +} pub struct Query; impl Query { @@ -86,7 +116,7 @@ impl Query { let results: Vec = match paginator.fetch_page(page).await { Ok(results) => results, Err(err) => { - println!("Error paginating, {:?}", err); + log::warn!("Error paginating, {:?}", err); vec![] } } @@ -95,13 +125,14 @@ impl Query { id: model.id, geofence_id: model.geofence_id, name: model.name, + description: model.description, hops: match Geometry::from_json_value(model.geometry) { Ok(geometry) => match geometry.value { geojson::Value::MultiPoint(mp) => mp.len(), _ => 0, }, Err(err) => { - println!("[Route] Error unwrapping geometry, {:?}", err); + log::warn!("[Route] Error unwrapping geometry, {:?}", err); 0 } }, @@ -122,6 +153,18 @@ impl Query { Entity::find().all(db).await } + pub async fn get_json_cache(db: &DatabaseConnection) -> Result, DbErr> { + Entity::find() + .from_raw_sql(Statement::from_sql_and_values( + DbBackend::MySql, + r#"SELECT id, name, geofence_id, mode, JSON_EXTRACT(geometry, '$.type') AS geo_type FROM route ORDER BY name"#, + vec![], + )) + .into_json() + .all(db) + .await + } + /// Returns all Geofence models in the db without their features pub async fn get_all_no_fences(db: &DatabaseConnection) -> Result, DbErr> { Entity::find() @@ -129,6 +172,7 @@ impl Query { .column(Column::Id) .column(Column::GeofenceId) .column(Column::Name) + .column(Column::Description) .column(Column::Mode) .column(Column::CreatedAt) .column(Column::UpdatedAt) @@ -150,6 +194,7 @@ impl Query { geofence_id: Set(incoming.geofence_id), geometry: Set(value), mode: Set(incoming.mode), + description: Set(incoming.description), created_at: Set(Utc::now()), updated_at: Set(Utc::now()), ..Default::default() @@ -182,6 +227,7 @@ impl Query { let mut old_model: ActiveModel = old_model.unwrap().into(); old_model.name = Set(new_model.name.to_owned()); + old_model.description = Set(new_model.description.to_owned()); old_model.geofence_id = Set(new_model.geofence_id); old_model.geometry = Set(value); old_model.mode = Set(new_model.mode); @@ -199,162 +245,207 @@ impl Query { } /// Returns a feature for a route queried by name - pub async fn route( + pub async fn feature_from_name( conn: &DatabaseConnection, - instance_name: &String, - ) -> Result { + name: String, + ) -> Result { let item = Entity::find() - .filter(Column::Name.eq(Value::String(Some(Box::new(instance_name.to_string()))))) + .filter(Column::Name.eq(Value::String(Some(Box::new(name.to_string()))))) .one(conn) .await?; if let Some(item) = item { - match Geometry::from_json_value(item.geometry) { - Ok(geometry) => Ok({ - let mut new_feature = Feature { - geometry: Some(geometry), - ..Default::default() - }; - let mode = get_enum(Some(item.mode)); - if let Some(mode) = mode { - new_feature.set_property("__koji_id", item.id); - new_feature.set_property("__geofence_id", item.geofence_id); - new_feature.add_instance_properties(Some(item.name), Some(&mode)); - } - new_feature - }), - Err(err) => { - println!("Unable to parse geometry for, {}", item.name); - Err(DbErr::Custom(format!("{:?}", err))) - } - } + item.to_feature() } else { - Err(DbErr::Custom("Route not found".to_string())) + Err(ModelError::Custom("Route not found".to_string())) } } - pub async fn upsert_from_collection( - conn: &DatabaseConnection, - area: FeatureCollection, - auto_mode: bool, - bootstrap: bool, - ) -> Result<(usize, usize), DbErr> { - let existing: HashMap = Query::get_all_no_fences(conn) - .await? + /// Returns a feature for a route queried by name + pub async fn feature(conn: &DatabaseConnection, id: u32) -> Result { + let item = Entity::find_by_id(id).one(conn).await?; + if let Some(item) = item { + item.to_feature() + } else { + Err(ModelError::Custom("Route not found".to_string())) + } + } + + /// Returns all route models as a FeatureCollection, + pub async fn as_collection(conn: &DatabaseConnection) -> Result { + let items = Entity::find() + .order_by(Column::Name, Order::Asc) + .all(conn) + .await?; + let items: Vec = items .into_iter() - .map(|model| (format!("{}_{}", model.name, model.mode), model)) + .filter_map(|item| item.to_feature().ok()) .collect(); - let mut inserts = 0; - let mut update_len = 0; - - for feat in area.into_iter() { - if let Some(name) = feat.property("__name") { - if let Some(name) = name.as_str() { - if let Some(mode) = feat.property("__type") { - if let Some(mode) = mode.as_str() { - let geofence_id = if let Some(fence_id) = feat.property("__geofence_id") - { - fence_id.as_u64() + Ok(items.to_collection(None, None)) + } + + async fn upsert_feature( + conn: &DatabaseConnection, + feat: Feature, + existing: &HashMap, + inserts_updates: &mut InsertsUpdates, + ) -> Result<(), DbErr> { + if let Some(name) = feat.property("__name") { + if let Some(name) = name.as_str() { + if let Some(mode) = feat.property("__mode") { + if let Some(mode) = mode.as_str() { + let geofence_id = if let Some(fence_id) = feat.property("__geofence_id") { + fence_id.as_u64() + } else { + let geofence = geofence::Entity::find() + .filter( + geofence::Column::Name + .eq(Value::String(Some(Box::new(name.to_string())))), + ) + .one(conn) + .await?; + if let Some(geofence) = geofence { + Some(geofence.id as u64) } else { - let geofence = geofence::Entity::find() - .filter( - geofence::Column::Name - .eq(Value::String(Some(Box::new(name.to_string())))), - ) - .one(conn) - .await?; - if let Some(geofence) = geofence { - Some(geofence.id as u64) - } else { - None - } - }; - if let Some(fence_id) = geofence_id { - if let Some(geometry) = feat.geometry.clone() { - let geometry = GeoJson::Geometry(geometry).to_json_value(); - if let Some(geometry) = geometry.as_object() { - let geometry = - sea_orm::JsonValue::Object(geometry.to_owned()); - let name = name.to_string(); - let mode = mode.to_string(); - let name = if auto_mode { - format!( - "{}_{}", - name, - if bootstrap { - "BS".to_string() - } else { - get_mode_acronym(Some(&mode)) - } - ) - } else { - name - }; - let is_update = existing.get(&format!("{}_{}", name, mode)); - let update_bool = is_update.is_some(); - - let mut active_model = if let Some(entry) = is_update { - Entity::find_by_id(entry.id) - .one(conn) - .await? - .unwrap() - .into() - } else { - ActiveModel { - ..Default::default() - } - }; - active_model.geofence_id = Set(fence_id as u32); - active_model.geometry = Set(geometry); - active_model.mode = Set(mode); - active_model.updated_at = Set(Utc::now()); - - if update_bool { - active_model.update(conn).await?; - update_len += 1; - } else { - active_model.name = Set(name); - active_model.created_at = Set(Utc::now()); - active_model.insert(conn).await?; - inserts += 1; + None + } + }; + if let Some(fence_id) = geofence_id { + if let Some(geometry) = feat.geometry.clone() { + let geometry = GeoJson::Geometry(geometry).to_json_value(); + if let Some(geometry) = geometry.as_object() { + let geometry = sea_orm::JsonValue::Object(geometry.to_owned()); + let name = name.to_string(); + let mode = mode.to_string(); + let is_update = existing.get(&format!("{}_{}", name, mode)); + let update_bool = is_update.is_some(); + let mut active_model = if let Some(entry) = is_update { + Entity::find_by_id(entry.id) + .one(conn) + .await? + .unwrap() + .into() + } else { + ActiveModel { + ..Default::default() } + }; + active_model.geofence_id = Set(fence_id as u32); + active_model.geometry = Set(geometry); + active_model.mode = Set(mode); + active_model.updated_at = Set(Utc::now()); + if update_bool { + active_model.update(conn).await?; + inserts_updates.updates += 1; + Ok(()) } else { - println!( - "[ROUTE_SAVE] geometry value is invalid for {}", - name - ) + active_model.name = Set(name); + active_model.created_at = Set(Utc::now()); + active_model.insert(conn).await?; + inserts_updates.inserts += 1; + Ok(()) } } else { - println!( - "[ROUTE_SAVE] geometry value does not exist for {}", + let error = format!( + "[ROUTE_SAVE] geometry value is invalid for {}", name - ) + ); + log::warn!("{}", error); + Err(DbErr::Custom(error)) } } else { - println!( - "[ROUTE_SAVE] __geofence_id property not found for {}", + let error = format!( + "[ROUTE_SAVE] geometry value does not exist for {}", name - ) + ); + log::warn!("{}", error); + Err(DbErr::Custom(error)) } } else { - println!( - "[ROUTE_SAVE] __mode property is not a valid string for {}", + let error = format!( + "[ROUTE_SAVE] __geofence_id property not found for {}", name - ) + ); + log::warn!("{}", error); + Err(DbErr::Custom(error)) } } else { - println!("[ROUTE_SAVE] __mode property not found for {}", name) + let error = format!( + "[ROUTE_SAVE] __mode property is not a valid string for {}", + name + ); + log::warn!("{}", error); + Err(DbErr::Custom(error)) } } else { - println!( - "[ROUTE_SAVE] __name property is not a valid string for {:?}", - name - ) + let error = format!("[ROUTE_SAVE] __mode property not found for {}", name); + log::warn!("{}", error); + Err(DbErr::Custom(error)) } } else { - println!("[ROUTE_SAVE] __name property not found, {:?}", feat.id) + let error = format!( + "[ROUTE_SAVE] __name property is not a valid string for {:?}", + name + ); + log::warn!("{}", error); + Err(DbErr::Custom(error)) } + } else { + let error = format!("[ROUTE_SAVE] __name property not found, {:?}", feat.id); + log::warn!("{}", error); + Err(DbErr::Custom(error)) } - Ok((inserts, update_len)) + } + + pub async fn upsert_from_geometry( + conn: &DatabaseConnection, + area: GeoFormats, + ) -> Result<(usize, usize), DbErr> { + let existing: HashMap = Query::get_all_no_fences(conn) + .await? + .into_iter() + .map(|model| (format!("{}_{}", model.name, model.mode), model)) + .collect(); + + let mut inserts_updates = InsertsUpdates:: { + inserts: 0, + updates: 0, + to_insert: vec![], + }; + + match area { + GeoFormats::Feature(feat) => { + Query::upsert_feature(conn, feat, &existing, &mut inserts_updates).await? + } + feat => { + let fc = match feat { + GeoFormats::FeatureCollection(fc) => fc, + geometry => geometry.to_collection(None, None), + }; + for feat in fc.into_iter() { + Query::upsert_feature(conn, feat, &existing, &mut inserts_updates).await? + } + } + } + + Ok((inserts_updates.inserts, inserts_updates.updates)) + } + + pub async fn by_geofence( + conn: &DatabaseConnection, + geofence_name: String, + ) -> Result, DbErr> { + let items = Entity::find() + .order_by(Column::Name, Order::Asc) + .left_join(geofence::Entity) + .filter(geofence::Column::Name.eq(geofence_name)) + .all(conn) + .await?; + + let items: Vec = items + .into_iter() + .filter_map(|item| item.to_feature().ok()) + .collect(); + Ok(items) } } diff --git a/server/model/src/error.rs b/server/model/src/error.rs new file mode 100644 index 00000000..df56f951 --- /dev/null +++ b/server/model/src/error.rs @@ -0,0 +1,26 @@ +use sea_orm::DbErr; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ModelError { + #[error("Database Error: {0}")] + Database(DbErr), + #[error("Geojson Error: {0}")] + Geojson(geojson::Error), + #[error("Not Implemented: {0}")] + NotImplemented(String), + #[error("{0}")] + Custom(String), +} + +impl From for ModelError { + fn from(error: DbErr) -> Self { + Self::Database(error) + } +} + +impl From for ModelError { + fn from(error: geojson::Error) -> Self { + Self::Geojson(error) + } +} diff --git a/server/model/src/lib.rs b/server/model/src/lib.rs index b722bb9f..30932ab0 100644 --- a/server/model/src/lib.rs +++ b/server/model/src/lib.rs @@ -1,13 +1,15 @@ use geojson::{Feature, FeatureCollection}; +use log; use num_traits::Float; use sea_orm::DatabaseConnection; use serde::{Deserialize, Serialize}; pub mod api; pub mod db; +pub mod error; pub mod utils; -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct KojiDb { pub koji_db: DatabaseConnection, pub data_db: DatabaseConnection, diff --git a/server/model/src/utils/mod.rs b/server/model/src/utils/mod.rs index 00203446..4bfda92e 100644 --- a/server/model/src/utils/mod.rs +++ b/server/model/src/utils/mod.rs @@ -66,7 +66,7 @@ pub fn get_enum_by_geometry(enum_val: &Value) -> Option { Value::Polygon(_) => Some(Type::PokemonIv), Value::MultiPolygon(_) => Some(Type::AutoQuest), _ => { - println!("Invalid Geometry Type"); + log::warn!("Invalid Geometry Type: {}", enum_val.type_name()); None } } @@ -90,4 +90,16 @@ pub fn get_mode_acronym(instance_type: Option<&String>) -> String { None => "", }.to_string() } - \ No newline at end of file + + pub fn get_enum_by_geometry_string(input: Option) -> Option { + if let Some(input) = input { + match input.to_lowercase().as_str() { + "point" => Some(Type::Leveling), + "multipoint" => Some(Type::CirclePokemon), + "multipolygon" => Some(Type::AutoQuest), + _ => None + } + } else { + None + } + } \ No newline at end of file diff --git a/server/model/src/utils/normalize.rs b/server/model/src/utils/normalize.rs index ed2e5355..5e0a4198 100644 --- a/server/model/src/utils/normalize.rs +++ b/server/model/src/utils/normalize.rs @@ -1,8 +1,10 @@ +use serde_json::json; + use super::*; use crate::{ api::text::TextHelpers, - db::{sea_orm_active_enums::Type, AreaRef, NameTypeId}, + db::{sea_orm_active_enums::Type, AreaRef}, }; pub fn fort(items: api::single_struct::SingleStruct, prefix: &str) -> Vec> @@ -40,68 +42,63 @@ where pub fn instance(instance: db::instance::Model) -> Feature { instance .data - .parse_scanner_instance(Some(instance.name), Some(&instance.r#type)) + .parse_scanner_instance(Some(instance.name), Some(instance.r#type)) } pub fn area(areas: Vec) -> Vec { let mut normalized = Vec::::new(); - let mut to_feature = |fence: Option, name: String, category: &str| -> String { + let mut to_feature = |fence: Option, name: &String, mode: Type| { if let Some(fence) = fence { if !fence.is_empty() { - normalized.push(fence.parse_scanner_instance( - Some(name.to_string()), - Some(match category { - "Fort" => &Type::CircleRaid, - "Quest" => &Type::ManualQuest, - "Pokemon" => &Type::CirclePokemon, - _ => &Type::AutoQuest, - }), - )); + normalized.push(fence.parse_scanner_instance(Some(name.to_string()), Some(mode))); } } - name }; for area in areas.into_iter() { - let name = to_feature(area.geofence, area.name, "Fence"); - let name = to_feature(area.fort_mode_route, name, "Fort"); - let name = to_feature(area.quest_mode_route, name, "Quest"); - to_feature(area.pokemon_mode_route, name, "Pokemon"); + to_feature(area.geofence, &area.name, Type::AutoQuest); + to_feature(area.fort_mode_route, &area.name, Type::CircleRaid); + to_feature(area.quest_mode_route, &area.name, Type::ManualQuest); + to_feature(area.pokemon_mode_route, &area.name, Type::CirclePokemon); } normalized } -pub fn area_ref(areas: Vec) -> Vec { - let mut normalized = Vec::::new(); +pub fn area_ref(areas: Vec) -> Vec { + let mut normalized = Vec::::new(); for area in areas.into_iter() { - if area.has_fort { - normalized.push(NameTypeId { - id: area.id, - name: area.name.clone(), - r#type: Some(Type::CircleRaid), - }); - } if area.has_geofence { - normalized.push(NameTypeId { - id: area.id, - name: area.name.clone(), - r#type: Some(Type::AutoQuest), - }); + normalized.push(json!({ + "id": area.id, + "name": area.name, + "mode": "AutoQuest", + "geo_type": "MultiPolygon", + })); + } + if area.has_fort { + normalized.push(json!({ + "id": area.id, + "name": area.name, + "mode": "CircleRaid", + "geo_type": "MultiPoint", + })); } if area.has_pokemon { - normalized.push(NameTypeId { - id: area.id, - name: area.name.clone(), - r#type: Some(Type::CirclePokemon), - }); + normalized.push(json!({ + "id": area.id, + "name": area.name, + "mode": "CirclePokemon", + "geo_type": "MultiPoint", + })); } if area.has_quest { - normalized.push(NameTypeId { - id: area.id, - name: area.name, - r#type: Some(Type::ManualQuest), - }); + normalized.push(json!({ + "id": area.id, + "name": area.name, + "mode": "ManualQuest", + "geo_type": "MultiPoint", + })); } } normalized diff --git a/server/nominatim/Cargo.toml b/server/nominatim/Cargo.toml index 70d40e5d..6a87c2b6 100644 --- a/server/nominatim/Cargo.toml +++ b/server/nominatim/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nominatim" -version = "0.1.0" +version = "0.4.0" edition = "2021" publish = false diff --git a/server/src/main.rs b/server/src/main.rs index 1fd7419b..42e5571a 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,5 +1,16 @@ fn main() { - if let Err(err) = api::main() { - println!("Error: {}", err); + dotenv::from_filename(std::env::var("ENV").unwrap_or(".env".to_string())).ok(); + + // error | warn | info | debug | trace + env_logger::init_from_env( + env_logger::Env::new() + .default_filter_or(std::env::var("LOG_LEVEL").unwrap_or("info".to_string())), + ); + + if let Err(err) = api::start() { + log::error!( + "[KOJI] Kōji encountered a critical error and shut down: {:?}", + err + ) } }