From c7d425960d0cf4fcc7d41e8b4c4792a547b186e0 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Fri, 21 May 2021 19:52:56 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=A5=20Completely=20implement=20filters?= =?UTF-8?q?=20(in=20a=20non-atomic=20commit)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sorry --- nest_frontend/components/base/BoxChart.js | 10 +-- nest_frontend/components/base/BoxMap.js | 67 +++++++++++---- nest_frontend/components/base/BoxWordcloud.js | 2 +- .../components/interactive/BadgeFilter.js | 4 +- .../interactive/BoxConditionDatetime.js | 58 +++---------- .../interactive/BoxConditionHashtag.js | 47 ++--------- .../components/interactive/BoxConditionMap.js | 83 +++---------------- .../interactive/BoxConditionUser.js | 46 ++-------- .../interactive/BoxConditionUser.module.css | 7 -- .../interactive/BoxFilterContains.js | 17 +++- .../interactive/BoxFilterDatetime.js | 35 ++++++++ .../interactive/BoxFilterHasPlace.js | 49 +++++++++++ .../interactive/BoxFilterHashtag.js | 44 ++++------ .../interactive/BoxFilterLocation.js | 37 +++++++++ .../components/interactive/BoxFilterUser.js | 36 ++++---- .../components/interactive/BoxFilters.js | 2 +- .../interactive/BoxRepositoryTweets.js | 2 +- .../interactive/BoxVisualizationChart.js | 6 +- .../interactive/BoxVisualizationMap.js | 42 +++++----- .../interactive/BoxVisualizationStats.js | 18 ++-- .../interactive/BoxVisualizationWordcloud.js | 4 +- .../interactive/ButtonToggleBeforeAfter.js | 10 +-- .../interactive/FormInlineBADatetime.js | 62 ++++++++++++++ .../interactive/FormInlineHashtag.js | 26 ++++++ .../interactive/FormInlineLocation.js | 60 ++++++++++++++ ...dule.css => FormInlineLocation.module.css} | 2 +- .../components/interactive/FormInlineText.js | 14 +++- .../components/interactive/FormInlineUser.js | 26 ++++++ .../components/interactive/PickerFilter.js | 15 +++- .../interactive/PickerVisualization.js | 4 +- .../components/providers/RepositoryViewer.js | 50 ++++++----- nest_frontend/hooks/useMapView.js | 18 ++++ nest_frontend/routes/PageRepository.js | 4 +- nest_frontend/utils/Filter.js | 62 +++++++++----- nest_frontend/utils/defaultMapLocation.js | 2 + nest_frontend/utils/location.js | 2 +- nest_frontend/utils/osmZoomLevels.js | 26 ++++++ 37 files changed, 638 insertions(+), 361 deletions(-) delete mode 100644 nest_frontend/components/interactive/BoxConditionUser.module.css create mode 100644 nest_frontend/components/interactive/BoxFilterDatetime.js create mode 100644 nest_frontend/components/interactive/BoxFilterHasPlace.js create mode 100644 nest_frontend/components/interactive/BoxFilterLocation.js create mode 100644 nest_frontend/components/interactive/FormInlineBADatetime.js create mode 100644 nest_frontend/components/interactive/FormInlineHashtag.js create mode 100644 nest_frontend/components/interactive/FormInlineLocation.js rename nest_frontend/components/interactive/{BoxConditionHashtag.module.css => FormInlineLocation.module.css} (51%) create mode 100644 nest_frontend/components/interactive/FormInlineUser.js create mode 100644 nest_frontend/hooks/useMapView.js create mode 100644 nest_frontend/utils/defaultMapLocation.js create mode 100644 nest_frontend/utils/osmZoomLevels.js diff --git a/nest_frontend/components/base/BoxChart.js b/nest_frontend/components/base/BoxChart.js index 3e8bee7..db6256e 100644 --- a/nest_frontend/components/base/BoxChart.js +++ b/nest_frontend/components/base/BoxChart.js @@ -3,7 +3,7 @@ import BoxFull from "./BoxFull" import ChartComponent from "react-chartjs-2" -export default function BoxChart({chartProps, ...props}) { +export default function BoxChart({ chartProps, ...props }) { const getCssVar = (variable) => { const computedStyle = window.getComputedStyle(document.querySelector("main")) return computedStyle.getPropertyValue(variable).trim() @@ -23,7 +23,7 @@ export default function BoxChart({chartProps, ...props}) { }, ticks: { color: getCssVar("--fg-primary"), - } + }, }, y: { beginAtZero: true, @@ -33,7 +33,7 @@ export default function BoxChart({chartProps, ...props}) { }, ticks: { color: getCssVar("--fg-primary"), - } + }, }, }, elements: { @@ -46,8 +46,8 @@ export default function BoxChart({chartProps, ...props}) { plugins: { legend: { display: false, - } - } + }, + }, }} {...chartProps} /> diff --git a/nest_frontend/components/base/BoxMap.js b/nest_frontend/components/base/BoxMap.js index a39f8c7..12b4093 100644 --- a/nest_frontend/components/base/BoxMap.js +++ b/nest_frontend/components/base/BoxMap.js @@ -1,25 +1,33 @@ -import React from "react" +import React, { useCallback, useEffect, useMemo, useState } from "react" import Style from "./BoxMap.module.css" import BoxFull from "./BoxFull" import { MapContainer, TileLayer } from "react-leaflet" -export default function BoxMap({ - setMap, - startingPosition = { lat: 41.89309, lng: 12.48289 }, - startingZoom = 3, - button, - children, - ...props - }) { - return ( - +export default function BoxMap( + { + mapViewHook, + button, + children, + ...props + }) { + const [map, setMap] = useState(null) + + const onMapMove = useCallback( + () => mapViewHook.setCenter(map.getCenter()), + [mapViewHook, map], + ) + + const onMapZoom = useCallback( + () => mapViewHook.setZoom(map.getZoom()), + [mapViewHook, map], + ) + + const mapContainer = useMemo( + () => ( @@ -34,6 +42,33 @@ export default function BoxMap({ + ), + [mapViewHook], + ) + + useEffect( + () => { + if(map === null) { + return + } + + map.on("move", onMapMove) + map.on("zoom", onMapZoom) + + return () => { + map.off("move", onMapMove) + map.off("zoom", onMapZoom) + } + }, + [map, mapViewHook] + ) + + return ( + + {mapContainer} ) } diff --git a/nest_frontend/components/base/BoxWordcloud.js b/nest_frontend/components/base/BoxWordcloud.js index 8bad4f1..0545a93 100644 --- a/nest_frontend/components/base/BoxWordcloud.js +++ b/nest_frontend/components/base/BoxWordcloud.js @@ -34,7 +34,7 @@ export default function BoxWordcloud({ words, callbacks = {}, ...props }) { callbacks={callbacks} /> ), - [words] + [words], ) return ( diff --git a/nest_frontend/components/interactive/BadgeFilter.js b/nest_frontend/components/interactive/BadgeFilter.js index d4123f7..c031b61 100644 --- a/nest_frontend/components/interactive/BadgeFilter.js +++ b/nest_frontend/components/interactive/BadgeFilter.js @@ -1,6 +1,4 @@ import React, { useContext } from "react" -import { faAt, faClock, faGlobe, faHashtag, faMapPin } from "@fortawesome/free-solid-svg-icons" -import ContextRepositoryEditor from "../../contexts/ContextRepositoryEditor" import Badge from "../base/Badge" import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer" @@ -13,7 +11,7 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer" * @constructor */ export default function BadgeFilter({ filter }) { - const {removeFilter} = useContext(ContextRepositoryViewer) + const { removeFilter } = useContext(ContextRepositoryViewer) return ( { - let text = event.target.value - text = text.toUpperCase() - text = text.replace(INVALID_USER_CHARACTERS, "") - return setDatetime(text) - } - - const onButtonClick = e => { - const naive = new Date(datetime) - if(naive.toString() === "Invalid Date") { - console.debug("Refusing to add condition: ", naive, " is an Invalid Date.") + const submit = ({ date, isBefore }) => { + if(date.toString() === "Invalid Date") { + console.debug("Refusing to add condition: ", date, " is an Invalid Date.") return } - const aware = convertToLocalISODate(naive) - addCondition(new Condition("TIME", `${ba ? ">" : "<"} ${aware}`)) - setDatetime("") - - // Prevent reloading the page! - e.preventDefault() + const aware = convertToLocalISODate(date) + addCondition(new Condition("TIME", `${isBefore ? "<" : ">"} ${aware}`)) } return ( @@ -64,24 +43,9 @@ export default function BoxConditionDatetime({ ...props }) { } {...props} > - - - - - + ) } diff --git a/nest_frontend/components/interactive/BoxConditionHashtag.js b/nest_frontend/components/interactive/BoxConditionHashtag.js index f808b20..e83fb86 100644 --- a/nest_frontend/components/interactive/BoxConditionHashtag.js +++ b/nest_frontend/components/interactive/BoxConditionHashtag.js @@ -1,18 +1,11 @@ -import React, { useContext, useState } from "react" +import React, { useContext } from "react" import BoxFull from "../base/BoxFull" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import { faHashtag, faPlus } from "@fortawesome/free-solid-svg-icons" -import InputWithIcon from "../base/InputWithIcon" -import FormInline from "../base/FormInline" -import Style from "./BoxConditionHashtag.module.css" -import ButtonIconOnly from "../base/ButtonIconOnly" +import { faHashtag } from "@fortawesome/free-solid-svg-icons" import useRepositoryEditor from "../../hooks/useRepositoryEditor" import Condition from "../../utils/Condition" import ContextLanguage from "../../contexts/ContextLanguage" - -// Official hashtag regex from https://stackoverflow.com/a/22490853/4334568 -// noinspection RegExpAnonymousGroup,LongLine -const INVALID_HASHTAG_CHARACTERS = /([^a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73\u2b740-\u2b81\u2f800-\u2fa1])/g +import FormInlineHashtag from "./FormInlineHashtag" /** @@ -24,22 +17,11 @@ const INVALID_HASHTAG_CHARACTERS = /([^a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\ * @constructor */ export default function BoxConditionHashtag({ ...props }) { - const [hashtag, setHashtag] = useState("") const { addCondition } = useRepositoryEditor() const { strings } = useContext(ContextLanguage) - const onInputChange = event => { - let text = event.target.value - text = text.replace(INVALID_HASHTAG_CHARACTERS, "") - return setHashtag(text) - } - - const onButtonClick = e => { - addCondition(new Condition("HASHTAG", hashtag)) - setHashtag("") - - // Prevent reloading the page! - e.preventDefault() + const submit = value => { + addCondition(new Condition("HASHTAG", value)) } return ( @@ -55,22 +37,9 @@ export default function BoxConditionHashtag({ ...props }) { } {...props} > - - - - + ) } diff --git a/nest_frontend/components/interactive/BoxConditionMap.js b/nest_frontend/components/interactive/BoxConditionMap.js index 7709de2..d2a699b 100644 --- a/nest_frontend/components/interactive/BoxConditionMap.js +++ b/nest_frontend/components/interactive/BoxConditionMap.js @@ -1,4 +1,4 @@ -import React, { useCallback, useContext, useEffect, useState } from "react" +import React, { useCallback, useContext } from "react" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { faMapPin, faPlus } from "@fortawesome/free-solid-svg-icons" import ButtonIconOnly from "../base/ButtonIconOnly" @@ -6,34 +6,8 @@ import useRepositoryEditor from "../../hooks/useRepositoryEditor" import Condition from "../../utils/Condition" import ContextLanguage from "../../contexts/ContextLanguage" import BoxMap from "../base/BoxMap" - - -/** - * https://wiki.openstreetmap.org/wiki/Zoom_levels - */ -const MPIXEL = [ - 156412, - 78206, - 39103, - 19551, - 9776, - 4888, - 2444, - 1222, - 610.984, - 305.492, - 152.746, - 76.373, - 38.187, - 19.093, - 9.547, - 4.773, - 2.387, - 1.193, - 0.596, - 0.298, - 0.149, -] +import useMapView from "../../hooks/useMapView" +import osmZoomLevels from "../../utils/osmZoomLevels" /** @@ -44,55 +18,25 @@ const MPIXEL = [ * @constructor */ export default function BoxConditionMap({ ...props }) { - const [position, setPosition] = useState() - const [zoom, setZoom] = useState() - const [map, setMap] = useState(null) + const mapViewHook = useMapView() const { addCondition } = useRepositoryEditor() const { strings } = useContext(ContextLanguage) - const onMove = useCallback( + const onButtonClick = useCallback( () => { - setPosition(map.getCenter()) + const radius = mapViewHook.zoom * osmZoomLevels[mapViewHook.zoom] + + addCondition(new Condition( + "COORDINATES", + `< ${radius} ${mapViewHook.center.lat} ${mapViewHook.center.lng}`, + )) }, - [map], + [mapViewHook, addCondition] ) - const onZoom = useCallback( - () => { - setZoom(map.getZoom()) - }, - [map], - ) - - useEffect( - () => { - if(map === null) { - return - } - - map.on("move", onMove) - map.on("zoom", onZoom) - return () => { - map.off("move", onMove) - map.off("zoom", onZoom) - } - }, - [map, onMove, onZoom], - ) - - const onButtonClick = () => { - const mapSize = map.getSize() - const minSize = Math.min(mapSize.x, mapSize.y) - const radius = minSize * MPIXEL[zoom] - - addCondition(new Condition( - "COORDINATES", - `< ${radius} ${position.lat} ${position.lng}`, - )) - } - return ( {strings.searchBy} @@ -102,7 +46,6 @@ export default function BoxConditionMap({ ...props }) { {strings.byZone} } - setMap={setMap} button={ { - let text = event.target.value - text = text.replace(INVALID_USER_CHARACTERS, "") - return setUser(text) - } - - const onButtonClick = e => { - addCondition(new Condition("USER", user)) - setUser("") - - // Prevent reloading the page! - e.preventDefault() + const submit = value => { + addCondition(new Condition("USER", value)) } return ( @@ -54,22 +37,9 @@ export default function BoxConditionUser({ ...props }) { } {...props} > - - - - + ) } diff --git a/nest_frontend/components/interactive/BoxConditionUser.module.css b/nest_frontend/components/interactive/BoxConditionUser.module.css deleted file mode 100644 index f8b3933..0000000 --- a/nest_frontend/components/interactive/BoxConditionUser.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.Input { - flex-shrink: 1; -} - -.Button { - -} \ No newline at end of file diff --git a/nest_frontend/components/interactive/BoxFilterContains.js b/nest_frontend/components/interactive/BoxFilterContains.js index b36f4ee..4b57660 100644 --- a/nest_frontend/components/interactive/BoxFilterContains.js +++ b/nest_frontend/components/interactive/BoxFilterContains.js @@ -1,10 +1,11 @@ -import React, { useContext, useState } from "react" +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 FormInlineText from "./FormInlineText" import { faFont } from "@fortawesome/free-solid-svg-icons" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" export default function BoxFilterContains({ ...props }) { @@ -17,9 +18,19 @@ export default function BoxFilterContains({ ...props }) { // TODO: add this string return ( - + + {strings.searchBy} +   + +   + {strings.byContents} + + } + {...props} + > diff --git a/nest_frontend/components/interactive/BoxFilterDatetime.js b/nest_frontend/components/interactive/BoxFilterDatetime.js new file mode 100644 index 0000000..8fde916 --- /dev/null +++ b/nest_frontend/components/interactive/BoxFilterDatetime.js @@ -0,0 +1,35 @@ +import React from "react" +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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import FormInlineBADatetime from "./FormInlineBADatetime" + + +export default function BoxFilterDatetime({ ...props }) { + const strings = useStrings() + const { appendFilter } = useRepositoryViewer() + + const submit = ({ date, isBefore }) => { + appendFilter(new AfterDatetimeFilter(isBefore, date)) + } + + return ( + + {strings.searchBy} +   + +   + {strings.byTimePeriod} + + } + {...props} + > + + + ) +} diff --git a/nest_frontend/components/interactive/BoxFilterHasPlace.js b/nest_frontend/components/interactive/BoxFilterHasPlace.js new file mode 100644 index 0000000..799d5fa --- /dev/null +++ b/nest_frontend/components/interactive/BoxFilterHasPlace.js @@ -0,0 +1,49 @@ +import React from "react" +import BoxFull from "../base/BoxFull" +import FormInline from "../base/FormInline" +import useRepositoryViewer from "../../hooks/useRepositoryViewer" +import useStrings from "../../hooks/useStrings" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { faLocationArrow, faMapPin, faPlus } from "@fortawesome/free-solid-svg-icons" +import FormInlineLocation from "./FormInlineLocation" +import { HasPlaceFilter, LocationRadiusFilter } from "../../utils/Filter" +import ButtonIconOnly from "../base/ButtonIconOnly" + + +export default function BoxFilterHasPlace({ ...props }) { + const strings = useStrings() + + const { appendFilter } = useRepositoryViewer() + + const submit = () => { + appendFilter(new HasPlaceFilter(false)) + } + + // TODO: translate this + + return ( + + {strings.searchBy} +   + +   + {strings.byHasPlace} + + } + {...props} + > + +
+ {strings.hasPlaceExplaination} +
+ +
+
+ ) +} diff --git a/nest_frontend/components/interactive/BoxFilterHashtag.js b/nest_frontend/components/interactive/BoxFilterHashtag.js index 4096492..76aba03 100644 --- a/nest_frontend/components/interactive/BoxFilterHashtag.js +++ b/nest_frontend/components/interactive/BoxFilterHashtag.js @@ -1,43 +1,35 @@ -import React, { useContext, useState } from "react" +import React from "react" import BoxFull from "../base/BoxFull" -import FormInline from "../base/FormInline" -import InputWithIcon from "../base/InputWithIcon" -import Style from "./BoxConditionUser.module.css" -import { faAt, faFilter, faFont, faHashtag } from "@fortawesome/free-solid-svg-icons" -import ButtonIconOnly from "../base/ButtonIconOnly" +import { faClock } from "@fortawesome/free-solid-svg-icons" import useRepositoryViewer from "../../hooks/useRepositoryViewer" import useStrings from "../../hooks/useStrings" -import { ContainsFilter, HashtagFilter, UserFilter } from "../../utils/Filter" -import Condition from "../../utils/Condition" -import FormInlineText from "./FormInlineText" - - -const INVALID_HASHTAG_CHARACTERS = /([^a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73\u2b740-\u2b81\u2f800-\u2fa1])/g +import { HashtagFilter } from "../../utils/Filter" +import FormInlineHashtag from "./FormInlineHashtag" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" export default function BoxFilterHashtag({ ...props }) { - // TODO: Translate this - // TODO: and also use a better string maybe const strings = useStrings() - const { appendFilter } = useRepositoryViewer() - const validate = value => { - return value.replace(INVALID_HASHTAG_CHARACTERS, "") - } - const submit = value => { appendFilter(new HashtagFilter(false, value)) } return ( - - + + {strings.searchBy} +   + +   + {strings.byTimePeriod} + + } + {...props} + > + ) } diff --git a/nest_frontend/components/interactive/BoxFilterLocation.js b/nest_frontend/components/interactive/BoxFilterLocation.js new file mode 100644 index 0000000..851dc49 --- /dev/null +++ b/nest_frontend/components/interactive/BoxFilterLocation.js @@ -0,0 +1,37 @@ +import React from "react" +import BoxFull from "../base/BoxFull" +import FormInline from "../base/FormInline" +import useRepositoryViewer from "../../hooks/useRepositoryViewer" +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" + + +export default function BoxFilterLocation({ ...props }) { + const strings = useStrings() + + const { appendFilter, mapViewHook } = useRepositoryViewer() + + const submit = () => { + appendFilter(new LocationRadiusFilter(false, mapViewHook.center, mapViewHook.radius)) + } + + return ( + + {strings.searchBy} +   + +   + {strings.byZone} + + } + {...props} + > + + + ) +} diff --git a/nest_frontend/components/interactive/BoxFilterUser.js b/nest_frontend/components/interactive/BoxFilterUser.js index 37d24ea..9efd651 100644 --- a/nest_frontend/components/interactive/BoxFilterUser.js +++ b/nest_frontend/components/interactive/BoxFilterUser.js @@ -1,15 +1,11 @@ -import React, { useContext, useState } from "react" +import React from "react" import BoxFull from "../base/BoxFull" -import FormInline from "../base/FormInline" -import InputWithIcon from "../base/InputWithIcon" -import Style from "./BoxConditionUser.module.css" -import { faAt, faFilter, faFont } from "@fortawesome/free-solid-svg-icons" -import ButtonIconOnly from "../base/ButtonIconOnly" +import { faAt } from "@fortawesome/free-solid-svg-icons" import useRepositoryViewer from "../../hooks/useRepositoryViewer" import useStrings from "../../hooks/useStrings" -import { ContainsFilter, UserFilter } from "../../utils/Filter" -import Condition from "../../utils/Condition" -import FormInlineText from "./FormInlineText" +import { UserFilter } from "../../utils/Filter" +import FormInlineUser from "./FormInlineUser" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" export default function BoxFilterUser({ ...props }) { @@ -19,20 +15,24 @@ export default function BoxFilterUser({ ...props }) { const { appendFilter } = useRepositoryViewer() - const validate = value => { - return value.replace(/[^a-zA-Z0-9]/g, "") - } - const submit = value => { appendFilter(new UserFilter(false, value)) } return ( - - + {strings.searchBy} +   + +   + {strings.byUser} + + } + {...props} + > + diff --git a/nest_frontend/components/interactive/BoxFilters.js b/nest_frontend/components/interactive/BoxFilters.js index e9fe1f0..0745124 100644 --- a/nest_frontend/components/interactive/BoxFilters.js +++ b/nest_frontend/components/interactive/BoxFilters.js @@ -14,7 +14,7 @@ import BadgeFilter from "./BadgeFilter" */ export default function BoxFilters({ ...props }) { const { strings } = useContext(ContextLanguage) - const {filters} = useContext(ContextRepositoryViewer) + const { filters } = useContext(ContextRepositoryViewer) const badges = filters.map((filter, pos) => ) diff --git a/nest_frontend/components/interactive/BoxRepositoryTweets.js b/nest_frontend/components/interactive/BoxRepositoryTweets.js index 48966a7..b03a774 100644 --- a/nest_frontend/components/interactive/BoxRepositoryTweets.js +++ b/nest_frontend/components/interactive/BoxRepositoryTweets.js @@ -8,7 +8,7 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer" export default function BoxRepositoryTweets({ ...props }) { const { strings } = useContext(ContextLanguage) - const {tweets} = useContext(ContextRepositoryViewer) + const { tweets } = useContext(ContextRepositoryViewer) let content if(tweets.length === 0) { diff --git a/nest_frontend/components/interactive/BoxVisualizationChart.js b/nest_frontend/components/interactive/BoxVisualizationChart.js index b5718e0..58ecc9b 100644 --- a/nest_frontend/components/interactive/BoxVisualizationChart.js +++ b/nest_frontend/components/interactive/BoxVisualizationChart.js @@ -8,7 +8,7 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer" export default function BoxVisualizationChart({ ...props }) { const { strings } = useContext(ContextLanguage) - const {tweets} = useContext(ContextRepositoryViewer) + const { tweets } = useContext(ContextRepositoryViewer) const hours = [...Array(24).keys()].map(hour => hour.toString()) const hourlyTweetCount = Array(24).fill(0) @@ -37,9 +37,9 @@ export default function BoxVisualizationChart({ ...props }) { { label: "Tweets", data: hourlyTweetCount, - } + }, ], - } + }, }} {...props} /> diff --git a/nest_frontend/components/interactive/BoxVisualizationMap.js b/nest_frontend/components/interactive/BoxVisualizationMap.js index 6769d2d..9e4f82e 100644 --- a/nest_frontend/components/interactive/BoxVisualizationMap.js +++ b/nest_frontend/components/interactive/BoxVisualizationMap.js @@ -1,4 +1,4 @@ -import React, { useContext } from "react" +import React, { useContext, useMemo } from "react" import BoxMap from "../base/BoxMap" import ContextLanguage from "../../contexts/ContextLanguage" import { Marker, Popup } from "react-leaflet" @@ -8,28 +8,32 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer" export default function BoxVisualizationMap({ ...props }) { const { strings } = useContext(ContextLanguage) - const {tweets} = useContext(ContextRepositoryViewer) + const { tweets, mapViewHook } = useContext(ContextRepositoryViewer) - console.debug(tweets) - const markers = tweets.filter(tweet => tweet.location).map(tweet => { - const location = Location.fromTweet(tweet) + const markers = useMemo( + () => { + return tweets.filter(tweet => tweet.location).map(tweet => { + const location = Location.fromTweet(tweet) - return ( - - -

- {tweet["content"]} -

-

- — @{tweet["poster"]} -

-
-
- ) - }) + return ( + + +

+ {tweet["content"]} +

+

+ — @{tweet["poster"]} +

+
+
+ ) + }) + }, + [tweets], + ) return ( - + {markers} ) diff --git a/nest_frontend/components/interactive/BoxVisualizationStats.js b/nest_frontend/components/interactive/BoxVisualizationStats.js index 42825b6..4b9c721 100644 --- a/nest_frontend/components/interactive/BoxVisualizationStats.js +++ b/nest_frontend/components/interactive/BoxVisualizationStats.js @@ -8,7 +8,7 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer" export default function BoxVisualizationStats({ ...props }) { const { strings } = useContext(ContextLanguage) - const {tweets, words, rawTweets} = useContext(ContextRepositoryViewer) + const { tweets, words, rawTweets } = useContext(ContextRepositoryViewer) const tweetCount = useMemo( () => tweets.length, @@ -47,15 +47,19 @@ export default function BoxVisualizationStats({ ...props }) { const wordCount = useMemo( () => { - if(words.length === 0) return 0 + if(words.length === 0) { + return 0 + } return words.map(word => word.value).reduce((a, b) => a + b) }, - [words] + [words], ) const mostPopularWord = useMemo( () => { - if(words.length === 0) return "❌" + if(words.length === 0) { + return "❌" + } return words.sort((wa, wb) => { if(wa.value > wb.value) { return -1 @@ -149,7 +153,11 @@ export default function BoxVisualizationStats({ ...props }) { {uniqueUsersCount} - {mostActiveUser ? `${mostActiveUser.user} (${mostActiveUser.count} tweet${mostActiveUser.count === 1 ? "" : "s"})` : "❌"} + {mostActiveUser + ? `${mostActiveUser.user} (${mostActiveUser.count} tweet${mostActiveUser.count === 1 + ? "" + : "s"})` + : "❌"} diff --git a/nest_frontend/components/interactive/BoxVisualizationWordcloud.js b/nest_frontend/components/interactive/BoxVisualizationWordcloud.js index 543274e..9072146 100644 --- a/nest_frontend/components/interactive/BoxVisualizationWordcloud.js +++ b/nest_frontend/components/interactive/BoxVisualizationWordcloud.js @@ -9,7 +9,7 @@ import { ContainsFilter } from "../../utils/Filter" export default function BoxVisualizationWordcloud({ ...props }) { const { strings } = useContext(ContextLanguage) - const {words, appendFilter} = useContext(ContextRepositoryViewer) + const { words, appendFilter } = useContext(ContextRepositoryViewer) if(words.length === 0) { return ( @@ -27,7 +27,7 @@ export default function BoxVisualizationWordcloud({ ...props }) { ) diff --git a/nest_frontend/components/interactive/ButtonToggleBeforeAfter.js b/nest_frontend/components/interactive/ButtonToggleBeforeAfter.js index a7f2598..f8300a1 100644 --- a/nest_frontend/components/interactive/ButtonToggleBeforeAfter.js +++ b/nest_frontend/components/interactive/ButtonToggleBeforeAfter.js @@ -1,17 +1,15 @@ -import React, { useContext, useState } from "react" +import React, { useContext } from "react" import Style from "./ButtonToggleBeforeAfter.module.css" import classNames from "classnames" import Button from "../base/Button" import ContextLanguage from "../../contexts/ContextLanguage" -export default function ButtonToggleBeforeAfter({ onUpdate, className, ...props }) { - const [value, setValue] = useState(false) +export default function ButtonToggleBeforeAfter({ isBefore, setBefore, className, ...props }) { const { strings } = useContext(ContextLanguage) const onButtonClick = () => { - onUpdate(!value) - setValue(value => !value) + setBefore(a => !a) } return ( @@ -21,7 +19,7 @@ export default function ButtonToggleBeforeAfter({ onUpdate, className, ...props onClick={onButtonClick} {...props} > - {value ? strings.timeBefore : strings.timeAfter} + {isBefore ? strings.timeBefore : strings.timeAfter} ) } diff --git a/nest_frontend/components/interactive/FormInlineBADatetime.js b/nest_frontend/components/interactive/FormInlineBADatetime.js new file mode 100644 index 0000000..1e4055c --- /dev/null +++ b/nest_frontend/components/interactive/FormInlineBADatetime.js @@ -0,0 +1,62 @@ +import React, { useState } from "react" +import FormInline from "../base/FormInline" +import InputWithIcon from "../base/InputWithIcon" +import { faClock, faPlus } from "@fortawesome/free-solid-svg-icons" +import ButtonIconOnly from "../base/ButtonIconOnly" +import Style from "./FormInlineText.module.css" +import ButtonToggleBeforeAfter from "./ButtonToggleBeforeAfter" + + +const INVALID_CHARACTERS = /[^0-9TZ:+-]/g + + +export default function FormInlineBADatetime( + { + textIcon = faClock, + buttonIcon = faPlus, + buttonColor = "Green", + placeholder = new Date().toISOString(), + validate = value => value, + submit, + ...props + }, +) { + const [isBefore, setBefore] = useState(false) + const [value, setValue] = useState("") + + const _onSubmit = event => { + event.preventDefault() + submit({ + date: new Date(value), + isBefore, + }) + setValue("") + } + + const _onChange = event => { + setValue(validate(event.target.value.replace(INVALID_CHARACTERS, ""))) + } + + return ( + + + + + + ) +} diff --git a/nest_frontend/components/interactive/FormInlineHashtag.js b/nest_frontend/components/interactive/FormInlineHashtag.js new file mode 100644 index 0000000..a1a156f --- /dev/null +++ b/nest_frontend/components/interactive/FormInlineHashtag.js @@ -0,0 +1,26 @@ +import React from "react" +import FormInlineText from "./FormInlineText" +import { faHashtag } from "@fortawesome/free-solid-svg-icons" + + +// Official hashtag regex from https://stackoverflow.com/a/22490853/4334568 +// noinspection RegExpAnonymousGroup,LongLine +const INVALID_CHARACTERS = /([^a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73\u2b740-\u2b81\u2f800-\u2fa1])/g + + +export default function FormInlineHashtag({ submit, ...props }) { + + const validate = value => { + return value.replace(INVALID_CHARACTERS, "") + } + + return ( + + ) +} diff --git a/nest_frontend/components/interactive/FormInlineLocation.js b/nest_frontend/components/interactive/FormInlineLocation.js new file mode 100644 index 0000000..eab623b --- /dev/null +++ b/nest_frontend/components/interactive/FormInlineLocation.js @@ -0,0 +1,60 @@ +import React from "react" +import FormInline from "../base/FormInline" +import InputWithIcon from "../base/InputWithIcon" +import { faCircle, faPlus, faRulerHorizontal, faRulerVertical } from "@fortawesome/free-solid-svg-icons" +import ButtonIconOnly from "../base/ButtonIconOnly" +import Style from "./FormInlineLocation.module.css" + + +export default function FormInlineLocation( + { + mapViewHook, + radIcon = faCircle, + latIcon = faRulerHorizontal, + lngIcon = faRulerVertical, + buttonIcon = faPlus, + buttonColor = "Green", + placeholder = new Date().toISOString(), + validate = value => value, + submit, + ...props + }, +) { + + const _onSubmit = event => { + event.preventDefault() + submit() + } + + return ( + + + + + + + ) +} diff --git a/nest_frontend/components/interactive/BoxConditionHashtag.module.css b/nest_frontend/components/interactive/FormInlineLocation.module.css similarity index 51% rename from nest_frontend/components/interactive/BoxConditionHashtag.module.css rename to nest_frontend/components/interactive/FormInlineLocation.module.css index f8b3933..0db9538 100644 --- a/nest_frontend/components/interactive/BoxConditionHashtag.module.css +++ b/nest_frontend/components/interactive/FormInlineLocation.module.css @@ -1,4 +1,4 @@ -.Input { +.Radius, .Latitude, .Longitude { flex-shrink: 1; } diff --git a/nest_frontend/components/interactive/FormInlineText.js b/nest_frontend/components/interactive/FormInlineText.js index 660e4aa..00217f6 100644 --- a/nest_frontend/components/interactive/FormInlineText.js +++ b/nest_frontend/components/interactive/FormInlineText.js @@ -1,12 +1,22 @@ import React, { useState } from "react" import FormInline from "../base/FormInline" import InputWithIcon from "../base/InputWithIcon" -import { faPlus } from "@fortawesome/free-solid-svg-icons" +import { faFont, faPlus } from "@fortawesome/free-solid-svg-icons" import ButtonIconOnly from "../base/ButtonIconOnly" import Style from "./FormInlineText.module.css" -export default function FormInlineText({ textIcon, placeholder, buttonIcon = faPlus, buttonColor = "Green", validate = value => value, submit, ...props }) { +export default function FormInlineText( + { + textIcon = faFont, + buttonIcon = faPlus, + buttonColor = "Green", + placeholder = "", + validate = value => value, + submit, + ...props + }, +) { const [value, setValue] = useState("") const _onSubmit = event => { diff --git a/nest_frontend/components/interactive/FormInlineUser.js b/nest_frontend/components/interactive/FormInlineUser.js new file mode 100644 index 0000000..836acfa --- /dev/null +++ b/nest_frontend/components/interactive/FormInlineUser.js @@ -0,0 +1,26 @@ +import React from "react" +import FormInlineText from "./FormInlineText" +import { faAt } from "@fortawesome/free-solid-svg-icons" + + +// Official hashtag regex from https://stackoverflow.com/a/22490853/4334568 +// noinspection RegExpAnonymousGroup,LongLine +const INVALID_CHARACTERS = /[^a-zA-Z0-9]/g + + +export default function FormInlineUser({ submit, ...props }) { + + const validate = value => { + return value.replace(INVALID_CHARACTERS, "") + } + + return ( + + ) +} diff --git a/nest_frontend/components/interactive/PickerFilter.js b/nest_frontend/components/interactive/PickerFilter.js index 4416abf..c44f6b4 100644 --- a/nest_frontend/components/interactive/PickerFilter.js +++ b/nest_frontend/components/interactive/PickerFilter.js @@ -1,12 +1,12 @@ import React, { useContext } from "react" import ButtonIconOnly from "../base/ButtonIconOnly" -import { faAt, faClock, faFont, faHashtag, faMapPin, faStar } from "@fortawesome/free-solid-svg-icons" +import { faAt, faClock, faFont, faHashtag, faLocationArrow, faMapPin } from "@fortawesome/free-solid-svg-icons" import ButtonPicker from "./ButtonPicker" import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer" export default function PickerFilter({ ...props }) { - const {filterTab, setFilterTab} = useContext(ContextRepositoryViewer) + const { filterTab, setFilterTab, setVisualizationTab } = useContext(ContextRepositoryViewer) return (
@@ -37,7 +37,16 @@ export default function PickerFilter({ ...props }) { + { + setVisualizationTab("map") + setFilterTab("location") + }} + disabled={filterTab === "location"} + color={"Grey"} icon={faMapPin} />
diff --git a/nest_frontend/components/interactive/PickerVisualization.js b/nest_frontend/components/interactive/PickerVisualization.js index d04a492..37f85e0 100644 --- a/nest_frontend/components/interactive/PickerVisualization.js +++ b/nest_frontend/components/interactive/PickerVisualization.js @@ -4,8 +4,8 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer" import ButtonPicker from "./ButtonPicker" -export default function PickerVisualization({...props}) { - const {visualizationTab, setVisualizationTab} = useContext(ContextRepositoryViewer) +export default function PickerVisualization({ ...props }) { + const { visualizationTab, setVisualizationTab } = useContext(ContextRepositoryViewer) return (
diff --git a/nest_frontend/components/providers/RepositoryViewer.js b/nest_frontend/components/providers/RepositoryViewer.js index 84de8e3..f2dff8d 100644 --- a/nest_frontend/components/providers/RepositoryViewer.js +++ b/nest_frontend/components/providers/RepositoryViewer.js @@ -23,6 +23,10 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer" 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 BoxFilterDatetime from "../interactive/BoxFilterDatetime" +import BoxFilterHasPlace from "../interactive/BoxFilterHasPlace" export default function RepositoryViewer({ id, className, ...props }) { @@ -36,9 +40,11 @@ export default function RepositoryViewer({ id, className, ...props }) { setValue: setFilters, appendValue: appendFilter, spliceValue: spliceFilter, - removeValue: removeFilter + removeValue: removeFilter, } = useArrayState([]) + // FIXME: this has a severe performance impact, investigate + const mapViewHook = useMapView() // Repository const repositoryBr = useBackendResource( @@ -117,29 +123,33 @@ export default function RepositoryViewer({ id, className, ...props }) { {filterTab === "contains" ? : null} {filterTab === "hashtag" ? : null} {filterTab === "user" ? : null} - {filterTab === "time" ? "Time" : null} - {filterTab === "location" ? "Location" : null} + {filterTab === "time" ? : null} + {filterTab === "place" ? : null} + {filterTab === "location" ? : null} } return ( - +
{contents} diff --git a/nest_frontend/hooks/useMapView.js b/nest_frontend/hooks/useMapView.js new file mode 100644 index 0000000..990a931 --- /dev/null +++ b/nest_frontend/hooks/useMapView.js @@ -0,0 +1,18 @@ +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/routes/PageRepository.js b/nest_frontend/routes/PageRepository.js index 4c6441f..0cd0d6d 100644 --- a/nest_frontend/routes/PageRepository.js +++ b/nest_frontend/routes/PageRepository.js @@ -1,4 +1,4 @@ -import React, { useContext, useMemo, useState } from "react" +import React from "react" import { useParams } from "react-router" import RepositoryViewer from "../components/providers/RepositoryViewer" @@ -7,6 +7,6 @@ export default function PageRepository({ className, ...props }) { const { id } = useParams() return ( - + ) } diff --git a/nest_frontend/utils/Filter.js b/nest_frontend/utils/Filter.js index 8e88463..0f6b737 100644 --- a/nest_frontend/utils/Filter.js +++ b/nest_frontend/utils/Filter.js @@ -1,10 +1,11 @@ -import {Location} from "./location" +import { Location } from "./location" import { faAt, + faClock, faFilter, - faFont, faHashtag, + faFont, + faHashtag, faLocationArrow, - faMap, faMapMarkerAlt, faMapPin, } from "@fortawesome/free-solid-svg-icons" @@ -38,6 +39,7 @@ export class Filter { } } + export class ContainsFilter extends Filter { word @@ -68,7 +70,7 @@ export class HashtagFilter extends ContainsFilter { hashtag constructor(negate, hashtag) { - super(negate, `#${hashtag}`); + super(negate, `#${hashtag}`) this.hashtag = hashtag } @@ -109,15 +111,12 @@ export class UserFilter extends Filter { export class HasLocationFilter extends Filter { - hasLocation - - constructor(negate, hasLocation) { + constructor(negate) { super(negate) - this.hasLocation = hasLocation } check(tweet) { - return (tweet["location"] !== null) === this.hasLocation + return Boolean(tweet["location"]) } color() { @@ -129,21 +128,18 @@ export class HasLocationFilter extends Filter { } text() { - return this.hasLocation + return "" } } export class HasPlaceFilter extends Filter { - hasPlace - - constructor(negate, hasPlace) { + constructor(negate) { super(negate) - this.hasPlace = hasPlace } check(tweet) { - return (tweet["place"] !== null) === this.hasPlace + return Boolean(tweet["place"]) } color() { @@ -155,7 +151,7 @@ export class HasPlaceFilter extends Filter { } text() { - return this.hasPlace + return "" } } @@ -165,7 +161,7 @@ export class LocationRadiusFilter extends HasLocationFilter { radius constructor(negate, center, radius) { - super(negate, true); + super(negate) this.center = center this.radius = radius } @@ -175,12 +171,12 @@ export class LocationRadiusFilter extends HasLocationFilter { return false } - // FIXME: assuming the earth is flat + // 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(radius, 2) + const squaredRadius = Math.pow(this.radius, 2) return squaredDistance < squaredRadius } @@ -194,6 +190,32 @@ export class LocationRadiusFilter extends HasLocationFilter { } text() { - return `< ${this.radius} ${this.center.toString()}` + 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/defaultMapLocation.js b/nest_frontend/utils/defaultMapLocation.js new file mode 100644 index 0000000..042e333 --- /dev/null +++ b/nest_frontend/utils/defaultMapLocation.js @@ -0,0 +1,2 @@ +export const DEFAULT_MAP_CENTER = { lat: 0, lng: 0 } +export const DEFAULT_MAP_ZOOM = 3 diff --git a/nest_frontend/utils/location.js b/nest_frontend/utils/location.js index 6eea766..561f3d0 100644 --- a/nest_frontend/utils/location.js +++ b/nest_frontend/utils/location.js @@ -15,7 +15,7 @@ export class Location { if(!match) { throw new Error(`Invalid location string: ${locString}`) } - const {lat, lng} = match.groups + const { lat, lng } = match.groups return new Location(lat, lng) } diff --git a/nest_frontend/utils/osmZoomLevels.js b/nest_frontend/utils/osmZoomLevels.js new file mode 100644 index 0000000..1e21707 --- /dev/null +++ b/nest_frontend/utils/osmZoomLevels.js @@ -0,0 +1,26 @@ +/** + * https://wiki.openstreetmap.org/wiki/Zoom_levels + */ +export default [ + 62914560, + 31457280, + 15728640, + 7864320, + 3932160, + 1966080, + 983040, + 491520, + 245760, + 122880, + 61440, + 30720, + 15360, + 7680, + 3840, + 1920, + 960, + 480, + 240, + 120, + 60, +]