From 2fcab2a1f60d7921f6b79a2ae1d7663b5c4b475a Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 17 Jul 2024 19:32:58 +0200 Subject: [PATCH 1/6] We can now change color and opacity of user drawn points and shapes --- src/actions/dataActions.tsx | 68 ++++---- src/actions/mapActions.tsx | 22 +++ src/components/PlaceSelect.tsx | 65 ++++++-- .../PlaceStyleEditor/PlaceStyleEditor.tsx | 155 ++++++++++++++++++ src/components/PlaceStyleEditor/index.tsx | 27 +++ src/components/UserVectorLayer.tsx | 6 +- src/config.ts | 6 +- src/connected/PlaceSelect.tsx | 9 +- src/model/place.ts | 20 ++- src/reducers/dataReducer.ts | 91 ++++++---- src/resources/lang.json | 5 + src/selectors/controlSelectors.tsx | 2 +- 12 files changed, 396 insertions(+), 80 deletions(-) create mode 100644 src/components/PlaceStyleEditor/PlaceStyleEditor.tsx create mode 100644 src/components/PlaceStyleEditor/index.tsx diff --git a/src/actions/dataActions.tsx b/src/actions/dataActions.tsx index c2fbb825..96d0b43d 100644 --- a/src/actions/dataActions.tsx +++ b/src/actions/dataActions.tsx @@ -32,7 +32,12 @@ import i18n from "@/i18n"; import { ApiServerConfig, ApiServerInfo } from "@/model/apiServer"; import { ColorBar, ColorBars } from "@/model/colorBar"; import { Dataset, getDatasetUserVariables } from "@/model/dataset"; -import { findPlaceInPlaceGroups, Place, PlaceGroup } from "@/model/place"; +import { + findPlaceInPlaceGroups, + Place, + PlaceGroup, + PlaceStyle, +} from "@/model/place"; import { getUserPlacesFromCsv } from "@/model/user-place/csv"; import { getUserPlacesFromGeoJson } from "@/model/user-place/geojson"; import { getUserPlacesFromWkt } from "@/model/user-place/wkt"; @@ -81,7 +86,7 @@ import { } from "./controlActions"; import { VolumeRenderMode } from "@/states/controlState"; import { MessageLogAction, postMessage } from "./messageLogActions"; -import { renameUserPlaceInLayer } from "./mapActions"; +import { renameUserPlaceInLayer, restyleUserPlaceInLayer } from "./mapActions"; import { ColorBarNorm } from "@/model/variable"; import { getStatistics } from "@/api/getStatistics"; import { StatisticsRecord } from "@/model/statistics"; @@ -447,6 +452,36 @@ export function _renameUserPlace( //////////////////////////////////////////////////////////////////////////////// +export const RESTYLE_USER_PLACE = "RESTYLE_USER_PLACE"; + +export interface RestyleUserPlace { + type: typeof RESTYLE_USER_PLACE; + placeGroupId: string; + placeId: string; + placeStyle: PlaceStyle; +} + +export function restyleUserPlace( + placeGroupId: string, + placeId: string, + placeStyle: PlaceStyle, +) { + return (dispatch: Dispatch) => { + dispatch(_restyleUserPlace(placeGroupId, placeId, placeStyle)); + restyleUserPlaceInLayer(placeGroupId, placeId, placeStyle); + }; +} + +export function _restyleUserPlace( + placeGroupId: string, + placeId: string, + placeStyle: PlaceStyle, +): RestyleUserPlace { + return { type: RESTYLE_USER_PLACE, placeGroupId, placeId, placeStyle }; +} + +//////////////////////////////////////////////////////////////////////////////// + export const REMOVE_USER_PLACE = "REMOVE_USER_PLACE"; export interface RemoveUserPlace { @@ -530,34 +565,6 @@ export function addStatistics() { }; } -// const addStatisticsFromMock = () => { -// const i = statisticsRecords.length + 1; -// setStatisticsRecords([ -// ...statisticsRecords, -// { -// source: { -// datasetId: `ds${i}`, -// datasetTitle: `Dataset ${i}`, -// variableName: "CHL", -// placeId: "p029840456", -// geometry: null, -// }, -// minimum: i, -// maximum: i + 1, -// mean: i + 0.5, -// standardDev: 0.1 * i, -// histogram: { -// bins: Array.from({ length: 100 }, (_, index) => ({ -// x1: index, -// xc: index + 0.5, -// x2: index + 1, -// count: 1000 * Math.random(), -// })), -// }, -// }, -// ]); -// }; - export const ADD_STATISTICS = "ADD_STATISTICS"; export interface AddStatistics { @@ -1206,6 +1213,7 @@ export type DataAction = | AddImportedUserPlaces | RenameUserPlaceGroup | RenameUserPlace + | RestyleUserPlace | RemoveUserPlace | RemoveUserPlaceGroup | AddStatistics diff --git a/src/actions/mapActions.tsx b/src/actions/mapActions.tsx index 63f0c934..02cc8181 100644 --- a/src/actions/mapActions.tsx +++ b/src/actions/mapActions.tsx @@ -24,6 +24,8 @@ import { default as OlMap } from "ol/Map"; import { Geometry as OlGeometry } from "ol/geom"; +import { Vector as OlVectorLayer } from "ol/layer"; +import { Vector as OlVectorSource } from "ol/source"; import { fromExtent } from "ol/geom/Polygon"; import { Extent as OlExtent } from "ol/extent"; import { getCenter } from "ol/extent"; @@ -31,6 +33,8 @@ import { default as OlSimpleGeometry } from "ol/geom/SimpleGeometry"; import { GEOGRAPHIC_CRS } from "@/model/proj"; import { MAP_OBJECTS } from "@/states/controlState"; +import { PlaceStyle } from "@/model/place"; +import { setFeatureStyle } from "@/components/ol/style"; // noinspection JSUnusedLocalSymbols export function renameUserPlaceInLayer( @@ -47,6 +51,24 @@ export function renameUserPlaceInLayer( } } +export function restyleUserPlaceInLayer( + placeGroupId: string, + placeId: string, + placeStyle: PlaceStyle, +) { + if (MAP_OBJECTS[placeGroupId]) { + const userLayer = MAP_OBJECTS[ + placeGroupId + ] as OlVectorLayer; + const source = userLayer.getSource(); + const feature = source?.getFeatureById(placeId); + if (feature) { + // console.log("selected feature:", feature, placeStyle); + setFeatureStyle(feature, placeStyle.color, placeStyle.opacity); + } + } +} + export function locateInMap( mapId: string, location: OlGeometry | OlExtent, diff --git a/src/components/PlaceSelect.tsx b/src/components/PlaceSelect.tsx index 140f6140..d7aae5de 100644 --- a/src/components/PlaceSelect.tsx +++ b/src/components/PlaceSelect.tsx @@ -22,21 +22,23 @@ * SOFTWARE. */ -import * as React from "react"; +import { useState, MouseEvent } from "react"; +import { SxProps } from "@mui/system"; import Input from "@mui/material/Input"; import MenuItem from "@mui/material/MenuItem"; import Select, { SelectChangeEvent } from "@mui/material/Select"; -import { SxProps } from "@mui/system"; import EditIcon from "@mui/icons-material/Edit"; +import FormatColorFillIcon from "@mui/icons-material/FormatColorFill"; import RemoveCircleOutlineIcon from "@mui/icons-material/RemoveCircleOutline"; import TravelExploreIcon from "@mui/icons-material/TravelExplore"; import i18n from "@/i18n"; import { Dataset } from "@/model/dataset"; -import { Place, USER_ID_PREFIX } from "@/model/place"; +import { Place, PlaceInfo, PlaceStyle, USER_ID_PREFIX } from "@/model/place"; import { WithLocale } from "@/util/lang"; import EditableSelect from "./EditableSelect"; import ToolButton from "./ToolButton"; +import PlaceStyleEditor from "@/components/PlaceStyleEditor"; // noinspection JSUnusedLocalSymbols const styles: Record = { @@ -49,6 +51,7 @@ interface PlaceSelectProps extends WithLocale { datasets: Dataset[]; selectedPlaceGroupIds: string[] | null; selectedPlaceId: string | null; + selectedPlaceInfo: PlaceInfo | null; places: Place[]; placeLabels: string[]; selectPlace: ( @@ -61,6 +64,11 @@ interface PlaceSelectProps extends WithLocale { placeId: string, placeName: string, ) => void; + restyleUserPlace: ( + placeGroupId: string, + placeId: string, + placeStyle: PlaceStyle, + ) => void; removeUserPlace: ( placeGroupId: string, placeId: string, @@ -75,18 +83,23 @@ export default function PlaceSelect({ placeLabels, selectedPlaceId, selectedPlaceGroupIds, + selectedPlaceInfo, renameUserPlace, + restyleUserPlace, removeUserPlace, places, locateSelectedPlace, }: PlaceSelectProps) { - const [editMode, setEditMode] = React.useState(false); + const [editMode, setEditMode] = useState(false); + const [styleAnchorEl, setStyleAnchorEl] = useState(null); places = places || []; placeLabels = placeLabels || []; selectedPlaceId = selectedPlaceId || ""; selectedPlaceGroupIds = selectedPlaceGroupIds || []; + console.log("selectedPlaceInfo:", selectedPlaceInfo); + const selectedPlaceGroupId = selectedPlaceGroupIds!.length === 1 ? selectedPlaceGroupIds![0] : null; @@ -97,6 +110,10 @@ export default function PlaceSelect({ renameUserPlace(selectedPlaceGroupId!, selectedPlaceId!, placeName); }; + const updatePlaceStyle = (placeStyle: PlaceStyle) => { + restyleUserPlace(selectedPlaceGroupId!, selectedPlaceId!, placeStyle); + }; + const handlePlaceChange = (event: SelectChangeEvent) => { selectPlace(event.target.value || null, places, true); }; @@ -135,6 +152,10 @@ export default function PlaceSelect({ setEditMode(true); }; + const handleStyleButtonClick = (event: MouseEvent) => { + setStyleAnchorEl(event.currentTarget); + }; + const handleRemoveButtonClick = () => { removeUserPlace(selectedPlaceGroupId!, selectedPlaceId!, places); }; @@ -146,6 +167,12 @@ export default function PlaceSelect({ tooltipText={i18n.get("Rename place")} icon={} />, + } + />, v.trim().length > 0} - editMode={editMode} - setEditMode={setEditMode} - labelText={i18n.get("Place")} - select={select} - actions={actions} - /> + <> + v.trim().length > 0} + editMode={editMode} + setEditMode={setEditMode} + labelText={i18n.get("Place")} + select={select} + actions={actions} + /> + {selectedPlaceInfo && ( + + )} + ); } diff --git a/src/components/PlaceStyleEditor/PlaceStyleEditor.tsx b/src/components/PlaceStyleEditor/PlaceStyleEditor.tsx new file mode 100644 index 00000000..ad2bb5b5 --- /dev/null +++ b/src/components/PlaceStyleEditor/PlaceStyleEditor.tsx @@ -0,0 +1,155 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019-2024 by the xcube development team and contributors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { WithLocale } from "@/util/lang"; +import { makeStyles } from "@/util/styles"; +import Popover from "@mui/material/Popover"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import i18n from "@/i18n"; +import Slider from "@mui/material/Slider"; +import { PlaceStyle } from "@/model/place"; +import { MouseEvent, useState } from "react"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import { userPlaceColorsArray } from "@/config"; +import Tooltip from "@mui/material/Tooltip"; + +const styles = makeStyles({ + container: { + display: "grid", + gridTemplateColumns: "auto 120px", + gridTemplateRows: "auto", + gridTemplateAreas: "'colorLabel colorValue' 'opacityLabel opacityValue'", + rowGap: 1, + columnGap: 2.5, + padding: 1, + }, + colorLabel: { + gridArea: "colorLabel", + alignSelf: "center", + }, + colorValue: { + gridArea: "colorValue", + alignSelf: "center", + width: "100%", + height: "22px", + borderWidth: 1, + borderStyle: "solid", + borderColor: "black", + }, + opacityLabel: { + gridArea: "opacityLabel", + alignSelf: "center", + }, + opacityValue: { + gridArea: "opacityValue", + alignSelf: "center", + width: "100%", + }, + colorMenuItem: { padding: "4px 8px 4px 8px" }, + colorMenuItemBox: { width: "104px", height: "18px" }, +}); + +interface FeatureStyleEditorProps extends WithLocale { + anchorEl: HTMLElement | null; + setAnchorEl: (anchorEl: HTMLElement | null) => void; + placeStyle: PlaceStyle; + updatePlaceStyle: (placeStyle: PlaceStyle) => void; +} + +const PlaceStyleEditor = ({ + anchorEl, + setAnchorEl, + placeStyle, + updatePlaceStyle, +}: FeatureStyleEditorProps) => { + const [menuAnchorEl, setMenuAnchorEl] = useState(null); + + function handleColorClick(event: MouseEvent) { + setMenuAnchorEl(event.currentTarget); + } + + return ( + <> + setAnchorEl(null)} + anchorOrigin={{ vertical: "bottom", horizontal: "left" }} + transformOrigin={{ vertical: "top", horizontal: "left" }} + > + + {i18n.get("Color")} + + {i18n.get("Opacity")} + + + + updatePlaceStyle({ ...placeStyle, opacity: value as number }) + } + /> + + + setMenuAnchorEl(null)} + > + {userPlaceColorsArray.map(([colorName, _]) => ( + + updatePlaceStyle({ ...placeStyle, color: colorName }) + } + > + + + + + ))} + + + ); +}; + +export default PlaceStyleEditor; diff --git a/src/components/PlaceStyleEditor/index.tsx b/src/components/PlaceStyleEditor/index.tsx new file mode 100644 index 00000000..61b17aaa --- /dev/null +++ b/src/components/PlaceStyleEditor/index.tsx @@ -0,0 +1,27 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019-2024 by the xcube development team and contributors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import PlaceStyleEditor from "./PlaceStyleEditor"; + +export default PlaceStyleEditor; diff --git a/src/components/UserVectorLayer.tsx b/src/components/UserVectorLayer.tsx index e09a52cf..85c6f8c2 100644 --- a/src/components/UserVectorLayer.tsx +++ b/src/components/UserVectorLayer.tsx @@ -30,6 +30,7 @@ import { Config } from "@/config"; import { PlaceGroup, USER_DRAWN_PLACE_GROUP_ID } from "@/model/place"; import { Vector } from "./ol/layer/Vector"; import { setFeatureStyle } from "./ol/style"; +import { isNumber } from "@/util/types"; interface UserVectorLayerProps { placeGroup: PlaceGroup; @@ -69,13 +70,16 @@ const UserVectorLayer: React.FC = ({ feature.setId(place.id); } const color = (place.properties || {}).color || "red"; + const opacity = (place.properties || {}).opacity; const pointSymbol = (place.properties || {}).source ? "diamond" : "circle"; setFeatureStyle( feature, color, - Config.instance.branding.polygonFillOpacity, + isNumber(opacity) + ? opacity + : Config.instance.branding.polygonFillOpacity, pointSymbol, ); source.addFeature(feature); diff --git a/src/config.ts b/src/config.ts index 16e36302..30056741 100644 --- a/src/config.ts +++ b/src/config.ts @@ -24,6 +24,7 @@ import { Color, PaletteMode } from "@mui/material"; import { + blue, brown, cyan, green, @@ -34,6 +35,7 @@ import { pink, purple, red, + teal, yellow, } from "@mui/material/colors"; @@ -244,9 +246,10 @@ interface TileAccess { } // Array of user place colors in stable order (see #153) -const userPlaceColorsArray: [string, Color][] = [ +export const userPlaceColorsArray: [string, Color][] = [ ["red", red], ["yellow", yellow], + ["blue", blue], ["pink", pink], ["lightBlue", lightBlue], ["green", green], @@ -256,6 +259,7 @@ const userPlaceColorsArray: [string, Color][] = [ ["indigo", indigo], ["cyan", cyan], ["brown", brown], + ["teal", teal], ]; const userPlaceColors: { [name: string]: Color } = (() => { diff --git a/src/connected/PlaceSelect.tsx b/src/connected/PlaceSelect.tsx index 51e6ff4a..aecc397b 100644 --- a/src/connected/PlaceSelect.tsx +++ b/src/connected/PlaceSelect.tsx @@ -26,7 +26,11 @@ import { connect } from "react-redux"; import _PlaceSelect from "@/components/PlaceSelect"; import { AppState } from "@/states/appState"; -import { renameUserPlace, removeUserPlace } from "@/actions/dataActions"; +import { + renameUserPlace, + removeUserPlace, + restyleUserPlace, +} from "@/actions/dataActions"; import { selectPlace, locateSelectedPlaceInMap, @@ -35,6 +39,7 @@ import { import { selectedPlaceGroupPlacesSelector, selectedPlaceGroupPlaceLabelsSelector, + selectedPlaceInfoSelector, } from "@/selectors/controlSelectors"; const mapStateToProps = (state: AppState) => { @@ -43,6 +48,7 @@ const mapStateToProps = (state: AppState) => { datasets: state.dataState.datasets, selectedPlaceGroupIds: state.controlState.selectedPlaceGroupIds, selectedPlaceId: state.controlState.selectedPlaceId, + selectedPlaceInfo: selectedPlaceInfoSelector(state), places: selectedPlaceGroupPlacesSelector(state), placeLabels: selectedPlaceGroupPlaceLabelsSelector(state), }; @@ -51,6 +57,7 @@ const mapStateToProps = (state: AppState) => { const mapDispatchToProps = { selectPlace, renameUserPlace, + restyleUserPlace, removeUserPlace, locateSelectedPlace: locateSelectedPlaceInMap, openDialog, diff --git a/src/model/place.ts b/src/model/place.ts index 159ddcaa..7d11ab06 100644 --- a/src/model/place.ts +++ b/src/model/place.ts @@ -55,14 +55,21 @@ export interface PlaceGroup extends GeoJSON.FeatureCollection { placeGroups?: { [placeId: string]: PlaceGroup }; // placeGroups in placeGroups are not yet supported } +/** + * Style part of PlaceInfo + */ +export interface PlaceStyle { + color: string; + opacity: number; +} + /** * Computed place information. */ -export interface PlaceInfo { +export interface PlaceInfo extends PlaceStyle { placeGroup: PlaceGroup; place: Place; label: string; - color: string; image: string | null; description: string | null; } @@ -101,6 +108,7 @@ export const DEFAULT_DESCRIPTION_PROPERTY_NAMES = mkCases([ "comment", ]); export const DEFAULT_COLOR_PROPERTY_NAMES = mkCases(["color"]); +export const DEFAULT_OPACITY_PROPERTY_NAMES = mkCases(["opacity"]); export const DEFAULT_IMAGE_PROPERTY_NAMES = mkCases([ "image", "img", @@ -129,6 +137,14 @@ export function computePlaceInfo( getUserPlaceColorName(getPlaceHash(place)), DEFAULT_COLOR_PROPERTY_NAMES, ); + updatePlaceInfo( + infoObj, + placeGroup, + place, + "opacity", + 0.2, + DEFAULT_OPACITY_PROPERTY_NAMES, + ); updatePlaceInfo( infoObj, placeGroup, diff --git a/src/reducers/dataReducer.ts b/src/reducers/dataReducer.ts index 42d96380..238bc85c 100644 --- a/src/reducers/dataReducer.ts +++ b/src/reducers/dataReducer.ts @@ -48,12 +48,13 @@ import { ADD_STATISTICS, UPDATE_DATASET_USER_VARIABLES, UPDATE_EXPRESSION_CAPABILITIES, + RESTYLE_USER_PLACE, } from "@/actions/dataActions"; import { SELECT_TIME_RANGE, SelectTimeRange } from "@/actions/controlActions"; import i18n from "@/i18n"; import { newId } from "@/util/id"; import { Variable } from "@/model/variable"; -import { Place, USER_DRAWN_PLACE_GROUP_ID } from "@/model/place"; +import { Place, PlaceGroup, USER_DRAWN_PLACE_GROUP_ID } from "@/model/place"; import { TimeSeries, TimeSeriesGroup } from "@/model/timeSeries"; import { getDatasetUserVariables } from "@/model/dataset"; @@ -211,35 +212,30 @@ export function dataReducer( case RENAME_USER_PLACE: { const { placeGroupId, placeId, newName } = action; const userPlaceGroups = state.userPlaceGroups; - const pgIndex = userPlaceGroups.findIndex((pg) => pg.id === placeGroupId); - if (pgIndex >= 0) { - const pg = userPlaceGroups[pgIndex]; - const features = pg.features; - const pIndex = features.findIndex((p) => p.id === placeId); - if (pIndex >= 0) { - const p = features[pIndex]; - return { - ...state, - userPlaceGroups: [ - ...userPlaceGroups.slice(0, pgIndex), - { - ...pg, - features: [ - ...features.slice(0, pIndex), - { - ...p, - properties: { - ...p.properties, - label: newName, - }, - }, - ...features.slice(pIndex + 1), - ], - }, - ...userPlaceGroups.slice(pgIndex + 1), - ], - }; - } + const newUserPlaceGroups = updateUserPlaceGroup( + userPlaceGroups, + placeGroupId, + placeId, + { + label: newName, + }, + ); + if (newUserPlaceGroups) { + return { ...state, userPlaceGroups: newUserPlaceGroups }; + } + return state; + } + case RESTYLE_USER_PLACE: { + const { placeGroupId, placeId, placeStyle } = action; + const userPlaceGroups = state.userPlaceGroups; + const newUserPlaceGroups = updateUserPlaceGroup( + userPlaceGroups, + placeGroupId, + placeId, + placeStyle, + ); + if (newUserPlaceGroups) { + return { ...state, userPlaceGroups: newUserPlaceGroups }; } return state; } @@ -573,3 +569,38 @@ function getTimeSeriesArray( }); return timeSeriesArray; } + +function updateUserPlaceGroup( + userPlaceGroups: PlaceGroup[], + placeGroupId: string, + placeId: string, + placeProperties: Record, +) { + const pgIndex = userPlaceGroups.findIndex((pg) => pg.id === placeGroupId); + if (pgIndex >= 0) { + const pg = userPlaceGroups[pgIndex]; + const features = pg.features; + const pIndex = features.findIndex((p) => p.id === placeId); + if (pIndex >= 0) { + const p = features[pIndex]; + return [ + ...userPlaceGroups.slice(0, pgIndex), + { + ...pg, + features: [ + ...features.slice(0, pIndex), + { + ...p, + properties: { + ...p.properties, + ...placeProperties, + }, + }, + ...features.slice(pIndex + 1), + ], + }, + ...userPlaceGroups.slice(pgIndex + 1), + ]; + } + } +} diff --git a/src/resources/lang.json b/src/resources/lang.json index d9d55919..692e40c0 100644 --- a/src/resources/lang.json +++ b/src/resources/lang.json @@ -280,6 +280,11 @@ "de": "Umkehren", "se": "Omvänt" }, + { + "en": "Color", + "de": "Farbe", + "se": "Färg" + }, { "en": "Opacity", "de": "Opazität", diff --git a/src/selectors/controlSelectors.tsx b/src/selectors/controlSelectors.tsx index 18bb1117..0dbe39d7 100644 --- a/src/selectors/controlSelectors.tsx +++ b/src/selectors/controlSelectors.tsx @@ -1089,7 +1089,7 @@ export function getTileUrl( } export function getDefaultFillOpacity() { - return Config.instance.branding.polygonFillOpacity || 0.25; + return Config.instance.branding.polygonFillOpacity || 0.2; } export function getDefaultStyleImage() { From e84a57e5c53cfc0c4cd965dfca6ae6955fb5a021 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 18 Jul 2024 15:11:42 +0200 Subject: [PATCH 2/6] improved vector selection style --- src/components/UserVectorLayer.tsx | 9 ++---- src/components/Viewer.tsx | 47 ++++++++++++++++++------------ src/components/ol/style.ts | 2 +- src/config.ts | 8 +++++ src/selectors/controlSelectors.tsx | 4 +-- 5 files changed, 43 insertions(+), 27 deletions(-) diff --git a/src/components/UserVectorLayer.tsx b/src/components/UserVectorLayer.tsx index 85c6f8c2..80dc6d33 100644 --- a/src/components/UserVectorLayer.tsx +++ b/src/components/UserVectorLayer.tsx @@ -26,11 +26,10 @@ import * as React from "react"; import { default as OlVectorSource } from "ol/source/Vector"; import { default as OlGeoJSONFormat } from "ol/format/GeoJSON"; -import { Config } from "@/config"; +import { getUserPlaceFillOpacity } from "@/config"; import { PlaceGroup, USER_DRAWN_PLACE_GROUP_ID } from "@/model/place"; import { Vector } from "./ol/layer/Vector"; import { setFeatureStyle } from "./ol/style"; -import { isNumber } from "@/util/types"; interface UserVectorLayerProps { placeGroup: PlaceGroup; @@ -77,9 +76,7 @@ const UserVectorLayer: React.FC = ({ setFeatureStyle( feature, color, - isNumber(opacity) - ? opacity - : Config.instance.branding.polygonFillOpacity, + getUserPlaceFillOpacity(opacity), pointSymbol, ); source.addFeature(feature); @@ -92,7 +89,7 @@ const UserVectorLayer: React.FC = ({ id={placeGroup.id} opacity={placeGroup.id === USER_DRAWN_PLACE_GROUP_ID ? 1 : 0.8} visible={visible} - zIndex={500} + zIndex={501} source={sourceRef.current} /> ); diff --git a/src/components/Viewer.tsx b/src/components/Viewer.tsx index c4e9efd4..5f33a62c 100644 --- a/src/components/Viewer.tsx +++ b/src/components/Viewer.tsx @@ -42,7 +42,11 @@ import { default as OlStyle } from "ol/style/Style"; import { getRenderPixel } from "ol/render"; import i18n from "@/i18n"; -import { Config, getUserPlaceColor, getUserPlaceColorName } from "@/config"; +import { + getUserPlaceColor, + getUserPlaceColorName, + getUserPlaceFillOpacity, +} from "@/config"; import { Place, PlaceGroup, @@ -78,19 +82,30 @@ const COLOR_LEGEND_STYLE: React.CSSProperties = { top: 10, }; +const SELECTION_COLOR = [255, 220, 0, 0.8]; + const SELECTION_LAYER_STROKE = new OlStrokeStyle({ - color: [255, 200, 0, 1.0], - width: 3, + color: SELECTION_COLOR, + width: 10, + lineCap: "square", + lineDash: [10, 15], }); + const SELECTION_LAYER_FILL = new OlFillStyle({ - color: [255, 200, 0, 0.05], + color: [0, 0, 0, 0], }); + const SELECTION_LAYER_STYLE = new OlStyle({ stroke: SELECTION_LAYER_STROKE, fill: SELECTION_LAYER_FILL, image: new OlCircleStyle({ - radius: 10, - stroke: SELECTION_LAYER_STROKE, + radius: 15, + stroke: new OlStrokeStyle({ + color: SELECTION_COLOR, + width: 6, + lineCap: "square", + lineDash: [6, 6], + }), fill: SELECTION_LAYER_FILL, }), }); @@ -320,11 +335,7 @@ export default function Viewer({ const label = findNextLabel(userPlaceGroups, mapInteraction); const color = getUserPlaceColorName(colorIndex); const shadedColor = getUserPlaceColor(color, theme.palette.mode); - setFeatureStyle( - feature, - shadedColor, - Config.instance.branding.polygonFillOpacity, - ); + setFeatureStyle(feature, shadedColor, getUserPlaceFillOpacity()); addDrawnUserPlace( userDrawnPlaceGroupName, @@ -386,6 +397,13 @@ export default function Viewer({ {variableLayer} {overlayLayer} {datasetBoundaryLayer} + { <> {userPlaceGroups.map((placeGroup) => ( @@ -400,13 +418,6 @@ export default function Viewer({ ))} } - {placeGroupLayers} {/*