Skip to content
This repository has been archived by the owner on Apr 25, 2023. It is now read-only.

Commit

Permalink
feat: Add opacity field to map tiles (#220)
Browse files Browse the repository at this point in the history
* UI of tile map opacity slider, fix WAS bug

* add opacity to visualizer

* small refactor of SliderField

* remove console log

* updates based on PR review
  • Loading branch information
KaWaite committed May 10, 2022
1 parent 3bf4215 commit 006a8d0
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 24 deletions.
1 change: 0 additions & 1 deletion src/components/atoms/Slider/index.tsx
Expand Up @@ -3,7 +3,6 @@ import React, { ComponentProps } from "react";

import { styled, css } from "@reearth/theme";

// Assets
import "rc-slider/assets/index.css";

type Props = {
Expand Down
@@ -0,0 +1,220 @@
import RCSlider from "rc-slider";
import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";

import Flex from "@reearth/components/atoms/Flex";
import theme, { styled, metrics } from "@reearth/theme";
import { metricsSizes } from "@reearth/theme/metrics";

import { FieldProps } from "../types";

export type Props = FieldProps<number> & {
min?: number;
max?: number;
step?: number;
};

const inputRegex = /^\d+\.\d{2,}$/;

const SliderField: React.FC<Props> = ({
value,
linked,
overridden,
disabled,
onChange,
min,
max,
step,
}) => {
const [inputValue, setInputValue] = useState(value);
const [sliderValue, setSliderValue] = useState(value);
const isEditing = useRef(false);
const isDirty = useRef(false);

const calculatedStep = step ? step : max ? max / 10 : 0.1;

const opacityMarkers = useMemo(
() => ({
[min ?? 0]: min ?? 0,
[max ? max / 2 : 0.5]: max ? max / 2 : 0.5,
[max ?? 1]: max ?? 1,
}),
[max, min],
);

useEffect(() => {
isDirty.current = false;
setInputValue(value);
setSliderValue(value);
}, [value]);

const handleChange = useCallback(
(newValue: string) => {
if (!onChange || !isEditing.current || !isDirty.current) return;
if (newValue === "") {
onChange(undefined);
isDirty.current = false;
} else {
const floatValue = parseFloat(newValue);
if (
(typeof max === "number" && isFinite(max) && floatValue > max) ||
(typeof min === "number" && isFinite(min) && floatValue < min)
) {
setInputValue(value);
isDirty.current = false;
return;
}
if (!isNaN(floatValue)) {
onChange(floatValue);
isDirty.current = false;
}
}
},
[onChange, min, max, value],
);

const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const target = e.currentTarget;

if (inputRegex.test(target.value)) return;

setInputValue(target.valueAsNumber);
isDirty.current = isEditing.current;
}, []);

const handleSliderChange = useCallback((value: number) => {
setInputValue(value);
setSliderValue(value);
isDirty.current = isEditing.current;
}, []);

const handleKeyPress = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
handleChange(e.currentTarget.value);
}
},
[handleChange],
);

const handleFocus = useCallback(() => {
isEditing.current = true;
}, []);

const handleBlur = useCallback(
(e: React.SyntheticEvent<HTMLInputElement>) => {
handleChange(e.currentTarget.value);
isEditing.current = false;
},
[handleChange],
);

return (
<Wrapper>
<InputWrapper justify="end">
<StyledInput
type="number"
value={inputValue}
disabled={disabled}
onChange={handleInputChange}
onKeyPress={handleKeyPress}
onFocus={handleFocus}
onBlur={handleBlur}
min={min}
max={max}
step={calculatedStep}
linked={linked}
overridden={overridden}
inactive={!!disabled}
/>
</InputWrapper>
<Flex justify="center">
<div style={{ width: "90%" }}>
<StyledSlider
value={sliderValue}
min={min}
max={max}
step={calculatedStep}
marks={opacityMarkers}
onAfterChange={onChange}
onChange={handleSliderChange}
dotStyle={{ display: "none" }}
trackStyle={{
backgroundColor: theme.properties.focusBorder,
height: 8,
marginTop: 1,
marginLeft: 1,
borderRadius: "15px",
}}
handleStyle={{
border: "none",
height: 16,
width: 16,
marginTop: -3,
backgroundColor: theme.properties.focusBorder,
boxShadow: "0px 4px 4px rgba(0, 0, 0, 0.25)",
}}
railStyle={{
backgroundColor: "inherit",
border: `1px solid ${theme.main.border}`,
height: 10,
borderRadius: "15px",
}}
/>
</div>
</Flex>
</Wrapper>
);
};

const Wrapper = styled.div`
text-align: center;
width: 100%;
`;

const InputWrapper = styled(Flex)`
margin-bottom: 6px;
`;

type InputProps = Pick<Props, "linked" | "overridden"> & { inactive: boolean };

const StyledInput = styled.input<InputProps>`
border: 1px solid ${props => props.theme.properties.border};
background: ${props => props.theme.properties.bg};
width: 25px;
height: ${metrics.propertyTextInputHeight - 7}px;
padding-left: ${metricsSizes.s}px;
padding-right: ${metricsSizes.s}px;
outline: none;
color: ${({ inactive, linked, overridden, theme }) =>
overridden
? theme.main.warning
: linked
? theme.main.link
: inactive
? theme.text.pale
: theme.properties.contentsText};
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
-webkit-appearance: none;
}
&[type="number"] {
-moz-appearance: textfield;
}
&:focus-within {
border-color: ${({ theme }) => theme.properties.contentsText};
}
`;

const StyledSlider = styled(RCSlider)`
.rc-slider-mark-text {
color: ${({ theme }) => theme.properties.text};
}
.rc-slider-mark-text-active {
color: ${({ theme }) => theme.text.pale};
}
`;

export default SliderField;
Expand Up @@ -22,6 +22,7 @@ import NonEditableField from "./NonEditableField";
import NumberField from "./NumberField";
import RadioField from "./RadioField";
import SelectField from "./SelectField";
import SliderField from "./SliderField";
import SwitchField from "./SwitchField";
import TextField from "./TextField";
import { FieldProps } from "./types";
Expand Down Expand Up @@ -53,6 +54,7 @@ export type SchemaField<T extends ValueType = ValueType> = {
| "selection"
| "buttons"
| "range"
| "slider"
| "image"
| "video"
| "file"
Expand Down Expand Up @@ -190,13 +192,16 @@ const PropertyField: React.FC<Props> = ({
) : type === "latlng" ? (
<LocationField {...commonProps} />
) : type === "number" ? (
<NumberField
{...commonProps}
suffix={schema.suffix}
range={schema.ui === "range"}
max={schema.max}
min={schema.min}
/>
schema.ui === "slider" ? (
<SliderField {...commonProps} min={schema.min} max={schema.max} />
) : (
<NumberField
{...commonProps}
suffix={schema.suffix}
max={schema.max}
min={schema.min}
/>
)
) : type === "string" ? (
schema.ui === "color" ? (
<ColorField {...commonProps} />
Expand Down
40 changes: 27 additions & 13 deletions src/components/molecules/Visualizer/Engine/Cesium/hooks.ts
Expand Up @@ -18,6 +18,14 @@ import terrain from "./terrain";
import useEngineRef from "./useEngineRef";
import { convertCartesian3ToPosition } from "./utils";

export type ImageryLayerData = {
id: string;
provider: ImageryProvider;
min?: number;
max?: number;
opacity?: number;
};

const cesiumIonDefaultAccessToken = Ion.defaultAccessToken;

export default ({
Expand Down Expand Up @@ -50,26 +58,32 @@ export default ({
const engineAPI = useEngineRef(ref, cesium);

// imagery layers
const [imageryLayers, setImageryLayers] =
useState<[string, ImageryProvider, number | undefined, number | undefined][]>();
const [imageryLayers, setImageryLayers] = useState<ImageryLayerData[]>();

useDeepCompareEffect(() => {
const newTiles = (property?.tiles?.length ? property.tiles : undefined)
?.map(
t =>
[t.id, t.tile_type || "default", t.tile_url, t.tile_minLevel, t.tile_maxLevel] as const,
[
t.id,
t.tile_type || "default",
t.tile_url,
t.tile_minLevel,
t.tile_maxLevel,
t.tile_opacity,
] as const,
)
.map<[string, ImageryProvider | null, number | undefined, number | undefined]>(
([id, type, url, min, max]) => [
id,
type ? (url ? imagery[type](url) : imagery[type]()) : null,
min,
max,
],
.map(
([id, type, url, min, max, opacity]) =>
<ImageryLayerData>{
id,
provider: type ? (url ? imagery[type](url) : imagery[type]()) : null,
min,
max,
opacity,
},
)
.filter(
(t): t is [string, ImageryProvider, number | undefined, number | undefined] => !!t[1],
);
.filter(t => !!t.provider);
setImageryLayers(newTiles);
}, [property?.tiles ?? [], cesiumIonAccessToken]);

Expand Down
5 changes: 3 additions & 2 deletions src/components/molecules/Visualizer/Engine/Cesium/index.tsx
Expand Up @@ -152,12 +152,13 @@ const Cesium: React.ForwardRefRenderFunction<EngineRef, EngineProps> = (
terrainExaggerationRelativeHeight={terrainProperty.terrainExaggerationRelativeHeight}
terrainExaggeration={terrainProperty.terrainExaggeration}
/>
{imageryLayers?.map(([id, im, min, max]) => (
{imageryLayers?.map(({ id, provider, min, max, opacity }) => (
<ImageryLayer
key={id}
imageryProvider={im}
imageryProvider={provider}
minimumTerrainLevel={min}
maximumTerrainLevel={max}
alpha={opacity}
/>
))}
{ready ? children : null}
Expand Down
1 change: 1 addition & 0 deletions src/components/molecules/Visualizer/Engine/ref.ts
Expand Up @@ -90,6 +90,7 @@ export type SceneProperty = {
tile_url?: string;
tile_maxLevel?: number;
tile_minLevel?: number;
tile_opacity?: number;
}[];
terrain?: {
terrain?: boolean;
Expand Down
Expand Up @@ -53,7 +53,7 @@ export default function Area({
// reverse={area !== "middle" && section === "right"}
end={section === "right" || area === "bottom"}
align={(area === "middle" || section === "center") && widgets?.length ? align : undefined}
style={{ flexWrap: "wrap" }}
style={{ flexWrap: "wrap", pointerEvents: "none" }}
editorStyle={{
flexWrap: "wrap",
background: area === "middle" ? theme.alignSystem.blueBg : theme.alignSystem.orangeBg,
Expand Down
2 changes: 2 additions & 0 deletions src/components/organisms/EarthEditor/PropertyPane/convert.ts
Expand Up @@ -218,6 +218,8 @@ const toUi = (ui: PropertySchemaFieldUi | null | undefined): SchemaField["ui"] =
return "multiline";
case PropertySchemaFieldUi.Range:
return "range";
case PropertySchemaFieldUi.Slider:
return "slider";
case PropertySchemaFieldUi.Selection:
return "selection";
case PropertySchemaFieldUi.Video:
Expand Down
1 change: 1 addition & 0 deletions src/gql/graphql-client-api.tsx
Expand Up @@ -1451,6 +1451,7 @@ export enum PropertySchemaFieldUi {
Multiline = 'MULTILINE',
Range = 'RANGE',
Selection = 'SELECTION',
Slider = 'SLIDER',
Video = 'VIDEO'
}

Expand Down
6 changes: 6 additions & 0 deletions src/gql/graphql.schema.json
Expand Up @@ -11539,6 +11539,12 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "SLIDER",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "VIDEO",
"description": null,
Expand Down

0 comments on commit 006a8d0

Please sign in to comment.