Commit 4b9c0ea8 authored by Blaise de Carné's avatar Blaise de Carné
Browse files

feat: add color point on map

parent ef7989bc
Pipeline #23459 failed with stage
in 13 seconds
......@@ -6,9 +6,12 @@ et le projet utilise [Semantic Versioning](http://semver.org/).
## [Unreleased]
## [0.2.2] - 2022-02-17
### Ajout
- Explorer : ajout de la facette "Nombre de média"
- Dashboard : Le 2nd niveau de type de POI est maintenant visible dans le widget répartition
- Carte : les points sont maintenant colorisé en fonction de leur premier type primaire.
### Modification
- Carte : la taille des points est maintenant linéairement liée au niveau de zoom
......
/*
* This file is part of the DATAtourisme project.
* 2022
* @author Conjecto <contact@conjecto.com>
* SPDX-License-Identifier: GPL-3.0-or-later
* For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
*/
import { useCallback } from "react";
import MultiList, { MultiListProps } from "./MultiList";
......
......@@ -114,7 +114,7 @@ const ResultDataGridComponent = (props: ResultDataGridComponentProps) => {
defaultColumnOptions={_defaultColumnOptions}
{...gridProps}
/>
<div className={`absolute flex justify-center items-center top-0 left-0 bottom-0 right-0 transition ease-in-out delay-50 bg-white/80 pointer-events-non opacity-${loading ? "100" : "0"}`}>
<div className={`absolute flex justify-center items-center top-0 left-0 bottom-0 right-0 transition ease-in-out delay-50 bg-white/80 pointer-events-none opacity-${loading ? "100" : "0"}`}>
<Spinner size={16} className="w-16 h-16"></Spinner>
</div>
</div>
......
......@@ -16,7 +16,7 @@
import { ReactiveComponentProps } from "@appbaseio/reactivesearch/lib/components/basic/ReactiveComponent";
import { ReactiveComponent } from "@appbaseio/reactivesearch";
import { useEffect, useRef, useState } from "react";
import { PropsWithChildren, useEffect, useRef, useState } from "react";
import { MapBrowserEvent, Overlay } from "ol";
import Map from 'ol/Map'
......@@ -36,15 +36,16 @@ import { click } from "ol/events/condition";
import { ReactiveComponentRenderProps } from "../interfaces";
import { BiInfoCircle } from "react-icons/bi";
import hexRgb from "hex-rgb";
export type ResultMapProps = {
export type ResultMapProps = PropsWithChildren<{
dataField: string;
includeFields?: string[];
createFeature: (doc: any) => Feature<Point>;
onFeatureClick?: (feature: Feature<Point> | null) => void;
getTooltipContent?: (feature: Feature<Point> | null) => string;
selectedFeatureId?: string | number | null;
} & ReactiveComponentProps;
}> & Omit<ReactiveComponentProps, "children">;
// const style = {
// "symbol": {
......@@ -93,7 +94,7 @@ const parseExtent = (value: string): number[] => {
const ResultMapComponent = (
props: ReactiveComponentRenderProps & ResultMapProps
) => {
const { dataField, data, createFeature, onFeatureClick, selectedFeatureId, getTooltipContent, loading, setQuery, value, size } = props;
const { dataField, data, createFeature, onFeatureClick, selectedFeatureId, getTooltipContent, loading, setQuery, value, size, children } = props;
const mapElement = useRef<HTMLDivElement>(null);
const tooltipElement = useRef<HTMLDivElement>(null);
const center = [1.71, 46.71];
......@@ -118,8 +119,9 @@ const ResultMapComponent = (
symbol: {
symbolType: 'circle',
size: ['interpolate', ['linear'], ['zoom'], 1, 1, 15, 15],
// color: '#c02846',
color: ["case", ["==", ["get", "id"], ["var", "selectedFeatureId"]], "#c02846", "#116390"],
// https://github.com/openlayers/openlayers/issues/10381
// color: ["case", ["==", ["get", "id"], ["var", "selectedFeatureId"]], "#c02846", ['get', 'color']],
color: ["case", ["==", ["get", "id"], ["var", "selectedFeatureId"]], "#c02846", ['color', ['get', 'red'], ['get', 'green'], ['get', 'blue']]],
// color: ["case", ["==", ["get", "id"], ["var", "selectedFeatureId"]], "#c02846", ["case", ["==", ["get", "id"], ["var", "hoveredFeatureId"]], "#62bbec", "#116390"]],
offset: [0, 0],
opacity: 0.95
......@@ -213,7 +215,16 @@ const ResultMapComponent = (
// update markers
useEffect(() => {
if (map) {
const features = data.map((hit: any) => createFeature(hit));
const features = data.map((hit: any) => {
const feature = createFeature(hit);
const properties = feature.getProperties();
if (properties.color) {
// @see https://github.com/openlayers/openlayers/issues/10381
feature.setProperties({ ...properties, ...hexRgb(properties.color) });
}
return feature;
});
const _layer = new WebGLPointsLayer({
source : new VectorSource({ features }),
......@@ -241,10 +252,13 @@ const ResultMapComponent = (
}, [extent]);
return (<div ref={mapElement} className="map-container w-full h-full relative">
<div className='absolute z-10 bottom-0 left-0 bg-white bg-opacity-80 px-2 py-1 text-xs flex items-center'>
<BiInfoCircle className="mr-1" /><span>Le nombre de points affichés à l'écran est limité à <strong>{size}</strong></span>
<div className='absolute z-10 top-3 left-0 right-0 flex justify-center'>
<div className="bg-white bg-opacity-80 px-2 py-1 text-xs flex items-center">
<BiInfoCircle className="mr-1" /><span>Le nombre de points affichés à l'écran est limité à <strong>{size}</strong></span>
</div>
</div>
<div ref={tooltipElement} className="bg-black bg-opacity-80 text-white py-1 px-2 shadow-xl rounded-md"></div>
{ children }
</div>)
};
......@@ -252,7 +266,7 @@ const ResultMapComponent = (
* ResultMap
*/
const ResultMap = (props: ResultMapProps) => {
const { includeFields, dataField, size } = props;
const { includeFields, dataField, size, children, ...hocProps } = props;
const defaultQuery = (value: any) => {
let query: any = {exists: {field: dataField}};
......@@ -279,7 +293,7 @@ const ResultMap = (props: ResultMapProps) => {
render={(p: ReactiveComponentRenderProps) => (
<ResultMapComponent {...props} {...p} />
)}
{...props}
{...hocProps}
/>
);
};
......
......@@ -16,7 +16,7 @@ import LeaderBoardWidget from "./components/LeaderBoardWidget";
import MapWidget from "./components/MapWidget";
import POIEvolutionWidget from "./components/POIEvolutionWidget";
import POIRepartitionWidget from "./components/POIRepartitionWidget";
import { colors } from "./variables";
import { colors } from "../variables";
export type DashboardContextParams = {
client: ElasticsearchClient
......
......@@ -10,8 +10,8 @@ import { ResponsiveLine } from "@nivo/line";
import moment from "moment";
import { useContext, useEffect, useState } from "react";
import { format } from "../../utils/number";
import { analyzers } from "../../variables";
import { DashboardContext } from "../Dashboard";
import { anomalyCategories } from "../variables";
const AnomalyEvolutionWidget = () => {
const { client, theme, colors } = useContext(DashboardContext);
......@@ -31,7 +31,7 @@ const AnomalyEvolutionWidget = () => {
return Object.entries(obj).map((e) => {
const key = e[0];
const {label, color} = anomalyCategories[key];
const {label, color} = analyzers[key];
return { id: label || key, data: e[1], color };
});
};
......
......@@ -11,7 +11,7 @@ import { format } from "../../utils/number";
import { DashboardContext } from "../Dashboard";
import { ResponsivePie } from "@nivo/pie";
import { anomalyCategories } from "../variables";
import { analyzers } from "../../variables";
const AnomalyRepartitionWidget = () => {
const { client, theme } = useContext(DashboardContext);
......@@ -23,7 +23,7 @@ const AnomalyRepartitionWidget = () => {
const processBuckets = (data) => {
return data.map((b: any) => {
const { key } = b;
const { label, color } = anomalyCategories[key];
const { label, color } = analyzers[key];
return { id: label || key, value: b.doc_count, color };
});
};
......
......@@ -6,44 +6,7 @@
* For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
*/
import { anomalyAnalyzers } from "../variables";
// const colors = ["#0284c7", "#f97316", "#59a14f", "#af7aa1", "#76b7b2","#edc949", "#ff9da7", "#9c755f", "#bab0ab"];
export const colors: string[] = [
"#0284c7",
"#f97316",
"#65a30d",
"#facc15",
"#76b7b2",
"#a855f7",
"#fda4af",
"#9c755f",
"#bab0ab",
"#5a5a5a"
];
export const anomalyCategories: {[key: string]: {label: string, color: string}} = {
MediaAnalyzer: {
...anomalyAnalyzers.MediaAnalyzer,
color: colors[0],
},
TextAnalyzer: {
...anomalyAnalyzers.TextAnalyzer,
color: colors[1],
},
GeoCoordinatesAnalyzer: {
...anomalyAnalyzers.GeoCoordinatesAnalyzer,
color: colors[2],
},
ConsistencyAnalyzer: {
...anomalyAnalyzers.ConsistencyAnalyzer,
color: colors[3],
},
LinksAnalyzer: {
...anomalyAnalyzers.LinksAnalyzer,
color: colors[4],
},
};
import { colors } from "../variables";
export const objectCountCategories: {[key: string]: {label: string, color: string, area: boolean}} = {
objectCount: {
......
......@@ -9,7 +9,7 @@
import ConditionalComponent from "../../components/ConditionalComponent";
import MultiList from "../../components/MultiList";
import RangeMultiList from "../../components/RangeMultiList";
import { anomalyAnalyzerLabels } from "../../variables";
import { analyzersLabel } from "../../variables";
import AnomalyMessageMultiList from "./AnomalyMessageMultiList";
type AggregationsSidebarProps = {} & React.HTMLAttributes<HTMLDivElement>;
......@@ -85,7 +85,7 @@ const AggregationsSidebar = (props: AggregationsSidebarProps) => {
dataField="analyze.anomalies.analyzer"
showSearch={false}
size={999}
labels={anomalyAnalyzerLabels}
labels={analyzersLabel}
react={{
and: [
"search",
......
......@@ -10,6 +10,7 @@ import Feature from 'ol/Feature';
import Point from 'ol/geom/Point';
import ResultMap, { ResultMapProps } from "../../components/ResultMap";
import { types, typesByLabel } from '../../variables';
type CustomResultMapProps = {
onFeatureClick: (hit: any) => void
......@@ -21,9 +22,13 @@ const CustomResultMap = (props: CustomResultMapProps) => {
const createFeature = (hit: any): Feature<Point> => {
const lat = hit.isLocatedAt.geoPoint.lat;
const lon = hit.isLocatedAt.geoPoint.lon;
const type = hit.analyze.type1[0];
const color = typesByLabel[type]?.color;
return new Feature({
id: hit._id,
geometry: new Point(fromLonLat([lon,lat])),
geometry: new Point(fromLonLat([lon, lat])),
color,
hit
})
}
......@@ -31,14 +36,23 @@ const CustomResultMap = (props: CustomResultMapProps) => {
return (
<ResultMap
dataField="isLocatedAt.geoPoint"
includeFields={["isLocatedAt.geoPoint", "label"]}
includeFields={["isLocatedAt.geoPoint", "label", "analyze.type1"]}
URLParams={true}
size={5000}
createFeature={createFeature}
getTooltipContent={(f: Feature<Point>) => f.get('hit')['label']['@fr']}
{...props}
onFeatureClick={(f: Feature<Point>) => onFeatureClick(f.get("hit"))}
/>
>
<div className='absolute z-10 w-48 bottom-3 left-3 bg-white bg-opacity-80 px-2 py-2 text-sm flex flex-col shadow-lg'>
{Object.entries(types).map(([key, type]) => (
<div key={key} className='mb-2 last:mb-0 flex items-center'>
<div className='w-3 h-3 rounded-full mr-2' style={{ backgroundColor: type.color }}></div>
<div>{type.label}</div>
</div>
))}
</div>
</ResultMap>
);
};
......
......@@ -7,7 +7,7 @@
*/
import { BiError, BiHappy, BiTargetLock } from "react-icons/bi";
import { anomalyAnalyzerLabels } from "../../../../variables";
import { analyzers } from "../../../../variables";
const AnomaliesPanel = ({ data }: { data: { [key: string]: any } }) => {
const { anomalies } = data.analyze;
......@@ -36,7 +36,7 @@ const AnomaliesPanel = ({ data }: { data: { [key: string]: any } }) => {
<div className="flex items-center mb-2">
<BiError className="h-8 w-8 mr-2 text-gray-300" />
<div className="flex-1">
<div className="text-sm text-gray-600 flex items-center leading-none">{anomalyAnalyzerLabels[item.analyzer]}</div>
<div className="text-sm text-gray-600 flex items-center leading-none">{analyzers[item.analyzer].label}</div>
<div className="text-gray-900 font-bold text-md">{item.message}</div>
</div>
</div>
......
......@@ -11,7 +11,7 @@ import { FormatterProps } from "react-data-grid";
import { DefaultValueFormatter } from "../../../components/ResultDataGrid";
import extract from "../../../utils/extract";
import Popper from "@mui/base/PopperUnstyled";
import { anomalyAnalyzers } from "../../../variables";
import { analyzers } from "../../../variables";
const AnomalyStatsFormatter = (props: FormatterProps<any>) => {
const [anchorEl, setAnchorEl] = useState(null);
......@@ -42,7 +42,7 @@ const AnomalyStatsFormatter = (props: FormatterProps<any>) => {
<tbody>
{entries.map((entry) => {
const [key, value] = entry;
const { label } = anomalyAnalyzers[key];
const { label } = analyzers[key];
return (
<tr key={key}>
<td>{label}</td>
......
......@@ -11,10 +11,10 @@ import { Sparklines, SparklinesLine } from "react-sparklines";
import { FaRegEye } from "react-icons/fa";
import ElasticsearchClient, { SearchTotalHits } from "../utils/es-client";
import { format } from "../utils/number";
import { anomalyAnalyzerLabels, anomalyAnalyzers } from "../variables";
import StatsPanel from "./components/StatsPanel";
import EvolutionLabel from "./components/EvolutionLabel";
import { gotoExplorer } from "../utils/goto";
import { analyzers } from "../variables";
export type MonitorContextParams = {
client: ElasticsearchClient;
......@@ -56,7 +56,7 @@ const Monitor = ({ url }: { url: string }) => {
return (data.aggregations.anomalies as any).analyzer.buckets.map((b: any) => {
const { key, doc_count, parent, message } = b;
const label = anomalyAnalyzerLabels[key];
const label = analyzers[key].label;
const objectCount = parent.doc_count;
const stats = message.buckets.map((b2: any) => {
......@@ -293,7 +293,7 @@ const Monitor = ({ url }: { url: string }) => {
</thead>
<tbody>
{data.map((stats) => {
const Icon = anomalyAnalyzers[stats.id].icon;
const Icon = analyzers[stats.id].icon;
return (
<tr key={stats.id}>
<td className="p-3 whitespace-nowrap align-middle">
......
......@@ -5,30 +5,70 @@
* SPDX-License-Identifier: GPL-3.0-or-later
* For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
*/
import { IconType } from "react-icons";
import { BiImage, BiLink, BiListCheck, BiMapPin, BiSpreadsheet } from "react-icons/bi";
export const anomalyAnalyzers: {[key: string]: {label: string, icon: IconType}} = {
export const colors: string[] = [
"#0284c7",
"#f97316",
"#65a30d",
"#facc15",
"#76b7b2",
"#a855f7",
"#fda4af",
"#9c755f",
"#bab0ab",
"#5a5a5a"
];
export const analyzers: {[key: string]: {label: string, icon: IconType, color: string}} = {
MediaAnalyzer: {
label: "Anomalie de média",
icon: BiImage
icon: BiImage,
color: colors[0]
},
TextAnalyzer: {
label: "Anomalie de contenu",
icon: BiSpreadsheet
icon: BiSpreadsheet,
color: colors[1]
},
GeoCoordinatesAnalyzer: {
label: "Anomalie géospatiale",
icon: BiMapPin
icon: BiMapPin,
color: colors[2]
},
ConsistencyAnalyzer: {
label: "Anomalie de cohérence",
icon: BiListCheck
icon: BiListCheck,
color: colors[3]
},
LinksAnalyzer: {
label: "Anomalie de lien web",
icon: BiLink
icon: BiLink,
color: colors[4]
},
};
export const analyzersLabel = Object.fromEntries(Object.entries(analyzers).map(([key, { label }]) => [key, label]));
export const types: {[key: string]: {label: string, color: string}} = {
PlaceOfInterest: {
label: "Lieu",
color: colors[0]
},
EntertainmentAndEvent: {
label: "Fête et manifestation",
color: colors[1]
},
Product: {
label: "Produit",
color: colors[2]
},
Tour: {
label: "Itinéraire touristique",
color: colors[3]
}
};
export const anomalyAnalyzerLabels = Object.fromEntries(Object.entries(anomalyAnalyzers).map(([key, { label }]) => [key, label]));
export const typesByLabel = Object.fromEntries(Object.entries(types).map(([key, { label, ...rest }]) => [label, { key, ...rest }]));
......@@ -2,7 +2,7 @@
"name": "datatourisme/quality-platform-webapp",
"type": "project",
"description": "DATAtourisme - Quality Platform web application",
"version": "0.2.1",
"version": "0.2.2",
"license": "GPL-3.0-or-later",
"authors": [
{
......
......@@ -3884,6 +3884,11 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
hex-rgb@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/hex-rgb/-/hex-rgb-5.0.0.tgz#e2c9eb6a37498d66c5a350a221ed4c2c7d1a92d6"
integrity sha512-NQO+lgVUCtHxZ792FodgW0zflK+ozS9X9dwGp9XvvmPlH7pyxd588cn24TD3rmPm/N0AIRXF10Otah8yKqGw4w==
hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment