Add possibility of clicking geolocation map to Query players from there

This commit is contained in:
Aurora Lahtela 2023-10-18 19:12:36 +03:00
parent 9c9d029268
commit c032009144
26 changed files with 119 additions and 22 deletions

View File

@ -78,4 +78,9 @@ public class SpecialGraphFactory {
}
}
public Map<String, String> getGeocodes() {
if (geoCodes == null) prepareGeocodes();
return geoCodes;
}
}

View File

@ -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

View File

@ -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"),

View File

@ -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<String, String> 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<Integer> getMatchingUserIds(@Untrusted InputFilterDto query) {
return dbSystem.getDatabase().query(GeoInfoQueries.userIdsOfPlayersWithGeolocations(getSelected(query)));
List<String> selectedGeolocations = getSelected(query);
if (countryNamesByGeocode == null) {
prepCountryNames();
}
List<String> mappedFromGeocodes = selectedGeolocations.stream()
.map(geolocation -> countryNamesByGeocode.getOrDefault(geolocation, geolocation))
.collect(Collectors.toList());
return dbSystem.getDatabase().query(GeoInfoQueries.userIdsOfPlayersWithGeolocations(mappedFromGeocodes));
}
private void prepCountryNames() {
Map<String, String> geocodesByCountryName = specialGraphFactory.getGeocodes();
countryNamesByGeocode = geocodesByCountryName.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey, (s, s2) -> s));
}
}

View File

@ -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()));
}
}

View File

@ -824,6 +824,7 @@ html:
success: "新用户注册成功!你现在可以登录了。"
usernameTip: "用户名最多可以包含 50 个字符。"
text:
click: "Click for more"
clickAndDrag: "Click and Drag for more"
clickToExpand: "点击展开"
comparing15days: "对比 15 天的情况"

View File

@ -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í"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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ää"

View File

@ -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"

View File

@ -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"

View File

@ -824,6 +824,7 @@ html:
success: "新規ユーザー登録が完了しました!ログインできるようになりました。"
usernameTip: "ユーザー名は50文字以内で指定します"
text:
click: "Click for more"
clickAndDrag: "Click and Drag for more"
clickToExpand: "クリックして展開"
comparing15days: "直近15日との比較"

View File

@ -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일 비교"

View File

@ -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"

View File

@ -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"

View File

@ -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 дней"

View File

@ -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"

View File

@ -824,6 +824,7 @@ html:
success: "Ви успішно зареєстрували нового користувача! Тепер ви можете увійти в систему."
usernameTip: "Нікнейм має бути не довшим за 50 символів."
text:
click: "Click for more"
clickAndDrag: "Click and Drag for more"
clickToExpand: "Натисніть, щоб розгорнути"
comparing15days: "Порівняння 15 днів"

View File

@ -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 天的情況"

View File

@ -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 (
<Dropdown className="float-end" style={{position: "absolute", right: "0.5rem"}}
<Dropdown className="float-end" style={{margin: "-0.5rem", marginLeft: 0}}
title={t('html.label.geoProjection.dropdown')}>
<Dropdown.Toggle variant=''>
<Fa icon={faLayerGroup}/> {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 <CardLoader/>
if (!data?.geolocations_enabled) {
@ -48,12 +91,14 @@ const GeolocationsCard = ({data}) => {
return (
<Card id={"geolocations"}>
<Card.Header>
<h6 className="col-black">
<Fa icon={faGlobe} className="col-green"/> {t('html.label.geolocations')}
</h6>
<QueryPlayerListModal open={modalOpen} toggle={closeModal} queryData={queryData}
title={"View " + t('html.query.filter.generic.start') + t('html.query.filter.country.text') + ': ' + country}/>
<CardHeader icon={faGlobe} color={"green"} label={'html.label.geolocations'}>
<ProjectionDropDown projection={projection} setProjection={setProjection}/>
</Card.Header>
<p style={{margin: 0, fontWeight: "normal"}} className={"float-end"}>
<FontAwesomeIcon icon={faHandPointer}/> {t('html.text.click')}
</p>
</CardHeader>
<Card.Body className="chart-area" style={{height: "100%"}}>
<ExtendableRow id={'row-geolocations-graphs-card-0'}>
<Col md={3}>
@ -61,7 +106,7 @@ const GeolocationsCard = ({data}) => {
</Col>
<Col md={9}>
<GeolocationWorldMap series={data.geolocation_series} colors={data.colors}
projection={projection}/>
projection={projection} onClickCountry={onClickCountry}/>
</Col>
</ExtendableRow>
</Card.Body>

View File

@ -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 (<div id="countryWorldMap"/>);
};

View File

@ -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 (
<Modal id="queryModal" aria-labelledby="queryModalLabel" show={open} onHide={toggle} size="xl">
<Modal.Header>
<Modal.Title id="queryModalLabel">
<Fa icon={faSearch}/> {queryData ? getViewTitle(queryData, t, true) : t('html.query.title.text').replace('<', '')}
<Fa icon={faSearch}/> {queryData ? title || getViewTitle(queryData, t, true) : t('html.query.title.text').replace('<', '')}
</Modal.Title>
<button aria-label="Close" className="btn-close" type="button" onClick={toggle}/>
</Modal.Header>

View File

@ -7,7 +7,7 @@ import LoadIn from "../../components/animation/LoadIn";
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
const Geolocations = (
{className, geolocationData, pingData, geolocationError, pingError, seeGeolocations, seePing}
{className, identifier, geolocationData, pingData, geolocationError, pingError, seeGeolocations, seePing}
) => {
return (
<LoadIn>
@ -16,7 +16,8 @@ const Geolocations = (
<Col md={12}>
{seeGeolocations && <>
{geolocationError ? <ErrorViewCard error={geolocationError}/>
: <GeolocationsCard data={geolocationData}/>}
: <GeolocationsCard identifier={identifier}
data={geolocationData}/>}
</>}
{seePing && <>
{pingError ? <ErrorViewCard error={pingError}/> : <PingTableCard data={pingData}/>}

View File

@ -16,6 +16,7 @@ const ServerGeolocations = () => {
return (
<Geolocations className={"server-geolocations"}
identifier={identifier}
geolocationData={data} geolocationError={loadingError} seeGeolocations={seeGeolocations}
pingData={pingData} pingError={pingLoadingError} seePing={seePing}
/>