diff --git a/nest_frontend/components/interactive/BoxFilterContains.js b/nest_frontend/components/interactive/BoxFilterContains.js index 4b57660..545d263 100644 --- a/nest_frontend/components/interactive/BoxFilterContains.js +++ b/nest_frontend/components/interactive/BoxFilterContains.js @@ -2,7 +2,7 @@ import React from "react" import BoxFull from "../base/BoxFull" import useRepositoryViewer from "../../hooks/useRepositoryViewer" import useStrings from "../../hooks/useStrings" -import { ContainsFilter } from "../../utils/Filter" +import { FilterContains } from "../../utils/Filter" import FormInlineText from "./FormInlineText" import { faFont } from "@fortawesome/free-solid-svg-icons" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" @@ -13,7 +13,7 @@ export default function BoxFilterContains({ ...props }) { const { appendFilter } = useRepositoryViewer() const submit = value => { - appendFilter(new ContainsFilter(false, value)) + appendFilter(new FilterContains(false, value)) } // TODO: add this string diff --git a/nest_frontend/components/interactive/BoxFilterDatetime.js b/nest_frontend/components/interactive/BoxFilterDatetime.js index 8fde916..ab0c1ff 100644 --- a/nest_frontend/components/interactive/BoxFilterDatetime.js +++ b/nest_frontend/components/interactive/BoxFilterDatetime.js @@ -3,7 +3,7 @@ import BoxFull from "../base/BoxFull" import { faClock, faHashtag } from "@fortawesome/free-solid-svg-icons" import useRepositoryViewer from "../../hooks/useRepositoryViewer" import useStrings from "../../hooks/useStrings" -import { AfterDatetimeFilter } from "../../utils/Filter" +import { FilterInsideTimeRay } from "../../utils/Filter" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import FormInlineBADatetime from "./FormInlineBADatetime" @@ -13,7 +13,7 @@ export default function BoxFilterDatetime({ ...props }) { const { appendFilter } = useRepositoryViewer() const submit = ({ date, isBefore }) => { - appendFilter(new AfterDatetimeFilter(isBefore, date)) + appendFilter(new FilterInsideTimeRay(isBefore, date)) } return ( diff --git a/nest_frontend/components/interactive/BoxFilterHasPlace.js b/nest_frontend/components/interactive/BoxFilterHasPlace.js index b317b18..ffaa711 100644 --- a/nest_frontend/components/interactive/BoxFilterHasPlace.js +++ b/nest_frontend/components/interactive/BoxFilterHasPlace.js @@ -5,7 +5,7 @@ import useRepositoryViewer from "../../hooks/useRepositoryViewer" import useStrings from "../../hooks/useStrings" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { faLocationArrow, faPlus } from "@fortawesome/free-solid-svg-icons" -import { HasPlaceFilter } from "../../utils/Filter" +import { FilterWithPlace } from "../../utils/Filter" import ButtonIconOnly from "../base/ButtonIconOnly" @@ -15,7 +15,7 @@ export default function BoxFilterHasPlace({ ...props }) { const { appendFilter } = useRepositoryViewer() const submit = () => { - appendFilter(new HasPlaceFilter(false)) + appendFilter(new FilterWithPlace(false)) } // TODO: translate this diff --git a/nest_frontend/components/interactive/BoxFilterHashtag.js b/nest_frontend/components/interactive/BoxFilterHashtag.js index 76aba03..211d461 100644 --- a/nest_frontend/components/interactive/BoxFilterHashtag.js +++ b/nest_frontend/components/interactive/BoxFilterHashtag.js @@ -3,7 +3,7 @@ import BoxFull from "../base/BoxFull" import { faClock } from "@fortawesome/free-solid-svg-icons" import useRepositoryViewer from "../../hooks/useRepositoryViewer" import useStrings from "../../hooks/useStrings" -import { HashtagFilter } from "../../utils/Filter" +import { FilterHashtag } from "../../utils/Filter" import FormInlineHashtag from "./FormInlineHashtag" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" @@ -13,7 +13,7 @@ export default function BoxFilterHashtag({ ...props }) { const { appendFilter } = useRepositoryViewer() const submit = value => { - appendFilter(new HashtagFilter(false, value)) + appendFilter(new FilterHashtag(false, value)) } return ( diff --git a/nest_frontend/components/interactive/BoxFilterLocation.js b/nest_frontend/components/interactive/BoxFilterLocation.js index 851dc49..5c036f7 100644 --- a/nest_frontend/components/interactive/BoxFilterLocation.js +++ b/nest_frontend/components/interactive/BoxFilterLocation.js @@ -6,7 +6,7 @@ import useStrings from "../../hooks/useStrings" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { faMapPin } from "@fortawesome/free-solid-svg-icons" import FormInlineLocation from "./FormInlineLocation" -import { LocationRadiusFilter } from "../../utils/Filter" +import { FilterInsideMapArea } from "../../utils/Filter" export default function BoxFilterLocation({ ...props }) { @@ -15,7 +15,7 @@ export default function BoxFilterLocation({ ...props }) { const { appendFilter, mapViewHook } = useRepositoryViewer() const submit = () => { - appendFilter(new LocationRadiusFilter(false, mapViewHook.center, mapViewHook.radius)) + appendFilter(new FilterInsideMapArea(false, mapViewHook.center, mapViewHook.radius)) } return ( diff --git a/nest_frontend/components/interactive/BoxFilterUser.js b/nest_frontend/components/interactive/BoxFilterUser.js index 9efd651..edcb702 100644 --- a/nest_frontend/components/interactive/BoxFilterUser.js +++ b/nest_frontend/components/interactive/BoxFilterUser.js @@ -3,7 +3,7 @@ import BoxFull from "../base/BoxFull" import { faAt } from "@fortawesome/free-solid-svg-icons" import useRepositoryViewer from "../../hooks/useRepositoryViewer" import useStrings from "../../hooks/useStrings" -import { UserFilter } from "../../utils/Filter" +import { FilterPoster } from "../../utils/Filter" import FormInlineUser from "./FormInlineUser" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" @@ -16,7 +16,7 @@ export default function BoxFilterUser({ ...props }) { const { appendFilter } = useRepositoryViewer() const submit = value => { - appendFilter(new UserFilter(false, value)) + appendFilter(new FilterPoster(false, value)) } return ( diff --git a/nest_frontend/components/interactive/BoxVisualizationWordcloud.js b/nest_frontend/components/interactive/BoxVisualizationWordcloud.js index 9072146..be53923 100644 --- a/nest_frontend/components/interactive/BoxVisualizationWordcloud.js +++ b/nest_frontend/components/interactive/BoxVisualizationWordcloud.js @@ -4,7 +4,7 @@ import ContextLanguage from "../../contexts/ContextLanguage" import BoxFull from "../base/BoxFull" import Empty from "./Empty" import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer" -import { ContainsFilter } from "../../utils/Filter" +import { FilterContains } from "../../utils/Filter" export default function BoxVisualizationWordcloud({ ...props }) { @@ -20,7 +20,7 @@ export default function BoxVisualizationWordcloud({ ...props }) { } const onWordClick = word => { - appendFilter(new ContainsFilter(false, word.text)) + appendFilter(new FilterContains(false, word.text)) } return ( diff --git a/nest_frontend/objects/Filter.js b/nest_frontend/objects/Filter.js new file mode 100644 index 0000000..c8f8892 --- /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(word, negate = false) { + super(negate) + this.string = word.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(negate, `#${hashtag}`) + 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 index a222742..f1f21ac 100644 --- a/nest_frontend/objects/MapArea.js +++ b/nest_frontend/objects/MapArea.js @@ -36,7 +36,7 @@ export default class MapArea { * @returns {string} */ toString() { - return `${this.radius} ${this.center.toString()}` + return `< ${this.radius} ${this.center.toString()}` } /** @@ -47,9 +47,9 @@ export default class MapArea { toHumanString() { if(this.radius >= 2000) { const kmRadius = Math.round(this.radius / 1000) - return `${kmRadius}km ${this.center.toHumanString()}` + return `< ${kmRadius}km ${this.center.toHumanString()}` } - return `${this.radius}m ${this.center.toHumanString()}` + return `< ${this.radius}m ${this.center.toHumanString()}` } /** diff --git a/nest_frontend/objects/MapArea.test.js b/nest_frontend/objects/MapArea.test.js index 409f015..cf64005 100644 --- a/nest_frontend/objects/MapArea.test.js +++ b/nest_frontend/objects/MapArea.test.js @@ -9,5 +9,5 @@ test("MapArea can be constructed", () => { 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") + expect(mapArea.toString()).toBe("< 1000 0.0000000 0.0000000") }) diff --git a/nest_frontend/objects/TimeRay.js b/nest_frontend/objects/TimeRay.js index 04bde14..fcd2184 100644 --- a/nest_frontend/objects/TimeRay.js +++ b/nest_frontend/objects/TimeRay.js @@ -21,4 +21,8 @@ export default class TimeRay { toString() { return `${this.isBefore ? "<" : ">"} ${this.date.toISOString()}` } + + includes(date) { + return Boolean((this.date > date) ^ this.isBefore) + } } 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