mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2025-01-09 09:57:35 +01:00
Add possibility of clicking geolocation map to Query players from there
This commit is contained in:
parent
9c9d029268
commit
c032009144
@ -78,4 +78,9 @@ public class SpecialGraphFactory {
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, String> getGeocodes() {
|
||||
if (geoCodes == null) prepareGeocodes();
|
||||
return geoCodes;
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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"),
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
@ -824,6 +824,7 @@ html:
|
||||
success: "新用户注册成功!你现在可以登录了。"
|
||||
usernameTip: "用户名最多可以包含 50 个字符。"
|
||||
text:
|
||||
click: "Click for more"
|
||||
clickAndDrag: "Click and Drag for more"
|
||||
clickToExpand: "点击展开"
|
||||
comparing15days: "对比 15 天的情况"
|
||||
|
@ -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í"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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ää"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -824,6 +824,7 @@ html:
|
||||
success: "新規ユーザー登録が完了しました!ログインできるようになりました。"
|
||||
usernameTip: "ユーザー名は50文字以内で指定します"
|
||||
text:
|
||||
click: "Click for more"
|
||||
clickAndDrag: "Click and Drag for more"
|
||||
clickToExpand: "クリックして展開"
|
||||
comparing15days: "直近15日との比較"
|
||||
|
@ -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일 비교"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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 дней"
|
||||
|
@ -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"
|
||||
|
@ -824,6 +824,7 @@ html:
|
||||
success: "Ви успішно зареєстрували нового користувача! Тепер ви можете увійти в систему."
|
||||
usernameTip: "Нікнейм має бути не довшим за 50 символів."
|
||||
text:
|
||||
click: "Click for more"
|
||||
clickAndDrag: "Click and Drag for more"
|
||||
clickToExpand: "Натисніть, щоб розгорнути"
|
||||
comparing15days: "Порівняння 15 днів"
|
||||
|
@ -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 天的情況"
|
||||
|
@ -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>
|
||||
|
@ -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"/>);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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}/>}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user