diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/special/SpecialGraphFactory.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/special/SpecialGraphFactory.java index 27c259631..b45dac0be 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/special/SpecialGraphFactory.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/special/SpecialGraphFactory.java @@ -78,4 +78,9 @@ public class SpecialGraphFactory { } } + public Map getGeocodes() { + if (geoCodes == null) prepareGeocodes(); + return geoCodes; + } + } \ No newline at end of file diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/QueryJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/QueryJSONResolver.java index d5d2a537b..23b82889e 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/QueryJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/QueryJSONResolver.java @@ -113,7 +113,9 @@ public class QueryJSONResolver implements Resolver { WebUser user = request.getUser().orElse(new WebUser("")); return user.hasPermission(WebPermission.ACCESS_QUERY) || user.hasPermission(WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_CALENDAR) - || user.hasPermission(WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_CALENDAR); + || user.hasPermission(WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_CALENDAR) + || user.hasPermission(WebPermission.PAGE_NETWORK_GEOLOCATIONS_MAP) + || user.hasPermission(WebPermission.PAGE_SERVER_GEOLOCATIONS_MAP); } @GET diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java index bfc6c932c..88c15a328 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java @@ -125,6 +125,7 @@ public enum HtmlLang implements Lang { TITLE_MOST_PLAYED_WORLD("html.label.mostPlayedWorld", "Most played World"), TEXT_CLICK_TO_EXPAND("html.text.clickToExpand", "Click to expand"), TEXT_CLICK_AND_DRAG("html.text.clickAndDrag", "Click and Drag for more"), + TEXT_CLICK("html.text.click", "Click for more"), TITLE_SERVER_PLAYTIME_30("html.label.serverPlaytime30days", "Server Playtime for 30 days"), TITLE_INSIGHTS("html.label.insights30days", "Insights for 30 days"), LABEL_AFK_TIME("html.label.afkTime", "AFK Time"), diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/GeolocationsFilter.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/GeolocationsFilter.java index 96be370f8..2fa240ed3 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/GeolocationsFilter.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/GeolocationsFilter.java @@ -17,6 +17,7 @@ package com.djrapitops.plan.storage.database.queries.filter.filters; import com.djrapitops.plan.delivery.domain.datatransfer.InputFilterDto; +import com.djrapitops.plan.delivery.rendering.json.graphs.special.SpecialGraphFactory; import com.djrapitops.plan.storage.database.DBSystem; import com.djrapitops.plan.storage.database.queries.objects.GeoInfoQueries; import com.djrapitops.plan.utilities.dev.Untrusted; @@ -27,14 +28,20 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; @Singleton public class GeolocationsFilter extends MultiOptionFilter { private final DBSystem dbSystem; + private final SpecialGraphFactory specialGraphFactory; + private Map countryNamesByGeocode; @Inject - public GeolocationsFilter(DBSystem dbSystem) {this.dbSystem = dbSystem;} + public GeolocationsFilter(DBSystem dbSystem, SpecialGraphFactory specialGraphFactory) { + this.dbSystem = dbSystem; + this.specialGraphFactory = specialGraphFactory; + } @Override public String getKind() { @@ -52,6 +59,19 @@ public class GeolocationsFilter extends MultiOptionFilter { @Override public Set getMatchingUserIds(@Untrusted InputFilterDto query) { - return dbSystem.getDatabase().query(GeoInfoQueries.userIdsOfPlayersWithGeolocations(getSelected(query))); + List selectedGeolocations = getSelected(query); + if (countryNamesByGeocode == null) { + prepCountryNames(); + } + List mappedFromGeocodes = selectedGeolocations.stream() + .map(geolocation -> countryNamesByGeocode.getOrDefault(geolocation, geolocation)) + .collect(Collectors.toList()); + return dbSystem.getDatabase().query(GeoInfoQueries.userIdsOfPlayersWithGeolocations(mappedFromGeocodes)); + } + + private void prepCountryNames() { + Map geocodesByCountryName = specialGraphFactory.getGeocodes(); + countryNamesByGeocode = geocodesByCountryName.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey, (s, s2) -> s)); } } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/GeoInfoQueries.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/GeoInfoQueries.java index da334b4d4..248642cf2 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/GeoInfoQueries.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/GeoInfoQueries.java @@ -33,6 +33,7 @@ import org.apache.commons.text.TextStringBuilder; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; +import java.util.stream.Collectors; import static com.djrapitops.plan.storage.database.sql.building.Sql.*; @@ -172,8 +173,8 @@ public class GeoInfoQueries { String sql = SELECT + "u." + UsersTable.ID + FROM + GeoInfoTable.TABLE_NAME + " g" + INNER_JOIN + UsersTable.TABLE_NAME + " u on u.id=g." + GeoInfoTable.USER_ID + - WHERE + GeoInfoTable.GEOLOCATION + + WHERE + "LOWER(" + GeoInfoTable.GEOLOCATION + ")" + " IN (" + Sql.nParameters(selected.size()) + ")"; - return db -> db.querySet(sql, RowExtractors.getInt(UsersTable.ID), selected); + return db -> db.querySet(sql, RowExtractors.getInt(UsersTable.ID), selected.stream().map(String::toLowerCase).collect(Collectors.toList())); } } \ No newline at end of file diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml index 8eea06f69..d40f47bd6 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml @@ -824,6 +824,7 @@ html: success: "新用户注册成功!你现在可以登录了。" usernameTip: "用户名最多可以包含 50 个字符。" text: + click: "Click for more" clickAndDrag: "Click and Drag for more" clickToExpand: "点击展开" comparing15days: "对比 15 天的情况" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml index 07ec4d378..107b3eefd 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml @@ -824,6 +824,7 @@ html: success: "Registered a new user successfully! You can now login." usernameTip: "Uživatelské jméno může být dlouhé 50 znaků." text: + click: "Click for more" clickAndDrag: "Click and Drag for more" clickToExpand: "Klikněte pro rozbalení" comparing15days: "Srovnání posledních 15 dní" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml index da7f69d29..9e234a94e 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml @@ -824,6 +824,7 @@ html: success: "Registered a new user successfully! You can now login." usernameTip: "Username can be up to 50 characters." text: + click: "Click for more" clickAndDrag: "Click and Drag for more" clickToExpand: "Klicke zum erweitern" comparing15days: "Vergleiche 15 Tage" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml index 0bc4cdd00..a9f4d45ca 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml @@ -824,6 +824,7 @@ html: success: "Registered a new user successfully! You can now login." usernameTip: "Username can be up to 50 characters." text: + click: "Click for more" clickAndDrag: "Click and Drag for more" clickToExpand: "Click to expand" comparing15days: "Comparing 15 days" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml index 51dffb878..88fac8197 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml @@ -824,6 +824,7 @@ html: success: "Registered a new user successfully! You can now login." usernameTip: "El nombre de usuario no puede superar los 50 caracteres." text: + click: "Click for more" clickAndDrag: "Click and Drag for more" clickToExpand: "Haz clic para expandir" comparing15days: "Comparando 15 dias" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml index e682e3858..6637a7e1d 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml @@ -824,6 +824,7 @@ html: success: "Käyttäjä rekisteröitiin onnistuneesti! Voit nyt kirjautua." usernameTip: "Käyttäjänimi voi olla enintään 50 merkkiä." text: + click: "Click for more" clickAndDrag: "Click and Drag for more" clickToExpand: "Klikkaa laajentaaksesi" comparing15days: "Verrataan 15 päivää" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml index 8fd745286..99c3b9d72 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml @@ -824,6 +824,7 @@ html: success: "Registered a new user successfully! You can now login." usernameTip: "Le Nom d'Utilisateur peut comporter jusqu'à 50 caractères." text: + click: "Click for more" clickAndDrag: "Click and Drag for more" clickToExpand: "Cliquez pour agrandir" comparing15days: "Comparaison des 15 derniers Jours" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml index 9d5387886..cfabc98c5 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml @@ -824,6 +824,7 @@ html: success: "Registered a new user successfully! You can now login." usernameTip: "Username can be up to 50 characters." text: + click: "Click for more" clickAndDrag: "Click and Drag for more" clickToExpand: "Clicca per espendere" comparing15days: "Comparazione di 15 giorni" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml index 662c1a71d..e1c4262ca 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml @@ -824,6 +824,7 @@ html: success: "新規ユーザー登録が完了しました!ログインできるようになりました。" usernameTip: "ユーザー名は50文字以内で指定します" text: + click: "Click for more" clickAndDrag: "Click and Drag for more" clickToExpand: "クリックして展開" comparing15days: "直近15日との比較" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml index a5814fac6..b8c195c90 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml @@ -824,6 +824,7 @@ html: success: "Registered a new user successfully! You can now login." usernameTip: "Username can be up to 50 characters." text: + click: "Click for more" clickAndDrag: "Click and Drag for more" clickToExpand: "확장하려면 클릭" comparing15days: "지난 15일 비교" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml index c9b536760..0eacbea89 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml @@ -824,6 +824,7 @@ html: success: "Registered a new user successfully! You can now login." usernameTip: "Gebruikersnaam mag maximaal 50 tekens bevatten." text: + click: "Click for more" clickAndDrag: "Click and Drag for more" clickToExpand: "Klik om uit te breiden" comparing15days: "15 dagen vergelijken" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml index 76cc94ee4..a7c388333 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml @@ -824,6 +824,7 @@ html: success: "Registered a new user successfully! You can now login." usernameTip: "Username can be up to 50 characters." text: + click: "Click for more" clickAndDrag: "Click and Drag for more" clickToExpand: "Click to expand" comparing15days: "Comparing 15 days" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml index 8b5caf3fd..bc68295e0 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml @@ -824,6 +824,7 @@ html: success: "Registered a new user successfully! You can now login." usernameTip: "Ник должен быть не длиннее 50 символов." text: + click: "Click for more" clickAndDrag: "Click and Drag for more" clickToExpand: "Нажмите, чтобы развернуть" comparing15days: "Сравнение 15 дней" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml index a5944f190..953446c61 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml @@ -824,6 +824,7 @@ html: success: "Registered a new user successfully! You can now login." usernameTip: "Kullanıcı adı 50 karaktere kadar olabilir." text: + click: "Click for more" clickAndDrag: "Click and Drag for more" clickToExpand: "Genişletmek için tıklayın" comparing15days: "15 gün karşılaştırılıyor" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_UK.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_UK.yml index a289bc38d..e6a0ab20a 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_UK.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_UK.yml @@ -824,6 +824,7 @@ html: success: "Ви успішно зареєстрували нового користувача! Тепер ви можете увійти в систему." usernameTip: "Нікнейм має бути не довшим за 50 символів." text: + click: "Click for more" clickAndDrag: "Click and Drag for more" clickToExpand: "Натисніть, щоб розгорнути" comparing15days: "Порівняння 15 днів" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml index e61fe97b0..0596fc9e4 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml @@ -824,6 +824,7 @@ html: success: "Registered a new user successfully! You can now login." usernameTip: "使用者名稱最多可以包含 50 個字符。" text: + click: "Click for more" clickAndDrag: "Click and Drag for more" clickToExpand: "點擊展開" comparing15days: "對比 15 天的情況" diff --git a/Plan/react/dashboard/src/components/cards/common/GeolocationsCard.js b/Plan/react/dashboard/src/components/cards/common/GeolocationsCard.js index 5c2a781e1..032e7dcad 100644 --- a/Plan/react/dashboard/src/components/cards/common/GeolocationsCard.js +++ b/Plan/react/dashboard/src/components/cards/common/GeolocationsCard.js @@ -1,12 +1,18 @@ import {useTranslation} from "react-i18next"; import {Card, Col, Dropdown} from "react-bootstrap"; -import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; -import React, {useState} from "react"; +import {FontAwesomeIcon, FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; +import React, {useCallback, useState} from "react"; import {faExclamationTriangle, faGlobe, faLayerGroup} from "@fortawesome/free-solid-svg-icons"; import GeolocationBarGraph from "../../graphs/GeolocationBarGraph"; import GeolocationWorldMap, {ProjectionOptions} from "../../graphs/GeolocationWorldMap"; import {CardLoader} from "../../navigation/Loader"; import ExtendableRow from "../../layout/extension/ExtendableRow"; +import Highcharts from "highcharts/highstock"; +import {postQuery} from "../../../service/queryService"; +import {useMetadata} from "../../../hooks/metadataHook"; +import QueryPlayerListModal from "../../modal/QueryPlayerListModal"; +import {faHandPointer} from "@fortawesome/free-regular-svg-icons"; +import CardHeader from "../CardHeader"; const ProjectionDropDown = ({projection, setProjection}) => { const {t} = useTranslation(); @@ -14,7 +20,7 @@ const ProjectionDropDown = ({projection, setProjection}) => { const projectionOptions = Object.values(ProjectionOptions); return ( - {t(projection)} @@ -32,10 +38,47 @@ const ProjectionDropDown = ({projection, setProjection}) => { ) } -const GeolocationsCard = ({data}) => { +const GeolocationsCard = ({identifier, data}) => { const {t} = useTranslation(); + const {networkMetadata} = useMetadata(); const [projection, setProjection] = useState(ProjectionOptions.MILLER); + const [modalOpen, setModalOpen] = useState(false); + const [queryData, setQueryData] = useState(undefined); + const [country, setCountry] = useState(undefined); + + const closeModal = useCallback(() => { + setModalOpen(false); + }, [setModalOpen]); + + const onClickCountry = useCallback(async selectionInfo => { + const selectedCountry = selectionInfo?.point['iso-a3']; + if (!selectedCountry) return; + const end = Highcharts.dateFormat('%d/%m/%Y', Date.now()); + const query = { + filters: [{ + kind: "geolocations", + parameters: { + selected: `["${selectedCountry}"]` + } + }], + view: { + afterDate: "01/01/1970", afterTime: "00:00", + beforeDate: end, beforeTime: "00:00", + servers: networkMetadata?.servers.filter(server => server.serverUUID === identifier) || [] + } + } + setQueryData(undefined); + setCountry(undefined); + setModalOpen(true); + const data = await postQuery(query); + const loaded = data?.data; + if (loaded) { + setQueryData(loaded); + setCountry(selectionInfo.point.name); + } + }, [setQueryData, setModalOpen, networkMetadata, identifier, setCountry]); + if (!data) return if (!data?.geolocations_enabled) { @@ -48,12 +91,14 @@ const GeolocationsCard = ({data}) => { return ( - -
- {t('html.label.geolocations')} -
+ + -
+

+ {t('html.text.click')} +

+ @@ -61,7 +106,7 @@ const GeolocationsCard = ({data}) => { + projection={projection} onClickCountry={onClickCountry}/> diff --git a/Plan/react/dashboard/src/components/graphs/GeolocationWorldMap.js b/Plan/react/dashboard/src/components/graphs/GeolocationWorldMap.js index 9064b5f1c..5a6002848 100644 --- a/Plan/react/dashboard/src/components/graphs/GeolocationWorldMap.js +++ b/Plan/react/dashboard/src/components/graphs/GeolocationWorldMap.js @@ -28,7 +28,7 @@ const getProjection = option => { } } -const GeolocationWorldMap = ({series, colors, projection}) => { +const GeolocationWorldMap = ({series, colors, projection, onClickCountry}) => { const {t} = useTranslation(); const {nightModeEnabled, graphTheming} = useTheme(); @@ -37,7 +37,12 @@ const GeolocationWorldMap = ({series, colors, projection}) => { name: t('html.label.players'), type: 'map', data: series, - joinBy: ['iso-a3', 'code'] + joinBy: ['iso-a3', 'code'], + point: { + events: { + click: onClickCountry + } + } }; NoDataDisplay(Highcharts); @@ -71,7 +76,7 @@ const GeolocationWorldMap = ({series, colors, projection}) => { }, series: [mapSeries] }) - }, [colors, series, graphTheming, nightModeEnabled, t, projection]); + }, [colors, series, graphTheming, nightModeEnabled, t, projection, onClickCountry]); return (
); }; diff --git a/Plan/react/dashboard/src/components/modal/QueryPlayerListModal.js b/Plan/react/dashboard/src/components/modal/QueryPlayerListModal.js index 6184e45e0..d332502be 100644 --- a/Plan/react/dashboard/src/components/modal/QueryPlayerListModal.js +++ b/Plan/react/dashboard/src/components/modal/QueryPlayerListModal.js @@ -8,13 +8,13 @@ import {getViewTitle} from "../../views/query/QueryResultView"; import {ChartLoader} from "../navigation/Loader"; import {Link} from "react-router-dom"; -const QueryPlayerListModal = ({open, toggle, queryData}) => { +const QueryPlayerListModal = ({open, toggle, queryData, title}) => { const {t} = useTranslation(); return ( - {queryData ? getViewTitle(queryData, t, true) : t('html.query.title.text').replace('<', '')} + {queryData ? title || getViewTitle(queryData, t, true) : t('html.query.title.text').replace('<', '')}