}
*/
const login = useCallback(async (email, password) => {
- console.debug("Contattando il server per accedere...")
+ console.debug("Contacting the server to login...")
const data = await fetchData("POST", `/api/v1/login`, {
"email": email,
"password": password,
})
- console.debug("Memorizzando lo stato di login...")
+ console.debug("Saving login state...")
setUser({
email: data["user"]["email"],
isAdmin: data["user"]["isAdmin"],
@@ -64,18 +64,18 @@ export default function GlobalUser({ children }) {
token: data["access_token"],
})
- console.info("Accesso effettuato!")
+ console.info("Login successful!")
}, [fetchData, setUser])
/**
* Logout from the currently active server.
*/
const logout = useCallback(() => {
- console.debug("Ripulendo lo stato di login...")
+ console.debug("Clearing login state...")
setUser(null)
- console.debug("Stato di login ripulito!")
+ console.debug("Cleared login state!")
- console.info("Logout avvenuto con successo!")
+ console.info("Logout successful!")
}, [setUser])
return (
diff --git a/nest_frontend/components/providers/RepositoryEditor.js b/nest_frontend/components/providers/RepositoryEditor.js
index 248b00a..f933204 100644
--- a/nest_frontend/components/providers/RepositoryEditor.js
+++ b/nest_frontend/components/providers/RepositoryEditor.js
@@ -2,7 +2,7 @@ import React, { useCallback, useContext, useMemo, useState } from "react"
import ContextRepositoryEditor from "../../contexts/ContextRepositoryEditor"
import useArrayState from "../../hooks/useArrayState"
import Style from "./RepositoryEditor.module.css"
-import BoxConditionMap from "../interactive/BoxConditionMap"
+import BoxConditionLocation from "../interactive/BoxConditionLocation"
import BoxConditionHashtag from "../interactive/BoxConditionHashtag"
import BoxConditionUser from "../interactive/BoxConditionUser"
import BoxConditionDatetime from "../interactive/BoxConditionDatetime"
@@ -142,7 +142,7 @@ export default function RepositoryEditor({
}}
>
-
+
diff --git a/nest_frontend/components/providers/RepositoryViewer.js b/nest_frontend/components/providers/RepositoryViewer.js
index f2dff8d..b8ad41f 100644
--- a/nest_frontend/components/providers/RepositoryViewer.js
+++ b/nest_frontend/components/providers/RepositoryViewer.js
@@ -24,7 +24,7 @@ import BoxFilterContains from "../interactive/BoxFilterContains"
import BoxFilterUser from "../interactive/BoxFilterUser"
import BoxFilterHashtag from "../interactive/BoxFilterHashtag"
import BoxFilterLocation from "../interactive/BoxFilterLocation"
-import useMapView from "../../hooks/useMapView"
+import useMapAreaState from "../../hooks/useMapAreaState"
import BoxFilterDatetime from "../interactive/BoxFilterDatetime"
import BoxFilterHasPlace from "../interactive/BoxFilterHasPlace"
@@ -44,7 +44,7 @@ export default function RepositoryViewer({ id, className, ...props }) {
} = useArrayState([])
// FIXME: this has a severe performance impact, investigate
- const mapViewHook = useMapView()
+ const mapViewHook = useMapAreaState()
// Repository
const repositoryBr = useBackendResource(
diff --git a/nest_frontend/contexts/ContextLanguage.js b/nest_frontend/contexts/ContextLanguage.js
index a48fb29..0b55bb4 100644
--- a/nest_frontend/contexts/ContextLanguage.js
+++ b/nest_frontend/contexts/ContextLanguage.js
@@ -8,7 +8,7 @@ import LocalizationStrings from "../LocalizationStrings"
* - `setLang`: a function to change the current language
* - `strings`: an object containing all strings of the current language
*
- * Defaults to Italian.
+ * Defaults to Italian `it`.
*/
export default createContext({
lang: "it",
diff --git a/nest_frontend/contexts/ContextRepositoryEditor.js b/nest_frontend/contexts/ContextRepositoryEditor.js
index c8db99b..3364c6b 100644
--- a/nest_frontend/contexts/ContextRepositoryEditor.js
+++ b/nest_frontend/contexts/ContextRepositoryEditor.js
@@ -2,6 +2,8 @@ import { createContext } from "react"
/**
- * Context to quickly pass props to the children of {@link RepositoryEditor}.
+ * React Context representing containing all variables of a {@link RepositoryEditor}.
+ *
+ * It is `null` outside a RepositoryEditor.
*/
export default createContext(null)
diff --git a/nest_frontend/contexts/ContextRepositoryViewer.js b/nest_frontend/contexts/ContextRepositoryViewer.js
index a425822..17e387b 100644
--- a/nest_frontend/contexts/ContextRepositoryViewer.js
+++ b/nest_frontend/contexts/ContextRepositoryViewer.js
@@ -2,6 +2,8 @@ import { createContext } from "react"
/**
- * Context to quickly pass props to the children of {@link RepositoryViewer}.
+ * React Context representing containing all variables of a {@link RepositoryViewer}.
+ *
+ * It is `null` outside a RepositoryViewer.
*/
export default createContext(null)
diff --git a/nest_frontend/contexts/ContextServer.js b/nest_frontend/contexts/ContextServer.js
index c9a8402..7623b76 100644
--- a/nest_frontend/contexts/ContextServer.js
+++ b/nest_frontend/contexts/ContextServer.js
@@ -2,7 +2,7 @@ import { createContext } from "react"
/**
- * A context containing an object with the following values:
+ * A React Context containing an object with the following values:
* - `server`: the base URL of the currently active backend server
* - `setServer`: a function to change `server`
* - `fetchData`: a function to fetch JSON data from the backend server
diff --git a/nest_frontend/contexts/ContextTheme.js b/nest_frontend/contexts/ContextTheme.js
index 85f9a6b..bd5d07c 100644
--- a/nest_frontend/contexts/ContextTheme.js
+++ b/nest_frontend/contexts/ContextTheme.js
@@ -2,7 +2,7 @@ import { createContext } from "react"
/**
- * A context containing an object with the following elements:
+ * A React Context containing an object with the following elements:
* - `theme` - A string containing the name of the current theme.
* - `setTheme` - A function that allows changing the `theme`.
*
diff --git a/nest_frontend/contexts/ContextUser.js b/nest_frontend/contexts/ContextUser.js
index 9a883c5..5174353 100644
--- a/nest_frontend/contexts/ContextUser.js
+++ b/nest_frontend/contexts/ContextUser.js
@@ -2,7 +2,7 @@ import { createContext } from "react"
/**
- * A context containing an object with the following values:
+ * A React Context containing an object with the following values:
* - `user`: an object containing data about the currently logged in user
* - `login`: a function accepting `email, password` as parameters which tries to login the user
* - `logout`: a function accepting no parameters which logs the user out
diff --git a/nest_frontend/contexts/README.md b/nest_frontend/contexts/README.md
deleted file mode 100644
index 85f071e..0000000
--- a/nest_frontend/contexts/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Contexts
-
-In questa cartella sono contenuti i `Context` globali di React.
diff --git a/nest_frontend/hooks/useArrayState.js b/nest_frontend/hooks/useArrayState.js
index 9a8225c..5aeda3d 100644
--- a/nest_frontend/hooks/useArrayState.js
+++ b/nest_frontend/hooks/useArrayState.js
@@ -2,7 +2,7 @@ import { useCallback, useState } from "react"
/**
- * An hook similar to {@link useState} which stores an array of values.
+ * An hook similar to {@link useState} which stores an array instead of a single value.
*
* @param def - The starting value of the hook.
* @returns {{spliceValue, removeValue, setValue, appendValue, value}}
@@ -12,7 +12,7 @@ export default function useArrayState(def) {
const appendValue = useCallback(
newSingle => {
- console.debug("Aggiungendo ", newSingle, " ad ArrayState")
+ console.debug("Adding ", newSingle, " to ArrayState")
setValue(
oldArray => [...oldArray, newSingle],
)
@@ -22,7 +22,7 @@ export default function useArrayState(def) {
const spliceValue = useCallback(
position => {
- console.debug("Estraendo ", position, " da ArrayState")
+ console.debug("Splicing ", position, " from ArrayState")
setValue(
oldArray => {
oldArray.splice(position, 1)
diff --git a/nest_frontend/hooks/useAsyncEffect.js b/nest_frontend/hooks/useAsyncEffect.js
deleted file mode 100644
index 7595c3b..0000000
--- a/nest_frontend/hooks/useAsyncEffect.js
+++ /dev/null
@@ -1,17 +0,0 @@
-/* eslint-disable */
-import { useEffect } from "react"
-
-
-/**
- * {@link useEffect}, but with an async effect.
- *
- * @warning Breaks `react-hooks/exaustive-deps`.
- *
- * @param effect - The async effect.
- * @param deps - The dependencies of the hook.
- */
-export default function useAsyncEffect(effect, deps) {
- useEffect(() => {
- effect()
- }, [effect, ...deps])
-}
diff --git a/nest_frontend/hooks/useBackendRequest.js b/nest_frontend/hooks/useBackendRequest.js
index 9c85044..1ee6489 100644
--- a/nest_frontend/hooks/useBackendRequest.js
+++ b/nest_frontend/hooks/useBackendRequest.js
@@ -1,7 +1,6 @@
import { useCallback, useContext, useState } from "react"
import ContextServer from "../contexts/ContextServer"
import ContextUser from "../contexts/ContextUser"
-import makeURLSearchParams from "../utils/makeURLSearchParams"
/**
@@ -45,7 +44,7 @@ export default function useBackendRequest() {
// Use the body param as either search parameter or request body
if(body) {
if(["GET", "HEAD"].includes(method.toUpperCase())) {
- path += makeURLSearchParams(body).toString()
+ path += URLSearchParams.fromSerializableObject(body).toString()
}
else {
init["body"] = JSON.stringify(body)
diff --git a/nest_frontend/hooks/useLocalStorageState.js b/nest_frontend/hooks/useLocalStorageState.js
index 66f5be0..4c30613 100644
--- a/nest_frontend/hooks/useLocalStorageState.js
+++ b/nest_frontend/hooks/useLocalStorageState.js
@@ -2,7 +2,8 @@ import { useCallback, useState } from "react"
/**
- * Hook with the same API as {@link React.useState} which stores its value in the browser's {@link localStorage}.
+ * Hook with the same API as {@link React.useState} which additionally stores its value in the browser's
+ * {@link localStorage}.
*/
export default function useLocalStorageState(key, def) {
/**
diff --git a/nest_frontend/hooks/useMapAreaState.js b/nest_frontend/hooks/useMapAreaState.js
new file mode 100644
index 0000000..f1b8354
--- /dev/null
+++ b/nest_frontend/hooks/useMapAreaState.js
@@ -0,0 +1,21 @@
+import { useState } from "react"
+import Coordinates from "../objects/Coordinates"
+import MapArea from "../objects/MapArea"
+
+
+/**
+ * Hook which holds values required to create a {@link MapArea}.
+ */
+export default function useMapAreaState() {
+ const [zoom, setZoom] = useState(3)
+ const [center, setCenter] = useState(new Coordinates(0, 0))
+ const mapArea = MapArea.fromZoomLevel(zoom, center)
+
+ return {
+ zoom,
+ setZoom,
+ center,
+ setCenter,
+ mapArea,
+ }
+}
\ No newline at end of file
diff --git a/nest_frontend/hooks/useMapView.js b/nest_frontend/hooks/useMapView.js
deleted file mode 100644
index 990a931..0000000
--- a/nest_frontend/hooks/useMapView.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import { useState } from "react"
-import { DEFAULT_MAP_CENTER, DEFAULT_MAP_ZOOM } from "../utils/defaultMapLocation"
-import osmZoomLevels from "../utils/osmZoomLevels"
-
-
-export default function useMapView() {
- const [center, setCenter] = useState(DEFAULT_MAP_CENTER)
- const [zoom, setZoom] = useState(DEFAULT_MAP_ZOOM)
- const radius = osmZoomLevels[zoom]
-
- return {
- center,
- setCenter,
- zoom,
- setZoom,
- radius,
- }
-}
\ No newline at end of file
diff --git a/nest_frontend/hooks/useRepositoryViewer.js b/nest_frontend/hooks/useRepositoryViewer.js
index 20e84f5..f4ae358 100644
--- a/nest_frontend/hooks/useRepositoryViewer.js
+++ b/nest_frontend/hooks/useRepositoryViewer.js
@@ -1,10 +1,9 @@
import { useContext } from "react"
-import ContextRepositoryEditor from "../contexts/ContextRepositoryEditor"
import ContextRepositoryViewer from "../contexts/ContextRepositoryViewer"
/**
- * Hook to quickly use {@link ContextRepositoryEditor}.
+ * Hook to quickly use {@link ContextRepositoryViewer}.
*/
export default function useRepositoryViewer() {
const context = useContext(ContextRepositoryViewer)
@@ -12,4 +11,4 @@ export default function useRepositoryViewer() {
throw new Error("This component must be placed inside a RepositoryViewer.")
}
return context
-}
\ No newline at end of file
+}
diff --git a/nest_frontend/hooks/useStrings.js b/nest_frontend/hooks/useStrings.js
index 4a02c4a..ce3618d 100644
--- a/nest_frontend/hooks/useStrings.js
+++ b/nest_frontend/hooks/useStrings.js
@@ -3,7 +3,7 @@ import ContextLanguage from "../contexts/ContextLanguage"
/**
- * Hook to quickly use the strings of {@link ContextLanguage}.
+ * Hook to quickly use the `strings` attribute of {@link ContextLanguage}.
*/
export default function useStrings() {
return useContext(ContextLanguage).strings
diff --git a/nest_frontend/index.js b/nest_frontend/index.js
index c9b7cd1..b931256 100644
--- a/nest_frontend/index.js
+++ b/nest_frontend/index.js
@@ -3,6 +3,7 @@ import ReactDOM from "react-dom"
import "./index.css"
import App from "./App"
import reportWebVitals from "./reportWebVitals"
+import "./prototypes"
ReactDOM.render(
diff --git a/nest_frontend/media/README.md b/nest_frontend/media/README.md
deleted file mode 100644
index 7a5a05a..0000000
--- a/nest_frontend/media/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Media
-
-In questa cartella sono contenute le immagini statiche del sito web.
\ No newline at end of file
diff --git a/nest_frontend/objects/Condition.js b/nest_frontend/objects/Condition.js
new file mode 100644
index 0000000..aa94979
--- /dev/null
+++ b/nest_frontend/objects/Condition.js
@@ -0,0 +1,135 @@
+import {
+ IconDefinition,
+ faQuestionCircle,
+ faHashtag,
+ faAt,
+ faClock,
+ faLocationArrow,
+} from "@fortawesome/free-solid-svg-icons"
+
+
+/**
+ * Condition class for an undefined/unknown condition.
+ *
+ * See [the Condition spec](https://gitlab.steffo.eu/nest/g2-progetto/-/wikis/sprint-2/Specifica-delle-Conditions).
+ */
+export class Condition {
+ content
+ type
+ id
+
+ constructor(type, content, id = null) {
+ this.content = content
+ this.type = type
+ this.id = id
+ }
+
+ /**
+ * Get the condition as an object readable by the backend.
+ *
+ * @returns {{id, type, content}}
+ */
+ serialize() {
+ return {
+ type: this.type,
+ content: this.content,
+ id: this.id,
+ }
+ }
+
+ /**
+ * Display parameters for the badge representing this condition.
+ *
+ * @returns {{color: string, icon: IconDefinition, title, content}}
+ */
+ display() {
+ return {
+ color: "Grey",
+ icon: faQuestionCircle,
+ title: this.id,
+ children: this.content,
+ }
+ }
+}
+
+
+/**
+ * Require a tweet to contain a specific hashtag to be gathered.
+ */
+export class ConditionHashtag extends Condition {
+ constructor(hashtag, id = null) {
+ super(0, hashtag, id)
+ }
+
+ display() {
+ return {
+ color: "Grey",
+ icon: faHashtag,
+ title: this.id,
+ children: this.content,
+ }
+ }
+}
+
+
+/**
+ * Require a tweet to be posted by a certain user to be gathered.
+ */
+export class ConditionUser extends Condition {
+ constructor(user, id = null) {
+ super(5, user, id)
+ }
+
+ display() {
+ return {
+ color: "Green",
+ icon: faAt,
+ title: this.id,
+ children: this.content,
+ }
+ }
+}
+
+
+/**
+ * Require a tweet to be posted before or after a certain time to be gathered.
+ */
+export class ConditionTime extends Condition {
+ timeRay
+
+ constructor(timeRay, id = null) {
+ super(2, timeRay.toString(), id)
+ this.timeRay = timeRay
+ }
+
+ display() {
+ return {
+ color: "Yellow",
+ icon: faClock,
+ title: this.id,
+ children: this.content,
+ }
+ }
+}
+
+
+/**
+ * Require a tweet to have coordinates associated and to be posted within the {@link MapArea}.
+ */
+export class ConditionLocation extends Condition {
+ mapArea
+
+ constructor(mapArea, id = null) {
+ super(3, mapArea.toString(), id)
+ this.mapArea = mapArea
+ }
+
+ display() {
+ return {
+ color: "Red",
+ icon: faLocationArrow,
+ title: this.id,
+ children: this.mapArea.toHumanString(),
+ }
+ }
+}
diff --git a/nest_frontend/objects/Condition.test.js b/nest_frontend/objects/Condition.test.js
new file mode 100644
index 0000000..ee838af
--- /dev/null
+++ b/nest_frontend/objects/Condition.test.js
@@ -0,0 +1,56 @@
+import { Condition, ConditionHashtag, ConditionLocation, ConditionTime, ConditionUser } from "./Condition"
+import TimeRay from "./TimeRay"
+import MapArea from "./MapArea"
+import Coordinates from "./Coordinates"
+
+
+test("Condition can be constructed", () => {
+ expect(new Condition(0, "hi")).toBeTruthy()
+ expect(new Condition(0, "hi", 1)).toBeTruthy()
+})
+
+test("ConditionHashtag can be constructed", () => {
+ expect(new ConditionHashtag("PdS2021")).toBeTruthy()
+ expect(new ConditionHashtag("PdS2021", 1)).toBeTruthy()
+})
+
+test("ConditionUser can be constructed", () => {
+ expect(new ConditionUser("USteffo")).toBeTruthy()
+ expect(new ConditionUser("USteffo", 1)).toBeTruthy()
+})
+
+test("ConditionTime can be constructed", () => {
+ const now = new Date()
+ const timeRay = new TimeRay(true, now)
+
+ expect(new ConditionTime(timeRay)).toBeTruthy()
+ expect(new ConditionTime(timeRay, 1)).toBeTruthy()
+})
+
+test("ConditionLocation can be constructed", () => {
+ const mapArea = new MapArea(1000, new Coordinates(0.000, 0.000))
+
+ expect(new ConditionLocation(mapArea)).toBeTruthy()
+ expect(new ConditionLocation(mapArea, 1)).toBeTruthy()
+})
+
+test("ConditionHashtag has the correct type", () => {
+ expect(new ConditionHashtag("PdS2021").type).toBe(0)
+})
+
+test("ConditionUser has the correct type", () => {
+ expect(new ConditionUser("USteffo").type).toBe(5)
+})
+
+test("ConditionTime has the correct type", () => {
+ const now = new Date()
+ const timeRay = new TimeRay(true, now)
+
+ expect(new ConditionTime(timeRay).type).toBe(5)
+})
+
+test("ConditionLocation has the correct type", () => {
+ const mapArea = new MapArea(1000, new Coordinates(0.000, 0.000))
+
+ expect(new ConditionLocation(mapArea).type).toBe(3)
+})
diff --git a/nest_frontend/objects/Coordinates.js b/nest_frontend/objects/Coordinates.js
new file mode 100644
index 0000000..a27f965
--- /dev/null
+++ b/nest_frontend/objects/Coordinates.js
@@ -0,0 +1,77 @@
+/**
+ * A pair of coordinates, latitude `lat` and longitude `lng`.
+ */
+import { LatLng } from "leaflet/dist/leaflet-src.esm"
+
+
+export default class Coordinates {
+ lat
+ lng
+
+ /**
+ * @param lat - Latitude.
+ * @param lng - Longitude.
+ */
+ constructor(lat, lng) {
+ this.lat = lat
+ this.lng = lng
+ }
+
+ /**
+ * Create a new {@link Coordinates} from the format used by the backend.
+ *
+ * @param str - The string to create the object from.
+ * @returns {Coordinates}
+ */
+ static fromCrawlerString(str) {
+ const match = /[{]([0-9.]+),([0-9.]+)[}]/.exec(str)
+ if(!match) {
+ throw new Error(`Invalid location string: ${str}`)
+ }
+ return new Coordinates(match[1], match[2])
+ }
+
+ /**
+ * @returns {string}
+ */
+ toString() {
+ return `${this.lat.toFixed(7)} ${this.lng.toFixed(7)}`
+ }
+
+ /**
+ * Render the Coordinates as an human-readable string.
+ *
+ * @returns {string}
+ */
+ toHumanString() {
+ return `${this.lat.toFixed(3)} ${this.lng.toFixed(3)}`
+ }
+
+ /**
+ * Transform this object in a Geolib compatible-one.
+ */
+ toGeolib() {
+ return {
+ latitude: this.lat,
+ longitude: this.lng,
+ }
+ }
+
+ /**
+ * Transform this object in a 2-ple.
+ *
+ * @returns {[Number, Number]}
+ */
+ toArray() {
+ return [this.lat, this.lng]
+ }
+
+ /**
+ * Transform this object in a {@link LatLng} / Leaflet compatible-one.
+ *
+ * @returns {LatLng}
+ */
+ toLatLng() {
+ return new LatLng(this.lat, this.lng)
+ }
+}
diff --git a/nest_frontend/objects/Errors.js b/nest_frontend/objects/Errors.js
new file mode 100644
index 0000000..ada665d
--- /dev/null
+++ b/nest_frontend/objects/Errors.js
@@ -0,0 +1,104 @@
+/**
+ * Error thrown when a function is not implemented in the current class/instance.
+ */
+class NotImplementedError {
+ name
+
+ constructor(name) {
+ this.name = name
+ }
+}
+
+
+/**
+ * An error in the N.E.S.T. frontend-backend communication.
+ */
+class BackendCommunicationError {
+
+}
+
+
+/**
+ * Error thrown when trying to access a backend view which doesn't exist or isn't allowed in the used hook.
+ */
+class ViewNotAllowedError extends BackendCommunicationError {
+ view
+
+ constructor(view) {
+ super()
+
+ this.view = view
+ }
+}
+
+
+/**
+ * Error thrown when trying to access a backend view when outside a {@link ContextServer}.
+ */
+class ServerNotConfiguredError extends BackendCommunicationError {
+
+}
+
+
+/**
+ * Error thrown when trying to access a backend view while another access is ongoing.
+ *
+ * This is not allowed due to potential race conditions.
+ */
+class FetchAlreadyRunningError extends BackendCommunicationError {
+
+}
+
+
+/**
+ * Abstract class for {@link DecodeError} and {@link ResultError}.
+ */
+class FetchError extends BackendCommunicationError {
+ status
+ statusText
+
+ constructor(status, statusText) {
+ super()
+
+ this.status = status
+ this.statusText = statusText
+ }
+}
+
+
+/**
+ * Error thrown when the frontend can't parse the data received from the backend.
+ */
+class DecodeError extends FetchError {
+ error
+
+ constructor(status, statusText, error) {
+ super(status, statusText)
+
+ this.error = error
+ }
+}
+
+
+/**
+ * Error thrown when the backend returns a falsy `"result"` value.
+ */
+class ResultError extends FetchError {
+ status
+ statusText
+ data
+
+ constructor(status, statusText, data) {
+ super(status, statusText)
+
+ this.data = data
+ }
+
+ getMsg() {
+ return this.data.msg
+ }
+
+ getCode() {
+ return this.data.code
+ }
+}
diff --git a/nest_frontend/objects/Filter.js b/nest_frontend/objects/Filter.js
new file mode 100644
index 0000000..a18584c
--- /dev/null
+++ b/nest_frontend/objects/Filter.js
@@ -0,0 +1,222 @@
+import {
+ faAt,
+ faClock,
+ faFilter,
+ faFont,
+ faHashtag,
+ faLocationArrow,
+ faMapMarkerAlt,
+ faMapPin,
+} from "@fortawesome/free-solid-svg-icons"
+
+
+/**
+ * A filter applicable in the Analysis mode.
+ */
+export class Filter {
+ negate
+
+ /**
+ * @param negate - If the filter output should be reversed.
+ */
+ constructor(negate = false) {
+ this.negate = negate
+ }
+
+ /**
+ * Check if a tweet passed through the filter or not, without applying `negate`.
+ *
+ * @param tweet - The tweet to check.
+ * @returns {boolean}
+ */
+ check(tweet) {
+ return true
+ }
+
+ /**
+ * Check if a tweet passed through the filter or not, applying `negate`.
+ *
+ * @param tweet - The tweet to check.
+ * @returns {boolean}
+ */
+ exec(tweet) {
+ return Boolean(this.check(tweet) ^ this.negate)
+ }
+
+ display() {
+ return {
+ color: "Grey",
+ icon: faFilter,
+ children: this.negate ? "False" : "True"
+ }
+ }
+}
+
+
+/**
+ * Checks if a tweet contains a string.
+ */
+export class FilterContains extends Filter {
+ string
+
+ constructor(string, negate = false) {
+ super(negate)
+ this.string = string.toLowerCase().trim()
+ }
+
+ check(tweet) {
+ return tweet.content?.toLowerCase().includes(this.string)
+ }
+
+ display() {
+ return {
+ color: "Grey",
+ icon: faFont,
+ children: this.string
+ }
+ }
+}
+
+
+/**
+ * Check if a tweet contains an hashtag.
+ */
+export class FilterHashtag extends FilterContains {
+ hashtag
+
+ constructor(hashtag, negate = false) {
+ super(`#${hashtag}`, negate)
+ this.hashtag = hashtag
+ }
+
+ display() {
+ return {
+ color: "Grey",
+ icon: faHashtag,
+ children: this.hashtag
+ }
+ }
+}
+
+
+/**
+ * Check if a tweet was posted by a certain user.
+ */
+export class FilterPoster extends Filter {
+ poster
+
+ constructor(poster, negate = false) {
+ super(negate)
+ this.poster = poster
+ }
+
+ check(tweet) {
+ return tweet.poster.toLowerCase() === this.poster.toLowerCase()
+ }
+
+ display() {
+ return {
+ color: "Green",
+ icon: faAt,
+ children: this.poster
+ }
+ }
+}
+
+
+/**
+ * Check if a tweet contains `location` metadata.
+ */
+export class FilterWithLocation extends Filter {
+ constructor(negate = false) {
+ super(negate)
+ }
+
+ check(tweet) {
+ return Boolean(tweet["location"])
+ }
+
+ display() {
+ return {
+ color: "Red",
+ icon: faLocationArrow,
+ children: ""
+ }
+ }
+}
+
+
+/**
+ * Check if a tweet contains `place` metadata.
+ */
+export class FilterWithPlace extends Filter {
+ constructor(negate = false) {
+ super(negate)
+ }
+
+ check(tweet) {
+ return Boolean(tweet["place"])
+ }
+
+ display() {
+ return {
+ color: "Red",
+ icon: faMapMarkerAlt,
+ children: ""
+ }
+ }
+}
+
+
+/**
+ * Check if a tweet's `location` is inside a {@link MapArea}.
+ */
+export class FilterInsideMapArea extends FilterWithLocation {
+ mapArea
+
+ constructor(mapArea, negate = false) {
+ super(negate)
+ this.mapArea = mapArea
+ }
+
+ check(tweet) {
+ if(!super.check(tweet)) {
+ return false
+ }
+
+ return this.mapArea.includes(tweet.location)
+ }
+
+ display() {
+ return {
+ color: "Red",
+ icon: faLocationArrow,
+ children: this.mapArea.toHumanString()
+ }
+ }
+}
+
+
+/**
+ * Check if a tweet's `post_time` is inside a {@link TimeRay}.
+ */
+export class FilterInsideTimeRay extends Filter {
+ timeRay
+
+ constructor(timeRay, negate = false) {
+ super(negate)
+ this.timeRay = timeRay
+ }
+
+ check(tweet) {
+ return this.datetime < new Date(tweet["insert_time"])
+ }
+
+ display() {
+ return {
+ color: "Yellow",
+ icon: faClock,
+ children: this.timeRay.toString()
+ }
+ }
+}
diff --git a/nest_frontend/objects/MapArea.js b/nest_frontend/objects/MapArea.js
new file mode 100644
index 0000000..f1f21ac
--- /dev/null
+++ b/nest_frontend/objects/MapArea.js
@@ -0,0 +1,64 @@
+import {getDistance} from "geolib"
+import osmZoomLevels from "../utils/osmZoomLevels"
+
+
+/**
+ * An area on a map, defined by a `center` and a `radius` in meters.
+ */
+export default class MapArea {
+ radius
+ center
+
+ /**
+ * @param radius - The radius of the area in meters.
+ * @param center - The center of the area.
+ */
+ constructor(radius, center) {
+ this.radius = radius
+ this.center = center
+ }
+
+ /**
+ * Create a new {@link MapArea} from the [zoom level of OpenStreetMaps][1], assuming the window is
+ * ~400 pixels large.
+ *
+ * [1]: https://wiki.openstreetmap.org/wiki/Zoom_levels
+ *
+ * @param zoom
+ * @param center
+ * @returns {MapArea}
+ */
+ static fromZoomLevel(zoom, center) {
+ return new MapArea(osmZoomLevels[zoom], center)
+ }
+
+ /**
+ * @returns {string}
+ */
+ toString() {
+ return `< ${this.radius} ${this.center.toString()}`
+ }
+
+ /**
+ * Render the {@link MapArea} as an human-readable string.
+ *
+ * @returns {string}
+ */
+ toHumanString() {
+ if(this.radius >= 2000) {
+ const kmRadius = Math.round(this.radius / 1000)
+ return `< ${kmRadius}km ${this.center.toHumanString()}`
+ }
+ return `< ${this.radius}m ${this.center.toHumanString()}`
+ }
+
+ /**
+ * Check if a pair of coordinates is included in the area.
+ *
+ * @param coords - The coordinates to check.
+ * @returns {boolean}
+ */
+ includes(coords) {
+ return getDistance(this.center.toGeolib(), coords.toGeolib()) <= this.radius
+ }
+}
diff --git a/nest_frontend/objects/MapArea.test.js b/nest_frontend/objects/MapArea.test.js
new file mode 100644
index 0000000..cf64005
--- /dev/null
+++ b/nest_frontend/objects/MapArea.test.js
@@ -0,0 +1,13 @@
+import Coordinates from "./Coordinates"
+import MapArea from "./MapArea"
+
+
+test("MapArea can be constructed", () => {
+ const mapArea = new MapArea(1000, new Coordinates(0.0, 0.0))
+ expect(mapArea).toBeTruthy()
+})
+
+test("MapArea can be rendered to a spec-compatible string", () => {
+ const mapArea = new MapArea(1000, new Coordinates(0.0, 0.0))
+ expect(mapArea.toString()).toBe("< 1000 0.0000000 0.0000000")
+})
diff --git a/nest_frontend/objects/TimeRay.js b/nest_frontend/objects/TimeRay.js
new file mode 100644
index 0000000..fcd2184
--- /dev/null
+++ b/nest_frontend/objects/TimeRay.js
@@ -0,0 +1,28 @@
+/**
+ * An half-line of time, defined by a `date` and a boolean `isBefore` indicating if the time before or after the
+ * specified date should be selected.
+ */
+export default class TimeRay {
+ isBefore
+ date
+
+ /**
+ * @param isBefore - `true` to select times earlier than the date, `false` to select times after the date.
+ * @param date - The date to start measurements from.
+ */
+ constructor(isBefore, date) {
+ this.isBefore = isBefore
+ this.date = date
+ }
+
+ /**
+ * @returns {string}
+ */
+ toString() {
+ return `${this.isBefore ? "<" : ">"} ${this.date.toISOString()}`
+ }
+
+ includes(date) {
+ return Boolean((this.date > date) ^ this.isBefore)
+ }
+}
diff --git a/nest_frontend/utils/convertToLocalISODate.js b/nest_frontend/prototypes/Date.js
similarity index 59%
rename from nest_frontend/utils/convertToLocalISODate.js
rename to nest_frontend/prototypes/Date.js
index 8282621..79e80cf 100644
--- a/nest_frontend/utils/convertToLocalISODate.js
+++ b/nest_frontend/prototypes/Date.js
@@ -1,20 +1,10 @@
-// Wow, JS, davvero?
-// Davvero tutte le date.toISOString() sono considerate UTC?
-// Wow.
-
-/**
- * Convert a {@link Date} object to a timezone aware ISO String, using the user's local timezone.
- *
- * @param date
- * @returns {string}
- */
-export default function convertToLocalISODate(date) {
- if(date.toString() === "Invalid Date") {
+Date.prototype.toAwareISOString = function() {
+ if(this.toString() === "Invalid Date") {
throw new Error("Data non valida ricevuta come parametro.")
}
// Create a timezone naive ISO string
- const naive = date.toISOString()
+ const naive = this.toISOString()
// Find the local timezone
const tz = -new Date().getTimezoneOffset()
@@ -29,4 +19,4 @@ export default function convertToLocalISODate(date) {
// Replace the naive part with the aware part
return naive.replace("Z", `${tz < 0 ? "-" : "+"}${tzHours}${tzMinutes}`)
-}
\ No newline at end of file
+}
diff --git a/nest_frontend/utils/makeURLSearchParams.js b/nest_frontend/prototypes/URLSearchParams.js
similarity index 86%
rename from nest_frontend/utils/makeURLSearchParams.js
rename to nest_frontend/prototypes/URLSearchParams.js
index c699be4..ef94c5c 100644
--- a/nest_frontend/utils/makeURLSearchParams.js
+++ b/nest_frontend/prototypes/URLSearchParams.js
@@ -1,7 +1,7 @@
import isString from "is-string"
-export default function makeURLSearchParams(obj) {
+URLSearchParams.fromSerializableObject = function(obj) {
let usp = new URLSearchParams()
for(const key in obj) {
if(!obj.hasOwnProperty(key)) {
diff --git a/nest_frontend/prototypes/index.js b/nest_frontend/prototypes/index.js
new file mode 100644
index 0000000..6589068
--- /dev/null
+++ b/nest_frontend/prototypes/index.js
@@ -0,0 +1,2 @@
+import "./Date"
+import "./URLSearchParams"
diff --git a/nest_frontend/routes/README.md b/nest_frontend/routes/README.md
deleted file mode 100644
index 3ae1b2c..0000000
--- a/nest_frontend/routes/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Routes
-
-In questa cartella sono contenuti i `Component` che vengono renderati come pagine intere.
diff --git a/nest_frontend/utils/Condition.js b/nest_frontend/utils/Condition.js
deleted file mode 100644
index 10c061f..0000000
--- a/nest_frontend/utils/Condition.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import isString from "is-string"
-
-
-const typeEnums = {
- "HASHTAG": 0,
- "TIME": 2,
- "COORDINATES": 3,
- "PLACE": 4,
- "USER": 5,
-}
-
-
-/**
- * A search/filtering Condition.
- *
- * See https://gitlab.steffo.eu/nest/g2-progetto/-/wikis/Specifica-delle-Conditions .
- */
-export default class Condition {
-
- /**
- * Create a new Condition.
- *
- * @param type - The type of Condition to create.
- * It can be a number or one of the following strings:
- * `"hashtag"`, `"time"`, `"coordinates"`, `"place"`.
- * @param content - The content of the Condition.
- * @param id - The id of the Condition on the backend, or null if the Condition hasn't been committed yet.
- */
- constructor(type, content, id = null) {
- if(isString(type)) {
- this.type = typeEnums[type.toUpperCase()]
- }
- else {
- this.type = type
- }
-
- this.content = content
- this.id = id
- }
-}
diff --git a/nest_frontend/utils/Errors.js b/nest_frontend/utils/Errors.js
deleted file mode 100644
index f4ba224..0000000
--- a/nest_frontend/utils/Errors.js
+++ /dev/null
@@ -1,69 +0,0 @@
-class NestError {
-
-}
-
-
-class ViewNotAllowedError extends NestError {
- view
-
- constructor(view) {
- super()
-
- this.view = view
- }
-}
-
-
-class ServerNotConfiguredError extends NestError {
-
-}
-
-
-class FetchAlreadyRunningError extends NestError {
-
-}
-
-
-class FetchError extends NestError {
- status
- statusText
-
- constructor(status, statusText) {
- super()
-
- this.status = status
- this.statusText = statusText
- }
-}
-
-
-class DecodeError extends FetchError {
- error
-
- constructor(status, statusText, error) {
- super(status, statusText)
-
- this.error = error
- }
-}
-
-
-class ResultError extends FetchError {
- status
- statusText
- data
-
- constructor(status, statusText, data) {
- super(status, statusText)
-
- this.data = data
- }
-
- getMsg() {
- return this.data.msg
- }
-
- getCode() {
- return this.data.code
- }
-}
diff --git a/nest_frontend/utils/Filter.js b/nest_frontend/utils/Filter.js
deleted file mode 100644
index 0f6b737..0000000
--- a/nest_frontend/utils/Filter.js
+++ /dev/null
@@ -1,221 +0,0 @@
-import { Location } from "./location"
-import {
- faAt,
- faClock,
- faFilter,
- faFont,
- faHashtag,
- faLocationArrow,
- faMapMarkerAlt,
- faMapPin,
-} from "@fortawesome/free-solid-svg-icons"
-
-
-export class Filter {
- negate
-
- constructor(negate) {
- this.negate = negate
- }
-
- check(tweet) {
- return true
- }
-
- exec(tweet) {
- return this.check(tweet) ^ this.negate
- }
-
- color() {
- return "Grey"
- }
-
- icon() {
- return faFilter
- }
-
- text() {
- return this.negate ? "False" : "True"
- }
-}
-
-
-export class ContainsFilter extends Filter {
- word
-
- constructor(negate, word) {
- super(negate)
- this.word = word.toLowerCase().trim()
- }
-
- check(tweet) {
- return tweet.content?.toLowerCase().includes(this.word)
- }
-
- color() {
- return "Grey"
- }
-
- icon() {
- return faFont
- }
-
- text() {
- return this.word
- }
-}
-
-
-export class HashtagFilter extends ContainsFilter {
- hashtag
-
- constructor(negate, hashtag) {
- super(negate, `#${hashtag}`)
- this.hashtag = hashtag
- }
-
- icon() {
- return faHashtag
- }
-
- text() {
- return this.hashtag
- }
-}
-
-
-export class UserFilter extends Filter {
- user
-
- constructor(negate, user) {
- super(negate)
- this.user = user.toLowerCase().trim().replace(/^@/, "")
- }
-
- check(tweet) {
- return tweet.poster.toLowerCase() === this.user
- }
-
- color() {
- return "Green"
- }
-
- icon() {
- return faAt
- }
-
- text() {
- return this.user
- }
-}
-
-
-export class HasLocationFilter extends Filter {
- constructor(negate) {
- super(negate)
- }
-
- check(tweet) {
- return Boolean(tweet["location"])
- }
-
- color() {
- return "Red"
- }
-
- icon() {
- return faMapMarkerAlt
- }
-
- text() {
- return ""
- }
-}
-
-
-export class HasPlaceFilter extends Filter {
- constructor(negate) {
- super(negate)
- }
-
- check(tweet) {
- return Boolean(tweet["place"])
- }
-
- color() {
- return "Red"
- }
-
- icon() {
- return faLocationArrow
- }
-
- text() {
- return ""
- }
-}
-
-
-export class LocationRadiusFilter extends HasLocationFilter {
- center
- radius
-
- constructor(negate, center, radius) {
- super(negate)
- this.center = center
- this.radius = radius
- }
-
- check(tweet) {
- if(!super.check(tweet)) {
- return false
- }
-
- // FIXME: Maths is hard
- const location = Location.fromTweet(tweet)
- const latDiff = Math.abs(location.lat - this.center.lat)
- const lngDiff = Math.abs(location.lng - this.center.lng)
- const squaredDistance = Math.pow(latDiff, 2) + Math.pow(lngDiff, 2)
- const squaredRadius = Math.pow(this.radius, 2)
-
- return squaredDistance < squaredRadius
- }
-
- color() {
- return "Red"
- }
-
- icon() {
- return faMapPin
- }
-
- text() {
- return `< ${this.radius}m ${this.center.lat.toFixed(3)} ${this.center.lng.toFixed(3)}`
- }
-}
-
-
-export class AfterDatetimeFilter extends Filter {
- datetime
-
- constructor(negate, datetime) {
- super(negate)
- this.datetime = datetime
- }
-
- check(tweet) {
- return this.datetime < new Date(tweet["insert_time"])
- }
-
- color() {
- return "Yellow"
- }
-
- icon() {
- return faClock
- }
-
- text() {
- return `${this.negate ? "<" : ">"} ${this.datetime.toISOString()}`
- }
-}
\ No newline at end of file
diff --git a/nest_frontend/utils/README.md b/nest_frontend/utils/README.md
deleted file mode 100644
index a34cb5a..0000000
--- a/nest_frontend/utils/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Utils
-
-In questa cartella sono contenute alcune funzioni di utility per il sito.
diff --git a/nest_frontend/utils/defaultMapLocation.js b/nest_frontend/utils/defaultMapLocation.js
deleted file mode 100644
index 042e333..0000000
--- a/nest_frontend/utils/defaultMapLocation.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export const DEFAULT_MAP_CENTER = { lat: 0, lng: 0 }
-export const DEFAULT_MAP_ZOOM = 3
diff --git a/nest_frontend/utils/goToOnSuccess.js b/nest_frontend/utils/goToOnSuccess.js
index 2095a28..b240d77 100644
--- a/nest_frontend/utils/goToOnSuccess.js
+++ b/nest_frontend/utils/goToOnSuccess.js
@@ -4,7 +4,6 @@
* @param func - The function to decorate.
* @param history - The history to push the destination to.
* @param destination - The path of the destination.
- * @returns {(function(): void)|*}
*/
export default function goToOnSuccess(func, history, destination) {
return async (...args) => {
diff --git a/nest_frontend/utils/location.js b/nest_frontend/utils/location.js
deleted file mode 100644
index 561f3d0..0000000
--- a/nest_frontend/utils/location.js
+++ /dev/null
@@ -1,37 +0,0 @@
-export const locationRegex = /[{](?[0-9.]+),(?[0-9.]+)[}]/
-
-
-export class Location {
- lat
- lng
-
- constructor(lat, lng) {
- this.lat = lat
- this.lng = lng
- }
-
- static fromString(locString) {
- const match = locationRegex.exec(locString)
- if(!match) {
- throw new Error(`Invalid location string: ${locString}`)
- }
- const { lat, lng } = match.groups
- return new Location(lat, lng)
- }
-
- static fromTweet(tweet) {
- if(tweet.location === null) {
- throw new Error(`Tweet has no location: ${tweet}`)
- }
-
- return Location.fromString(tweet.location)
- }
-
- toArray() {
- return [this.lat, this.lng]
- }
-
- toString() {
- return `${this.lat.toFixed(3)} ${this.lng.toFixed(3)}`
- }
-}
diff --git a/nest_frontend/utils/makeIcon.js b/nest_frontend/utils/makeIcon.js
new file mode 100644
index 0000000..4a364ed
--- /dev/null
+++ b/nest_frontend/utils/makeIcon.js
@@ -0,0 +1,29 @@
+import React, { isValidElement } from "react"
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
+import {IconDefinition} from "@fortawesome/fontawesome-svg-core"
+
+
+/**
+ * Try to create an icon element based on what is passed to the function:
+ * - If a {@link JSX.Element} is passed, a `` element containing it will be created and returned.
+ * - If a {@link IconDefinition} is passed, a `` element containing a {@link FontAwesomeIcon} will be created
+ * and returned.
+ * - If a falsy value is passed, `null` will be returned.
+ *
+ * @param icon - The icon value.
+ * @param props - Props to pass to the span element when it is created.
+ * @returns {JSX.Element|null}
+ */
+export default function makeIcon(icon, props) {
+ if(isValidElement(icon)) {
+ return {icon}
+ }
+ else if(icon) {
+ return (
+
+ )
+ }
+ else {
+ return null
+ }
+}
diff --git a/nest_frontend/utils/make_icon.js b/nest_frontend/utils/make_icon.js
deleted file mode 100644
index 22f960d..0000000
--- a/nest_frontend/utils/make_icon.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import React, { isValidElement } from "react"
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
-
-
-export default function make_icon(icon, className) {
- if(isValidElement(icon)) {
- return icon
- }
- else if(icon) {
- return (
-
- )
- }
- else {
- return null
- }
-}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index bf22cb9..ac710f9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"@testing-library/user-event": "^12.8.3",
"chart.js": "^3.2.1",
"classnames": "^2.3.1",
+ "geolib": "^3.3.1",
"is-string": "^1.0.5",
"leaflet": "^1.7.1",
"react": "^17.0.2",
@@ -9416,6 +9417,11 @@
"node": ">=6.9.0"
}
},
+ "node_modules/geolib": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/geolib/-/geolib-3.3.1.tgz",
+ "integrity": "sha512-sfahBXFcgELdpumDZV5b3KWiINkZxC5myAkLk067UUcTmTXaiE9SWmxMEHztn/Eus4JX6kesHxaIuZlniYgUtg=="
+ },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -30086,6 +30092,11 @@
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="
},
+ "geolib": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/geolib/-/geolib-3.3.1.tgz",
+ "integrity": "sha512-sfahBXFcgELdpumDZV5b3KWiINkZxC5myAkLk067UUcTmTXaiE9SWmxMEHztn/Eus4JX6kesHxaIuZlniYgUtg=="
+ },
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
diff --git a/package.json b/package.json
index f79bf75..2b3bfa4 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"@testing-library/user-event": "^12.8.3",
"chart.js": "^3.2.1",
"classnames": "^2.3.1",
+ "geolib": "^3.3.1",
"is-string": "^1.0.5",
"leaflet": "^1.7.1",
"react": "^17.0.2",