diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx
index 755509308..bcccceb67 100644
--- a/src/client/app/components/BarChartComponent.tsx
+++ b/src/client/app/components/BarChartComponent.tsx
@@ -8,7 +8,7 @@ import { PlotRelayoutEvent } from 'plotly.js';
import * as React from 'react';
import Plot from 'react-plotly.js';
import { TimeInterval } from '../../../common/TimeInterval';
-import { updateSliderRange } from '../redux/actions/extraActions';
+import { updateSliderRange } from '../redux/slices/graphSlice';
import { readingsApi, stableEmptyBarReadings } from '../redux/api/readingsApi';
import { useAppDispatch, useAppSelector } from '../redux/reduxHooks';
import { selectPlotlyBarDataFromResult, selectPlotlyBarDeps } from '../redux/selectors/barChartSelectors';
@@ -115,14 +115,15 @@ export default function BarChartComponent() {
const startTS = utc(e['xaxis.range[0]']);
const endTS = utc(e['xaxis.range[1]']);
const workingTimeInterval = new TimeInterval(startTS, endTS);
- dispatch(updateSliderRange(workingTimeInterval));
+ dispatch(updateSliderRange(workingTimeInterval.toString()));
}
else if (e['xaxis.range']) {
// this case is when the slider knobs are dragged.
const range = e['xaxis.range']!;
const startTS = range && range[0];
const endTS = range && range[1];
- dispatch(updateSliderRange(new TimeInterval(utc(startTS), utc(endTS))));
+ const interval = new TimeInterval(utc(startTS), utc(endTS)).toString();
+ dispatch(updateSliderRange(interval));
}
}, 500, { leading: false, trailing: true })}
diff --git a/src/client/app/components/ChartSelectComponent.tsx b/src/client/app/components/ChartSelectComponent.tsx
index 9625734ae..296d37048 100644
--- a/src/client/app/components/ChartSelectComponent.tsx
+++ b/src/client/app/components/ChartSelectComponent.tsx
@@ -2,17 +2,13 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-import { sortBy, values } from 'lodash';
import * as React from 'react';
import { useState } from 'react';
import { FormattedMessage } from 'react-intl';
-import { useSelector } from 'react-redux';
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
import { useAppDispatch, useAppSelector } from '../redux/reduxHooks';
import { graphSlice, selectChartToRender } from '../redux/slices/graphSlice';
-import { SelectOption } from '../types/items';
import { ChartTypes } from '../types/redux/graph';
-import { State } from '../types/redux/state';
import translate from '../utils/translate';
import TooltipMarkerComponent from './TooltipMarkerComponent';
@@ -24,10 +20,10 @@ export default function ChartSelectComponent() {
const currentChartToRender = useAppSelector(selectChartToRender);
const dispatch = useAppDispatch();
const [expand, setExpand] = useState(false);
- const mapsById = useSelector((state: State) => state.maps.byMapID);
- const sortedMaps = sortBy(values(mapsById).map(map => (
- { value: map.id, label: map.name, isDisabled: !(map.origin && map.opposite) } as SelectOption
- )), 'label');
+ // const mapsById = useAppSelector(selectMapDataById);
+ // const sortedMaps = sortBy(values(mapsById).map(map => (
+ // { value: map.id, label: map.name, isDisabled: !(map.origin && map.opposite) } as SelectOption
+ // )), 'label');
return (
<>
@@ -52,11 +48,6 @@ export default function ChartSelectComponent() {
key={chartType}
onClick={() => {
dispatch(graphSlice.actions.changeChartToRender(chartType));
- if (chartType === ChartTypes.map && Object.keys(sortedMaps).length === 1) {
- // If there is only one map, selectedMap is the id of the only map. ie; display map automatically if only 1 map
- dispatch({ type: 'UPDATE_SELECTED_MAPS', mapID: sortedMaps[0].value });
-
- }
}}
>
{translate(`${chartType}`)}
diff --git a/src/client/app/components/DateRangeComponent.tsx b/src/client/app/components/DateRangeComponent.tsx
index 22f7846ed..ceffc9e14 100644
--- a/src/client/app/components/DateRangeComponent.tsx
+++ b/src/client/app/components/DateRangeComponent.tsx
@@ -9,9 +9,8 @@ import * as React from 'react';
import 'react-calendar/dist/Calendar.css';
import { useAppDispatch, useAppSelector } from '../redux/reduxHooks';
import { selectSelectedLanguage } from '../redux/slices/appStateSlice';
-import { changeSliderRange, selectQueryTimeInterval, updateTimeInterval, selectChartToRender} from '../redux/slices/graphSlice';
+import { changeSliderRange, selectQueryTimeInterval, updateTimeInterval, selectChartToRender } from '../redux/slices/graphSlice';
import '../styles/DateRangeCustom.css';
-import { Dispatch } from '../types/redux/actions';
import { dateRangeToTimeInterval, timeIntervalToDateRange } from '../utils/dateRangeCompatibility';
import translate from '../utils/translate';
import TooltipMarkerComponent from './TooltipMarkerComponent';
@@ -22,15 +21,15 @@ import { ChartTypes } from '../types/redux/graph';
* @returns Date Range Calendar Picker
*/
export default function DateRangeComponent() {
- const dispatch: Dispatch = useAppDispatch();
+ const dispatch = useAppDispatch();
const queryTimeInterval = useAppSelector(selectQueryTimeInterval);
const locale = useAppSelector(selectSelectedLanguage);
const chartType = useAppSelector(selectChartToRender);
- const datePickerVisible = chartType !== ChartTypes.compare;
+ const datePickerVisible = chartType !== ChartTypes.compare;
const handleChange = (value: Value) => {
- dispatch(updateTimeInterval(dateRangeToTimeInterval(value)));
- dispatch(changeSliderRange(dateRangeToTimeInterval(value)));
+ dispatch(updateTimeInterval(dateRangeToTimeInterval(value).toString()));
+ dispatch(changeSliderRange(dateRangeToTimeInterval(value).toString()));
};
diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx
index a48b18f56..60a46cdff 100644
--- a/src/client/app/components/HeaderButtonsComponent.tsx
+++ b/src/client/app/components/HeaderButtonsComponent.tsx
@@ -8,7 +8,6 @@ import { FormattedMessage } from 'react-intl';
import { Link, useLocation } from 'react-router-dom';
import { DropdownItem, DropdownMenu, DropdownToggle, Nav, NavLink, Navbar, UncontrolledDropdown } from 'reactstrap';
import TooltipHelpComponent from '../components/TooltipHelpComponent';
-import { clearGraphHistory } from '../redux/actions/extraActions';
import { authApi } from '../redux/api/authApi';
import { selectOEDVersion } from '../redux/api/versionApi';
import { useAppDispatch, useAppSelector } from '../redux/reduxHooks';
@@ -19,6 +18,7 @@ import { UserRole } from '../types/items';
import translate from '../utils/translate';
import LanguageSelectorComponent from './LanguageSelectorComponent';
import TooltipMarkerComponent from './TooltipMarkerComponent';
+import { clearGraphHistory } from '../redux/slices/graphSlice';
/**
* React Component that defines the header buttons at the top of a page
diff --git a/src/client/app/components/HistoryComponent.tsx b/src/client/app/components/HistoryComponent.tsx
index 3af439a63..7534ca6f2 100644
--- a/src/client/app/components/HistoryComponent.tsx
+++ b/src/client/app/components/HistoryComponent.tsx
@@ -5,7 +5,7 @@
import * as React from 'react';
import { useAppDispatch, useAppSelector } from '../redux/reduxHooks';
import { selectForwardHistory, selectPrevHistory } from '../redux/slices/graphSlice';
-import { historyStepBack, historyStepForward } from '../redux/actions/extraActions';
+import { historyStepBack, historyStepForward } from '../redux/slices/graphSlice';
import TooltipMarkerComponent from './TooltipMarkerComponent';
/**
diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx
index f71058629..2c909609e 100644
--- a/src/client/app/components/LineChartComponent.tsx
+++ b/src/client/app/components/LineChartComponent.tsx
@@ -8,7 +8,7 @@ import { PlotRelayoutEvent } from 'plotly.js';
import * as React from 'react';
import Plot from 'react-plotly.js';
import { TimeInterval } from '../../../common/TimeInterval';
-import { updateSliderRange } from '../redux/actions/extraActions';
+import { updateSliderRange } from '../redux/slices/graphSlice';
import { readingsApi, stableEmptyLineReadings } from '../redux/api/readingsApi';
import { useAppDispatch, useAppSelector } from '../redux/reduxHooks';
import { selectLineChartQueryArgs } from '../redux/selectors/chartQuerySelectors';
@@ -101,14 +101,15 @@ export default function LineChartComponent() {
const startTS = utc(e['xaxis.range[0]']);
const endTS = utc(e['xaxis.range[1]']);
const workingTimeInterval = new TimeInterval(startTS, endTS);
- dispatch(updateSliderRange(workingTimeInterval));
+ dispatch(updateSliderRange(workingTimeInterval.toString()));
}
else if (e['xaxis.range']) {
// this case is when the slider knobs are dragged.
const range = e['xaxis.range']!;
const startTS = range && range[0];
const endTS = range && range[1];
- dispatch(updateSliderRange(new TimeInterval(utc(startTS), utc(endTS))));
+ const interval = new TimeInterval(utc(startTS), utc(endTS));
+ dispatch(updateSliderRange(interval.toString()));
}
}, 500, { leading: false, trailing: true })
diff --git a/src/client/app/components/MapChartComponent.tsx b/src/client/app/components/MapChartComponent.tsx
index e1a5dde28..8d23dd1bd 100644
--- a/src/client/app/components/MapChartComponent.tsx
+++ b/src/client/app/components/MapChartComponent.tsx
@@ -5,11 +5,10 @@
import { orderBy } from 'lodash';
import * as moment from 'moment';
import * as React from 'react';
-import Plot from 'react-plotly.js';
-import { useSelector } from 'react-redux';
import {
selectAreaUnit, selectBarWidthDays,
selectGraphAreaNormalization, selectSelectedGroups,
+ selectSelectedMap,
selectSelectedMeters, selectSelectedUnit
} from '../redux/slices/graphSlice';
import { selectGroupDataById } from '../redux/api/groupsApi';
@@ -19,7 +18,6 @@ import { selectUnitDataById } from '../redux/api/unitsApi';
import { useAppSelector } from '../redux/reduxHooks';
import { selectMapChartQueryArgs } from '../redux/selectors/chartQuerySelectors';
import { DataType } from '../types/Datasources';
-import { State } from '../types/redux/state';
import { UnitRepresentType } from '../types/redux/units';
import {
CartesianPoint,
@@ -34,6 +32,8 @@ import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConvers
import getGraphColor from '../utils/getGraphColor';
import translate from '../utils/translate';
import SpinnerComponent from './SpinnerComponent';
+import { selectMapById } from '../redux/api/mapsApi';
+import Plot from 'react-plotly.js';
/**
* @returns map component
@@ -57,285 +57,271 @@ export default function MapChartComponent() {
// RTK Types Disagree with maps ts types so, use old until migration completer for maps.
// This is also an issue when trying to refactor maps reducer into slice.
- const selectedMap = useSelector((state: State) => state.maps.selectedMap);
- const byMapID = useSelector((state: State) => state.maps.byMapID);
- const editedMaps = useSelector((state: State) => state.maps.editedMaps);
+ const selectedMap = useAppSelector(selectSelectedMap);
+ const map = useAppSelector(state => selectMapById(state, selectedMap));
+
if (meterIsFetching || groupIsFetching) {
return ;
}
-
- // Map to use.
- let map;
// Holds Plotly mapping info.
const data = [];
- // Holds the image to use.
- let image;
- if (selectedMap !== 0) {
- const mapID = selectedMap;
- if (byMapID[mapID]) {
- map = byMapID[mapID];
- if (editedMaps[mapID]) {
- map = editedMaps[mapID];
+
+ // Holds the hover text for each point for Plotly
+ const hoverText: string[] = [];
+ // Holds the size of each circle for Plotly.
+ const size: number[] = [];
+ // Holds the color of each circle for Plotly.
+ const colors: string[] = [];
+ // If there is no map then use a new, empty image as the map. I believe this avoids errors
+ // and gives the blank screen.
+ // Arrays to hold the Plotly grid location (x, y) for circles to place on map.
+ const x: number[] = [];
+ const y: number[] = [];
+
+ // const timeInterval = state.graph.queryTimeInterval;
+ // const barDuration = state.graph.barDuration
+ // Make sure there is a map with values so avoid issues.
+ if (map && map.origin && map.opposite) {
+ // The size of the original map loaded into OED.
+ const imageDimensions: Dimensions = {
+ width: map.imgWidth,
+ height: map.imgHeight
+ };
+ // Determine the dimensions so within the Plotly coordinates on the user map.
+ const imageDimensionNormalized = normalizeImageDimensions(imageDimensions);
+ // This is the origin & opposite from the calibration. It is the lower, left
+ // and upper, right corners of the user map.
+ const origin = map.origin;
+ const opposite = map.opposite;
+ // Get the GPS degrees per unit of Plotly grid for x and y. By knowing the two corners
+ // (or really any two distinct points) you can calculate this by the change in GPS over the
+ // change in x or y which is the map's width & height in this case.
+ const scaleOfMap = calculateScaleFromEndpoints(origin, opposite, imageDimensionNormalized, map.northAngle);
+ // Loop over all selected meters. Maps only work for meters at this time.
+ // The y-axis label depends on the unit which is in selectUnit state.
+ let unitLabel: string = '';
+ // If graphingUnit is -99 then none selected and nothing to graph so label is empty.
+ // This will probably happen when the page is first loaded.
+ if (unitID !== -99) {
+ const selectUnitState = unitDataById[unitID];
+ if (selectUnitState !== undefined) {
+ // Quantity and flow units have different unit labels.
+ // Look up the type of unit if it is for quantity/flow (should not be raw) and decide what to do.
+ // Bar graphics are always quantities.
+ if (selectUnitState.unitRepresent === UnitRepresentType.quantity) {
+ // If it is a quantity unit then that is the unit you are graphing but it is normalized to per day.
+ unitLabel = selectUnitState.identifier + ' / day';
+ } else if (selectUnitState.unitRepresent === UnitRepresentType.flow) {
+ // If it is a flow meter then you need to multiply by time to get the quantity unit then show as per day.
+ // The quantity/time for flow has varying time so label by multiplying by time.
+ // To make sure it is clear, also indicate it is a quantity.
+ // Note this should not be used for raw data.
+ // It might not be usual to take a flow and make it into a quantity so this label is a little different to
+ // catch people's attention. If sites/users don't like OED doing this then we can eliminate flow for these types
+ // of graphics as we are doing for rate.
+ unitLabel = selectUnitState.identifier + ' * time / day ≡ quantity / day';
+ }
+ if (areaNormalization) {
+ unitLabel += ' / ' + translate(`AreaUnitType.${selectedAreaUnit}`);
+ }
}
}
- // Holds the hover text for each point for Plotly
- const hoverText: string[] = [];
- // Holds the size of each circle for Plotly.
- const size: number[] = [];
- // Holds the color of each circle for Plotly.
- const colors: string[] = [];
- // If there is no map then use a new, empty image as the map. I believe this avoids errors
- // and gives the blank screen.
- image = (map) ? map.image : new Image();
- // Arrays to hold the Plotly grid location (x, y) for circles to place on map.
- const x: number[] = [];
- const y: number[] = [];
- // const timeInterval = state.graph.queryTimeInterval;
- // const barDuration = state.graph.barDuration
- // Make sure there is a map with values so avoid issues.
- if (map && map.origin && map.opposite) {
- // The size of the original map loaded into OED.
- const imageDimensions: Dimensions = {
- width: image.width,
- height: image.height
- };
- // Determine the dimensions so within the Plotly coordinates on the user map.
- const imageDimensionNormalized = normalizeImageDimensions(imageDimensions);
- // This is the origin & opposite from the calibration. It is the lower, left
- // and upper, right corners of the user map.
- const origin = map.origin;
- const opposite = map.opposite;
- // Get the GPS degrees per unit of Plotly grid for x and y. By knowing the two corners
- // (or really any two distinct points) you can calculate this by the change in GPS over the
- // change in x or y which is the map's width & height in this case.
- const scaleOfMap = calculateScaleFromEndpoints(origin, opposite, imageDimensionNormalized, map.northAngle);
- // Loop over all selected meters. Maps only work for meters at this time.
- // The y-axis label depends on the unit which is in selectUnit state.
- let unitLabel: string = '';
- // If graphingUnit is -99 then none selected and nothing to graph so label is empty.
- // This will probably happen when the page is first loaded.
- if (unitID !== -99) {
- const selectUnitState = unitDataById[unitID];
- if (selectUnitState !== undefined) {
- // Quantity and flow units have different unit labels.
- // Look up the type of unit if it is for quantity/flow (should not be raw) and decide what to do.
- // Bar graphics are always quantities.
- if (selectUnitState.unitRepresent === UnitRepresentType.quantity) {
- // If it is a quantity unit then that is the unit you are graphing but it is normalized to per day.
- unitLabel = selectUnitState.identifier + ' / day';
- } else if (selectUnitState.unitRepresent === UnitRepresentType.flow) {
- // If it is a flow meter then you need to multiply by time to get the quantity unit then show as per day.
- // The quantity/time for flow has varying time so label by multiplying by time.
- // To make sure it is clear, also indicate it is a quantity.
- // Note this should not be used for raw data.
- // It might not be usual to take a flow and make it into a quantity so this label is a little different to
- // catch people's attention. If sites/users don't like OED doing this then we can eliminate flow for these types
- // of graphics as we are doing for rate.
- unitLabel = selectUnitState.identifier + ' * time / day ≡ quantity / day';
- }
+ for (const meterID of selectedMeters) {
+ // Get meter id number.
+ // Get meter GPS value.
+ const gps = meterDataById[meterID].gps;
+ // filter meters with actual gps coordinates.
+ if (gps !== undefined && gps !== null && meterReadings !== undefined) {
+ let meterArea = meterDataById[meterID].area;
+ // we either don't care about area, or we do in which case there needs to be a nonzero area
+ if (!areaNormalization || (meterArea > 0 && meterDataById[meterID].areaUnit != AreaUnitType.none)) {
if (areaNormalization) {
- unitLabel += ' / ' + translate(`AreaUnitType.${selectedAreaUnit}`);
+ // convert the meter area into the proper unit, if needed
+ meterArea *= getAreaUnitConversion(meterDataById[meterID].areaUnit, selectedAreaUnit);
}
- }
- }
-
- for (const meterID of selectedMeters) {
- // Get meter id number.
- // Get meter GPS value.
- const gps = meterDataById[meterID].gps;
- // filter meters with actual gps coordinates.
- if (gps !== undefined && gps !== null && meterReadings !== undefined) {
- let meterArea = meterDataById[meterID].area;
- // we either don't care about area, or we do in which case there needs to be a nonzero area
- if (!areaNormalization || (meterArea > 0 && meterDataById[meterID].areaUnit != AreaUnitType.none)) {
- if (areaNormalization) {
- // convert the meter area into the proper unit, if needed
- meterArea *= getAreaUnitConversion(meterDataById[meterID].areaUnit, selectedAreaUnit);
- }
- // Convert the gps value to the equivalent Plotly grid coordinates on user map.
- // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map.
- // It must be on true north map since only there are the GPS axis parallel to the map axis.
- // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating
- // it coordinates on the true north map and then rotating/shifting to the user map.
- const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle);
- // Only display items within valid info and within map.
- if (itemMapInfoOk(meterID, DataType.Meter, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid)) {
- // The x, y value for Plotly to use that are on the user map.
- x.push(meterGPSInUserGrid.x);
- y.push(meterGPSInUserGrid.y);
- // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the barDuration might have changed
- // and be fetching. The unit could change from that menu so also need to check.
- // Get the bar data to use for the map circle.
- // const readingsData = meterReadings[timeInterval.toString()][barDuration.toISOString()][unitID];
- const readingsData = meterReadings[meterID];
- // This protects against there being no readings or that the data is being updated.
- if (readingsData !== undefined && !meterIsFetching) {
- // Meter name to include in hover on graph.
- const label = meterDataById[meterID].identifier;
- // The usual color for this meter.
- colors.push(getGraphColor(meterID, DataType.Meter));
- if (!readingsData) {
- throw new Error('Unacceptable condition: readingsData.readings is undefined.');
+ // Convert the gps value to the equivalent Plotly grid coordinates on user map.
+ // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map.
+ // It must be on true north map since only there are the GPS axis parallel to the map axis.
+ // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating
+ // it coordinates on the true north map and then rotating/shifting to the user map.
+ const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle);
+ // Only display items within valid info and within map.
+ if (itemMapInfoOk(meterID, DataType.Meter, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid)) {
+ // The x, y value for Plotly to use that are on the user map.
+ x.push(meterGPSInUserGrid.x);
+ y.push(meterGPSInUserGrid.y);
+ // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the barDuration might have changed
+ // and be fetching. The unit could change from that menu so also need to check.
+ // Get the bar data to use for the map circle.
+ // const readingsData = meterReadings[timeInterval.toString()][barDuration.toISOString()][unitID];
+ const readingsData = meterReadings[meterID];
+ // This protects against there being no readings or that the data is being updated.
+ if (readingsData !== undefined && !meterIsFetching) {
+ // Meter name to include in hover on graph.
+ const label = meterDataById[meterID].identifier;
+ // The usual color for this meter.
+ colors.push(getGraphColor(meterID, DataType.Meter));
+ if (!readingsData) {
+ throw new Error('Unacceptable condition: readingsData.readings is undefined.');
+ }
+ // Use the most recent time reading for the circle on the map.
+ // This has the limitations of the bar value where the last one can include ranges without
+ // data (GitHub issue on this).
+ // TODO: It might be better to do this similarly to compare. (See GitHub issue)
+ const readings = orderBy(readingsData, ['startTimestamp'], ['desc']);
+ const mapReading = readings[0];
+ let timeReading: string;
+ let averagedReading = 0;
+ if (readings.length === 0) {
+ // No data. The next lines causes an issue so set specially.
+ // There may be a better overall fix for no data.
+ timeReading = 'no data to display';
+ size.push(0);
+ } else {
+ // only display a range of dates for the hover text if there is more than one day in the range
+ // Shift to UTC since want database time not local/browser time which is what moment does.
+ timeReading = `${moment.utc(mapReading.startTimestamp).format('ll')}`;
+ if (barDuration.asDays() != 1) {
+ // subtracting one extra day caused by day ending at midnight of the next day.
+ // Going from DB unit timestamp that is UTC so force UTC with moment, as usual.
+ timeReading += ` - ${moment.utc(mapReading.endTimestamp).subtract(1, 'days').format('ll')}`;
}
- // Use the most recent time reading for the circle on the map.
- // This has the limitations of the bar value where the last one can include ranges without
- // data (GitHub issue on this).
- // TODO: It might be better to do this similarly to compare. (See GitHub issue)
- const readings = orderBy(readingsData, ['startTimestamp'], ['desc']);
- const mapReading = readings[0];
- let timeReading: string;
- let averagedReading = 0;
- if (readings.length === 0) {
- // No data. The next lines causes an issue so set specially.
- // There may be a better overall fix for no data.
- timeReading = 'no data to display';
- size.push(0);
- } else {
- // only display a range of dates for the hover text if there is more than one day in the range
- // Shift to UTC since want database time not local/browser time which is what moment does.
- timeReading = `${moment.utc(mapReading.startTimestamp).format('ll')}`;
- if (barDuration.asDays() != 1) {
- // subtracting one extra day caused by day ending at midnight of the next day.
- // Going from DB unit timestamp that is UTC so force UTC with moment, as usual.
- timeReading += ` - ${moment.utc(mapReading.endTimestamp).subtract(1, 'days').format('ll')}`;
- }
- // The value for the circle is the average daily usage.
- averagedReading = mapReading.reading / barDuration.asDays();
- if (areaNormalization) {
- averagedReading /= meterArea;
- }
- // The size is the reading value. It will be scaled later.
- size.push(averagedReading);
+ // The value for the circle is the average daily usage.
+ averagedReading = mapReading.reading / barDuration.asDays();
+ if (areaNormalization) {
+ averagedReading /= meterArea;
}
- // The hover text.
- hoverText.push(` ${timeReading} ${label}: ${averagedReading.toPrecision(6)} ${unitLabel}`);
+ // The size is the reading value. It will be scaled later.
+ size.push(averagedReading);
}
+ // The hover text.
+ hoverText.push(` ${timeReading} ${label}: ${averagedReading.toPrecision(6)} ${unitLabel}`);
}
}
}
}
+ }
- for (const groupID of selectedGroups) {
- // Get group id number.
- // Get group GPS value.
- const gps = groupDataById[groupID].gps;
- // Filter groups with actual gps coordinates.
- if (gps !== undefined && gps !== null && groupData !== undefined) {
- let groupArea = groupDataById[groupID].area;
- if (!areaNormalization || (groupArea > 0 && groupDataById[groupID].areaUnit != AreaUnitType.none)) {
- if (areaNormalization) {
- // convert the meter area into the proper unit, if needed
- groupArea *= getAreaUnitConversion(groupDataById[groupID].areaUnit, selectedAreaUnit);
- }
- // Convert the gps value to the equivalent Plotly grid coordinates on user map.
- // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map.
- // It must be on true north map since only there are the GPS axis parallel to the map axis.
- // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating
- // it coordinates on the true north map and then rotating/shifting to the user map.
- const groupGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle);
- // Only display items within valid info and within map.
- if (itemMapInfoOk(groupID, DataType.Group, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid)) {
- // The x, y value for Plotly to use that are on the user map.
- x.push(groupGPSInUserGrid.x);
- y.push(groupGPSInUserGrid.y);
- // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the barDuration might have changed
- // and be fetching. The unit could change from that menu so also need to check.
- // Get the bar data to use for the map circle.
- const readingsData = groupData[groupID];
- // This protects against there being no readings or that the data is being updated.
- if (readingsData && !groupIsFetching) {
- // Group name to include in hover on graph.
- const label = groupDataById[groupID].name;
- // The usual color for this group.
- colors.push(getGraphColor(groupID, DataType.Group));
- if (!readingsData) {
- throw new Error('Unacceptable condition: readingsData.readings is undefined.');
+ for (const groupID of selectedGroups) {
+ // Get group id number.
+ // Get group GPS value.
+ const gps = groupDataById[groupID].gps;
+ // Filter groups with actual gps coordinates.
+ if (gps !== undefined && gps !== null && groupData !== undefined) {
+ let groupArea = groupDataById[groupID].area;
+ if (!areaNormalization || (groupArea > 0 && groupDataById[groupID].areaUnit != AreaUnitType.none)) {
+ if (areaNormalization) {
+ // convert the meter area into the proper unit, if needed
+ groupArea *= getAreaUnitConversion(groupDataById[groupID].areaUnit, selectedAreaUnit);
+ }
+ // Convert the gps value to the equivalent Plotly grid coordinates on user map.
+ // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map.
+ // It must be on true north map since only there are the GPS axis parallel to the map axis.
+ // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating
+ // it coordinates on the true north map and then rotating/shifting to the user map.
+ const groupGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle);
+ // Only display items within valid info and within map.
+ if (itemMapInfoOk(groupID, DataType.Group, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid)) {
+ // The x, y value for Plotly to use that are on the user map.
+ x.push(groupGPSInUserGrid.x);
+ y.push(groupGPSInUserGrid.y);
+ // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the barDuration might have changed
+ // and be fetching. The unit could change from that menu so also need to check.
+ // Get the bar data to use for the map circle.
+ const readingsData = groupData[groupID];
+ // This protects against there being no readings or that the data is being updated.
+ if (readingsData && !groupIsFetching) {
+ // Group name to include in hover on graph.
+ const label = groupDataById[groupID].name;
+ // The usual color for this group.
+ colors.push(getGraphColor(groupID, DataType.Group));
+ if (!readingsData) {
+ throw new Error('Unacceptable condition: readingsData.readings is undefined.');
+ }
+ // Use the most recent time reading for the circle on the map.
+ // This has the limitations of the bar value where the last one can include ranges without
+ // data (GitHub issue on this).
+ // TODO: It might be better to do this similarly to compare. (See GitHub issue)
+ const readings = orderBy(readingsData, ['startTimestamp'], ['desc']);
+ const mapReading = readings[0];
+ let timeReading: string;
+ let averagedReading = 0;
+ if (readings.length === 0) {
+ // No data. The next lines causes an issue so set specially.
+ // There may be a better overall fix for no data.
+ timeReading = 'no data to display';
+ size.push(0);
+ } else {
+ // only display a range of dates for the hover text if there is more than one day in the range
+ timeReading = `${moment.utc(mapReading.startTimestamp).format('ll')}`;
+ if (barDuration.asDays() != 1) {
+ // subtracting one extra day caused by day ending at midnight of the next day.
+ // Going from DB unit timestamp that is UTC so force UTC with moment, as usual.
+ timeReading += ` - ${moment.utc(mapReading.endTimestamp).subtract(1, 'days').format('ll')}`;
}
- // Use the most recent time reading for the circle on the map.
- // This has the limitations of the bar value where the last one can include ranges without
- // data (GitHub issue on this).
- // TODO: It might be better to do this similarly to compare. (See GitHub issue)
- const readings = orderBy(readingsData, ['startTimestamp'], ['desc']);
- const mapReading = readings[0];
- let timeReading: string;
- let averagedReading = 0;
- if (readings.length === 0) {
- // No data. The next lines causes an issue so set specially.
- // There may be a better overall fix for no data.
- timeReading = 'no data to display';
- size.push(0);
- } else {
- // only display a range of dates for the hover text if there is more than one day in the range
- timeReading = `${moment.utc(mapReading.startTimestamp).format('ll')}`;
- if (barDuration.asDays() != 1) {
- // subtracting one extra day caused by day ending at midnight of the next day.
- // Going from DB unit timestamp that is UTC so force UTC with moment, as usual.
- timeReading += ` - ${moment.utc(mapReading.endTimestamp).subtract(1, 'days').format('ll')}`;
- }
- // The value for the circle is the average daily usage.
- averagedReading = mapReading.reading / barDuration.asDays();
- if (areaNormalization) {
- averagedReading /= groupArea;
- }
- // The size is the reading value. It will be scaled later.
- size.push(averagedReading);
+ // The value for the circle is the average daily usage.
+ averagedReading = mapReading.reading / barDuration.asDays();
+ if (areaNormalization) {
+ averagedReading /= groupArea;
}
- // The hover text.
- hoverText.push(` ${timeReading} ${label}: ${averagedReading.toPrecision(6)} ${unitLabel}`);
+ // The size is the reading value. It will be scaled later.
+ size.push(averagedReading);
}
+ // The hover text.
+ hoverText.push(` ${timeReading} ${label}: ${averagedReading.toPrecision(6)} ${unitLabel}`);
}
}
}
}
- // TODO Using the following seems to have no impact on the code. It has been noticed that this function is called
- // many times for each change. Someone should look at why that is happening and why some have no items in the arrays.
- // if (size.length > 0) {
- // TODO The max circle diameter should come from admin/DB.
- const maxFeatureFraction = map.circleSize;
- // Find the smaller of width and height. This is used since it means the circle size will be
- // scaled to that dimension and smaller relative to the other coordinate.
- const minDimension = Math.min(imageDimensionNormalized.width, imageDimensionNormalized.height);
- // The circle size is set to area below. Thus, we need to convert from wanting a max
- // diameter of minDimension * maxFeatureFraction to an area.
- const maxCircleSize = Math.PI * Math.pow(minDimension * maxFeatureFraction / 2, 2);
- // Find the largest circle which is usage.
- const largestCircleSize = Math.max(...size);
- // Scale largest circle to the max size and others will be scaled to be smaller.
- // Not that < 1 => a larger circle.
- const scaling = largestCircleSize / maxCircleSize;
-
- // Per https://plotly.com/javascript/reference/scatter/:
- // The opacity of 0.5 makes it possible to see the map even when there is a circle but the hover
- // opacity is 1 so it is easy to see.
- // Set the sizemode to area not diameter.
- // Set the sizemin so a circle cannot get so small that it might disappear. Unsure the best size.
- // Set the sizeref to scale each point to the desired area.
- // Note all sizes are in px so have to estimate the actual size. This could be an issue but maps are currently
- // a fixed size so not too much of an issue.
- // Also note that the circle can go off the edge of the map. At some point it would be nice to have a border
- // around the map to avoid this.
- const traceOne = {
- x,
- y,
- type: 'scatter',
- mode: 'markers',
- marker: {
- color: colors,
- opacity: 0.5,
- size,
- sizemin: 6,
- sizeref: scaling,
- sizemode: 'area'
- },
- text: hoverText,
- hoverinfo: 'text',
- opacity: 1,
- showlegend: false
- };
- data.push(traceOne);
}
+ // TODO Using the following seems to have no impact on the code. It has been noticed that this function is called
+ // many times for each change. Someone should look at why that is happening and why some have no items in the arrays.
+ // if (size.length > 0) {
+ // TODO The max circle diameter should come from admin/DB.
+ const maxFeatureFraction = map.circleSize;
+ // Find the smaller of width and height. This is used since it means the circle size will be
+ // scaled to that dimension and smaller relative to the other coordinate.
+ const minDimension = Math.min(imageDimensionNormalized.width, imageDimensionNormalized.height);
+ // The circle size is set to area below. Thus, we need to convert from wanting a max
+ // diameter of minDimension * maxFeatureFraction to an area.
+ const maxCircleSize = Math.PI * Math.pow(minDimension * maxFeatureFraction / 2, 2);
+ // Find the largest circle which is usage.
+ const largestCircleSize = Math.max(...size);
+ // Scale largest circle to the max size and others will be scaled to be smaller.
+ // Not that < 1 => a larger circle.
+ const scaling = largestCircleSize / maxCircleSize;
+
+ // Per https://plotly.com/javascript/reference/scatter/:
+ // The opacity of 0.5 makes it possible to see the map even when there is a circle but the hover
+ // opacity is 1 so it is easy to see.
+ // Set the sizemode to area not diameter.
+ // Set the sizemin so a circle cannot get so small that it might disappear. Unsure the best size.
+ // Set the sizeref to scale each point to the desired area.
+ // Note all sizes are in px so have to estimate the actual size. This could be an issue but maps are currently
+ // a fixed size so not too much of an issue.
+ // Also note that the circle can go off the edge of the map. At some point it would be nice to have a border
+ // around the map to avoid this.
+ const traceOne = {
+ x,
+ y,
+ type: 'scatter',
+ mode: 'markers',
+ marker: {
+ color: colors,
+ opacity: 0.5,
+ size,
+ sizemin: 6,
+ sizeref: scaling,
+ sizemode: 'area'
+ },
+ text: hoverText,
+ hoverinfo: 'text',
+ opacity: 1,
+ showlegend: false
+ };
+ data.push(traceOne);
}
// set map background image
@@ -357,7 +343,7 @@ export default function MapChartComponent() {
},
images: [{
layer: 'below',
- source: (image) ? image.src : '',
+ source: map?.mapSource,
xref: 'x',
yref: 'y',
x: 0,
@@ -377,4 +363,4 @@ export default function MapChartComponent() {
layout={layout}
/>
);
-}
+}
\ No newline at end of file
diff --git a/src/client/app/components/MapChartSelectComponent.tsx b/src/client/app/components/MapChartSelectComponent.tsx
index bea22af7f..9c1aa0b47 100644
--- a/src/client/app/components/MapChartSelectComponent.tsx
+++ b/src/client/app/components/MapChartSelectComponent.tsx
@@ -3,11 +3,10 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import * as React from 'react';
-import { sortBy, values } from 'lodash';
-import { useDispatch, useSelector } from 'react-redux';
-import { State } from '../types/redux/state';
-import { SelectOption } from '../types/items';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
+import { selectMapById, selectMapSelectOptions } from '../redux/api/mapsApi';
+import { useAppDispatch, useAppSelector } from '../redux/reduxHooks';
+import { selectSelectedMap, updateSelectedMaps } from '../redux/slices/graphSlice';
import SingleSelectComponent from './SingleSelectComponent';
import TooltipMarkerComponent from './TooltipMarkerComponent';
@@ -24,20 +23,16 @@ export default function MapChartSelectComponent() {
margin: 0
};
const messages = defineMessages({
- selectMap: {id: 'select.map'}
+ selectMap: { id: 'select.map' }
});
// TODO When this is converted to RTK then should use useAppDispatch().
//Utilizes useDispatch and useSelector hooks
- const dispatch = useDispatch();
- const sortedMaps = sortBy(values(useSelector((state: State) => state.maps.byMapID)).map(map => (
- { value: map.id, label: map.name, isDisabled: !(map.origin && map.opposite) } as SelectOption
- )), 'label');
+ const dispatch = useAppDispatch();
+
+ const sortedMaps = useAppSelector(selectMapSelectOptions);
+ const selectedMapData = useAppSelector(state => selectMapById(state, selectSelectedMap(state)));
- const selectedMap = {
- label: useSelector((state: State) => state.maps.byMapID[state.maps.selectedMap] ? state.maps.byMapID[state.maps.selectedMap].name : ''),
- value: useSelector((state: State) => state.maps.selectedMap)
- };
//useIntl instead of injectIntl and WrappedComponentProps
const intl = useIntl();
@@ -46,16 +41,14 @@ export default function MapChartSelectComponent() {
:
-
+
dispatch({type: 'UPDATE_SELECTED_MAPS', mapID: selected.value})}
- //When we specify stuff in actions files, we also specify other variables, in this case mapID.
- //This is where we specify values instead of triggering the action by itself.
+ onValueChange={selected => dispatch(updateSelectedMaps(selected.value))}
/>
diff --git a/src/client/app/components/PlotNavComponent.tsx b/src/client/app/components/PlotNavComponent.tsx
index 21e28cd60..0729c53b5 100644
--- a/src/client/app/components/PlotNavComponent.tsx
+++ b/src/client/app/components/PlotNavComponent.tsx
@@ -4,7 +4,7 @@
import * as React from 'react';
import { TimeInterval } from '../../../common/TimeInterval';
-import { clearGraphHistory } from '../redux/actions/extraActions';
+import { clearGraphHistory } from '../redux/slices/graphSlice';
import { useAppDispatch, useAppSelector } from '../redux/reduxHooks';
import { selectAnythingFetching } from '../redux/selectors/apiSelectors';
import {
@@ -40,7 +40,7 @@ export const ExpandComponent = () => {
const dispatch = useAppDispatch();
return (
{ dispatch(changeSliderRange(TimeInterval.unbounded())); }}
+ onClick={() => { dispatch(changeSliderRange(TimeInterval.unbounded().toString())); }}
/>
);
};
@@ -70,7 +70,7 @@ export const RefreshGraphComponent = () => {
{ !somethingFetching && dispatch(updateTimeInterval(sliderInterval)); }}
+ onClick={() => { !somethingFetching && dispatch(updateTimeInterval(sliderInterval.toString())); }}
/>
);
};
diff --git a/src/client/app/components/PlotOED.tsx b/src/client/app/components/PlotOED.tsx
index b460cf79a..0adbe7fdf 100644
--- a/src/client/app/components/PlotOED.tsx
+++ b/src/client/app/components/PlotOED.tsx
@@ -44,14 +44,15 @@ export const PlotOED = (props: OEDPlotProps) => {
const startTS = moment.utc(e['xaxis.range[0]']);
const endTS = moment.utc(e['xaxis.range[1]']);
const workingTimeInterval = new TimeInterval(startTS, endTS);
- dispatch(changeSliderRange(workingTimeInterval));
+ dispatch(changeSliderRange(workingTimeInterval.toString()));
}
else if (e['xaxis.range']) {
// this case is when the slider knobs are dragged.
const range = figure.current.layout?.xaxis?.range;
const startTS = range && range[0];
const endTS = range && range[1];
- dispatch(changeSliderRange(new TimeInterval(startTS, endTS)));
+ const interval = new TimeInterval(startTS, endTS).toString();
+ dispatch(changeSliderRange(interval));
}
}, 500, { leading: false, trailing: true });
diff --git a/src/client/app/components/RouteComponent.tsx b/src/client/app/components/RouteComponent.tsx
index 0a871d5b8..16d93065b 100644
--- a/src/client/app/components/RouteComponent.tsx
+++ b/src/client/app/components/RouteComponent.tsx
@@ -4,11 +4,8 @@
import * as React from 'react';
import { IntlProvider } from 'react-intl';
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
-import ReadingsCSVUploadComponent from '../components/csv/ReadingsCSVUploadComponent';
-import MetersCSVUploadComponent from '../components/csv/MetersCSVUploadComponent';
-import MapCalibrationContainer from '../containers/maps/MapCalibrationContainer';
-import MapsDetailContainer from '../containers/maps/MapsDetailContainer';
import { useAppSelector } from '../redux/reduxHooks';
+import { selectSelectedLanguage } from '../redux/slices/appStateSlice';
import LocaleTranslationData from '../translations/data';
import { UserRole } from '../types/items';
import AppLayout from './AppLayout';
@@ -18,14 +15,17 @@ import AdminComponent from './admin/AdminComponent';
import UsersDetailComponent from './admin/users/UsersDetailComponent';
import ConversionsDetailComponent from './conversion/ConversionsDetailComponent';
import GroupsDetailComponent from './groups/GroupsDetailComponent';
+import { MapCalibrationComponent } from './maps/MapCalibrationComponent';
+import MapsDetailComponent from './maps/MapsDetailComponent';
import MetersDetailComponent from './meters/MetersDetailComponent';
import AdminOutlet from './router/AdminOutlet';
+import ErrorComponent from './router/ErrorComponent';
import { GraphLink } from './router/GraphLinkComponent';
import NotFound from './router/NotFoundOutlet';
import RoleOutlet from './router/RoleOutlet';
import UnitsDetailComponent from './unit/UnitsDetailComponent';
-import ErrorComponent from './router/ErrorComponent';
-import { selectSelectedLanguage } from '../redux/slices/appStateSlice';
+import MetersCSVUploadComponent from './csv/MetersCSVUploadComponent';
+import ReadingsCSVUploadComponent from './csv/ReadingsCSVUploadComponent';
/**
* @returns the router component Responsible for client side routing.
@@ -55,10 +55,10 @@ const router = createBrowserRouter([
element: ,
children: [
{ path: 'admin', element: },
- { path: 'calibration', element: },
+ { path: 'calibration', element: },
+ { path: 'maps', element: },
{ path: 'conversions', element: },
{ path: 'csvMeters', element: },
- { path: 'maps', element: },
{ path: 'units', element: },
{ path: 'users', element: }
]
diff --git a/src/client/app/components/ThreeDComponent.tsx b/src/client/app/components/ThreeDComponent.tsx
index 658df7415..37305d073 100644
--- a/src/client/app/components/ThreeDComponent.tsx
+++ b/src/client/app/components/ThreeDComponent.tsx
@@ -11,7 +11,7 @@ import { selectUnitDataById } from '../redux/api/unitsApi';
import { useAppSelector } from '../redux/reduxHooks';
import { selectThreeDQueryArgs } from '../redux/selectors/chartQuerySelectors';
import { selectThreeDComponentInfo } from '../redux/selectors/threeDSelectors';
-import { selectGraphState } from '../redux/slices/graphSlice';
+import { selectGraphState, selectQueryTimeInterval } from '../redux/slices/graphSlice';
import { ThreeDReading } from '../types/readings';
import { GraphState, MeterOrGroup } from '../types/redux/graph';
import { GroupDataByID } from '../types/redux/groups';
@@ -38,6 +38,7 @@ export default function ThreeDComponent() {
const groupDataById = useAppSelector(selectGroupDataById);
const unitDataById = useAppSelector(selectUnitDataById);
const graphState = useAppSelector(selectGraphState);
+ const queryTimeInterval = useAppSelector(selectQueryTimeInterval);
const locale = useAppSelector(selectSelectedLanguage);
const { meterOrGroupID, meterOrGroupName, isAreaCompatible } = useAppSelector(selectThreeDComponentInfo);
@@ -53,7 +54,7 @@ export default function ThreeDComponent() {
layout = setHelpLayout(translate('select.meter.group'));
} else if (graphState.areaNormalization && !isAreaCompatible) {
layout = setHelpLayout(`${meterOrGroupName}${translate('threeD.area.incompatible')}`);
- } else if (!isValidThreeDInterval(roundTimeIntervalForFetch(graphState.queryTimeInterval))) {
+ } else if (!isValidThreeDInterval(roundTimeIntervalForFetch(queryTimeInterval))) {
// Not a valid time interval. ThreeD can only support up to 1 year of readings
layout = setHelpLayout(translate('threeD.date.range.too.long'));
} else if (!threeDData) {
diff --git a/src/client/app/components/conversion/ConversionsDetailComponent.tsx b/src/client/app/components/conversion/ConversionsDetailComponent.tsx
index 41654ed53..fc4a48528 100644
--- a/src/client/app/components/conversion/ConversionsDetailComponent.tsx
+++ b/src/client/app/components/conversion/ConversionsDetailComponent.tsx
@@ -7,11 +7,12 @@ import { FormattedMessage } from 'react-intl';
import SpinnerComponent from '../SpinnerComponent';
import TooltipHelpComponent from '../TooltipHelpComponent';
import { conversionsApi, stableEmptyConversions } from '../../redux/api/conversionsApi';
-import { stableEmptyUnitDataById, unitsAdapter, unitsApi } from '../../redux/api/unitsApi';
+import { stableEmptyUnitDataById, unitsApi } from '../../redux/api/unitsApi';
import { ConversionData } from '../../types/redux/conversions';
import TooltipMarkerComponent from '../TooltipMarkerComponent';
import ConversionViewComponent from './ConversionViewComponent';
import CreateConversionModalComponent from './CreateConversionModalComponent';
+import { unitsAdapter } from '../../redux/entityAdapters';
/**
* Defines the conversions page card view
diff --git a/src/client/app/components/maps/CreateMapModalComponent.tsx b/src/client/app/components/maps/CreateMapModalComponent.tsx
new file mode 100644
index 000000000..7980a82aa
--- /dev/null
+++ b/src/client/app/components/maps/CreateMapModalComponent.tsx
@@ -0,0 +1,80 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import * as React from 'react';
+import { useState } from 'react';
+import { FormattedMessage } from 'react-intl';
+import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input } from 'reactstrap';
+import { Link } from 'react-router-dom';
+
+interface CreateMapModalProps {
+ show: boolean;
+ handleClose: () => void;
+ createNewMap: () => void;
+}
+
+/**
+ * Defines the create map modal form
+ * params not given since props should be going away and painful. Remove eslint command when fixed.
+ * @returns Map create element
+ */
+/* eslint-disable-next-line */
+function CreateMapModalComponent({ show, handleClose, createNewMap }: CreateMapModalProps) {
+ // TODO: Get rid of props, migrate to RTK, finish modal
+ // Once modal is finished, it will be used in MapsDetailComponent
+ const [nameInput, setNameInput] = useState('');
+ const [noteInput, setNoteInput] = useState('');
+
+ const handleCreate = () => {
+ // TODO: Implement create functionality
+ createNewMap();
+ handleClose();
+ };
+
+ return (
+
+
+
+
+
+
+
+ createNewMap()}>
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default CreateMapModalComponent;
diff --git a/src/client/app/components/maps/EditMapModalComponent.tsx b/src/client/app/components/maps/EditMapModalComponent.tsx
new file mode 100644
index 000000000..f02cb2656
--- /dev/null
+++ b/src/client/app/components/maps/EditMapModalComponent.tsx
@@ -0,0 +1,185 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { debounce, isEqual } from 'lodash';
+import * as React from 'react';
+import { useState } from 'react';
+import { FormattedMessage, useIntl } from 'react-intl';
+import { Link } from 'react-router-dom';
+import { Button, Form, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
+import { mapsApi, selectMapById } from '../../redux/api/mapsApi';
+import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks';
+import { localEditsSlice } from '../../redux/slices/localEditsSlice';
+import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map';
+import { showErrorNotification } from '../../utils/notifications';
+
+interface EditMapModalProps {
+ map: MapMetadata;
+}
+
+// TODO: Migrate to RTK
+const EditMapModalComponent: React.FC = ({ map }) => {
+ const [showModal, setShowModal] = useState(false);
+ const dispatch = useAppDispatch();
+ const [nameInput, setNameInput] = useState(map.name);
+ const [noteInput, setNoteInput] = useState(map.note || '');
+ const [circleInput, setCircleInput] = useState(map.circleSize);
+ const [displayable, setDisplayable] = useState(map.displayable);
+ const [submitEdit] = mapsApi.useEditMapMutation();
+ const [deleteMap] = mapsApi.useDeleteMapMutation();
+ // Only used to track stable reference changes to reset form.
+ const apiMapCache = useAppSelector(state => selectMapById(state, map.id));
+ const intl = useIntl();
+
+
+ const handleShow = () => setShowModal(true);
+ const handleClose = () => setShowModal(false);
+ const updatedMap = (): MapMetadata => ({
+ ...map,
+ name: nameInput,
+ note: noteInput,
+ circleSize: circleInput,
+ displayable: displayable
+ });
+ const debouncedLocalUpdate = React.useMemo(() => debounce(
+ (map: MapMetadata) => !isEqual(map, updatedMap()) && dispatch(localEditsSlice.actions.setOneEdit(map)),
+ 1000
+ ), []);
+ React.useEffect(() => { debouncedLocalUpdate(updatedMap()); }, [nameInput, noteInput, circleInput, displayable]);
+
+ // Sync with API Cache changes, if any.
+ React.useEffect(() => {
+ setNameInput(map.name);
+ setNoteInput(map.note || '');
+ setCircleInput(map.circleSize);
+ setDisplayable(map.displayable);
+ }, [apiMapCache]);
+
+ const handleSave = () => {
+ submitEdit(updatedMap());
+ handleClose();
+ };
+
+ const handleDelete = () => {
+ const consent = window.confirm(intl.formatMessage({ id: 'map.confirm.remove' }, { name: map.name }));
+ if (consent) {
+ deleteMap(map.id);
+ handleClose();
+ }
+ };
+
+ const handleCalibrationSetting = (mode: CalibrationModeTypes) => {
+ // Add/update entry to localEdits Slice
+ dispatch(localEditsSlice.actions.setOneEdit(updatedMap()));
+ // Update Calibration Mode
+ dispatch(localEditsSlice.actions.updateMapCalibrationMode({ mode, id: map.id }));
+ handleClose();
+ };
+
+ const circIsValid = circleInput > 0.0 && circleInput <= 2.0;
+ const toggleCircleEdit = () => {
+ if (!circIsValid) {
+ showErrorNotification(intl.formatMessage({ id: 'invalid.number' }));
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default EditMapModalComponent;
diff --git a/src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts b/src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx
similarity index 57%
rename from src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts
rename to src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx
index 6dfcd905a..089bc66cd 100644
--- a/src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts
+++ b/src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx
@@ -2,23 +2,28 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-import { connect } from 'react-redux';
+import { PlotData, PlotMouseEvent } from 'plotly.js';
+import * as React from 'react';
import Plot from 'react-plotly.js';
-import { State } from '../../types/redux/state';
-import * as plotly from 'plotly.js';
-import { CartesianPoint, Dimensions, normalizeImageDimensions } from '../../utils/calibration';
-import { updateCurrentCartesian } from '../../redux/actions/map';
-import { store } from '../../store';
-import { CalibrationSettings } from '../../types/redux/map';
+import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks';
+import { selectSelectedLanguage } from '../../redux/slices/appStateSlice';
+import { localEditsSlice } from '../../redux/slices/localEditsSlice';
import Locales from '../../types/locales';
+import { CalibrationSettings } from '../../types/redux/map';
+import { CartesianPoint, Dimensions, normalizeImageDimensions } from '../../utils/calibration';
-function mapStateToProps(state: State) {
+/**
+ * @returns TODO DO ME
+ */
+export default function MapCalibrationChartDisplayContainer() {
+ const dispatch = useAppDispatch();
const x: number[] = [];
const y: number[] = [];
const texts: string[] = [];
+ const currentLanguange = useAppSelector(selectSelectedLanguage);
+ const map = useAppSelector(state => localEditsSlice.selectors.selectLocalEdit(state, localEditsSlice.selectors.selectCalibrationMapId(state)));
- const mapID = state.maps.calibratingMap;
- const map = state.maps.editedMaps[mapID];
+ const settings = useAppSelector(state => state.localEdits.calibrationSettings);
const points = map.calibrationSet;
if (points) {
for (const point of points) {
@@ -28,10 +33,9 @@ function mapStateToProps(state: State) {
}
}
const imageDimensions: Dimensions = normalizeImageDimensions({
- width: map.image.width,
- height: map.image.height
+ width: map.imgWidth,
+ height: map.imgHeight
});
- const settings = state.maps.calibrationSettings;
const backgroundTrace = createBackgroundTrace(imageDimensions, settings);
const dataPointTrace = {
x,
@@ -49,7 +53,7 @@ function mapStateToProps(state: State) {
};
const data = [backgroundTrace, dataPointTrace];
- const imageSource = map.image.src;
+ const imageSource = map.mapSource;
// for a detailed description of layout attributes: https://plotly.com/javascript/reference/#layout
const layout: any = {
@@ -80,23 +84,37 @@ function mapStateToProps(state: State) {
}]
};
- /***
- * Usage:
- * console.log(points, event)}>
- * Plotly no longer has IPlotlyChartProps so we will use any for now.
- */
- const props: any = {
- data,
- layout,
- onClick: (event: plotly.PlotMouseEvent) => handlePointClick(event),
- config: {
- locales: Locales // makes locales available for use
- }
- };
- props.config.locale = state.appState.selectedLanguage;
- return props;
+ return {
+ // trace 0 keeps a transparent trace of closely positioned points used for calibration(backgroundTrace),
+ // trace 1 keeps the data points used for calibration are automatically added to the same trace(dataPointTrace),
+ // event.points will include all points near a mouse click, including those in the backgroundTrace and the dataPointTrace,
+ // so the algorithm only looks at trace 0 since points from trace 1 are already put into the data set used for calibration.
+ event.event.preventDefault();
+ const eligiblePoints = [];
+ for (const point of event.points) {
+ const traceNumber = point.curveNumber;
+ if (traceNumber === 0) {
+ eligiblePoints.push(point);
+ }
+ }
+ // TODO VERIFY
+ const xValue = eligiblePoints[0].x as number;
+ const yValue = eligiblePoints[0].y as number;
+ const clickedPoint: CartesianPoint = {
+ x: Number(xValue.toFixed(6)),
+ y: Number(yValue.toFixed(6))
+ };
+ dispatch(localEditsSlice.actions.updateCurrentCartesian(clickedPoint));
+ }}
+ />;
}
/**
@@ -138,36 +156,4 @@ function createBackgroundTrace(imageDimensions: Dimensions, settings: Calibratio
showscale: false
};
return trace;
-}
-
-function handlePointClick(event: plotly.PlotMouseEvent) {
- event.event.preventDefault();
- const currentPoint: CartesianPoint = getClickedCoordinates(event);
- store.dispatch(updateCurrentCartesian(currentPoint));
-}
-
-function getClickedCoordinates(event: plotly.PlotMouseEvent) {
- event.event.preventDefault();
- /*
- * trace 0 keeps a transparent trace of closely positioned points used for calibration(backgroundTrace),
- * trace 1 keeps the data points used for calibration are automatically added to the same trace(dataPointTrace),
- * event.points will include all points near a mouse click, including those in the backgroundTrace and the dataPointTrace,
- * so the algorithm only looks at trace 0 since points from trace 1 are already put into the data set used for calibration.
- */
- const eligiblePoints = [];
- for (const point of event.points) {
- const traceNumber = point.curveNumber;
- if (traceNumber === 0) {
- eligiblePoints.push(point);
- }
- }
- const xValue = eligiblePoints[0].x as number;
- const yValue = eligiblePoints[0].y as number;
- const clickedPoint: CartesianPoint = {
- x: Number(xValue.toFixed(6)),
- y: Number(yValue.toFixed(6))
- };
- return clickedPoint;
-}
-
-export default connect(mapStateToProps)(Plot);
+}
\ No newline at end of file
diff --git a/src/client/app/components/maps/MapCalibrationComponent.tsx b/src/client/app/components/maps/MapCalibrationComponent.tsx
index 1d60d51cf..b6d2d42f2 100644
--- a/src/client/app/components/maps/MapCalibrationComponent.tsx
+++ b/src/client/app/components/maps/MapCalibrationComponent.tsx
@@ -3,53 +3,41 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import * as React from 'react';
-import MapCalibrationChartDisplayContainer from '../../containers/maps/MapCalibrationChartDisplayContainer';
-import MapCalibrationInfoDisplayContainer from '../../containers/maps/MapCalibrationInfoDisplayContainer';
-import MapCalibrationInitiateContainer from '../../containers/maps/MapCalibrationInitiateContainer';
-import MapsDetailContainer from '../../containers/maps/MapsDetailContainer';
+import { Navigate } from 'react-router-dom';
+import { useAppSelector } from '../../redux/reduxHooks';
+import { localEditsSlice } from '../../redux/slices/localEditsSlice';
import { CalibrationModeTypes } from '../../types/redux/map';
+import MapCalibrationChartDisplayComponent from './MapCalibrationChartDisplayComponent';
+import MapCalibrationInfoDisplayComponent from './MapCalibrationInfoDisplayComponent';
+import MapCalibrationInitiateComponent from './MapCalibrationInitiateComponent';
-interface MapCalibrationProps {
- mode: CalibrationModeTypes;
- isLoading: boolean;
- mapID: number;
-}
-
-export default class MapCalibrationComponent extends React.Component {
- constructor(props: any) {
- super(props);
- }
-
- public render() {
- if (this.props.mode === CalibrationModeTypes.initiate) {
- return (
-
- {/* TODO These types of plotly containers expect a lot of passed
- values and it gives a TS error. Given we plan to replace this
- with the react hooks version and it does not seem to cause any
- issues, this TS error is being suppressed for now.
- eslint-disable-next-line @typescript-eslint/ban-ts-comment
- @ts-ignore */}
-
-
-
+ );
- private handleChanges() {
- this.props.submitCalibratingMap();
- }
}
diff --git a/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx b/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx
index 1fcd0463b..dd6f0ba41 100644
--- a/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx
+++ b/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx
@@ -4,8 +4,11 @@
import * as React from 'react';
import { ChangeEvent } from 'react';
-import { FormattedMessage, WrappedComponentProps, injectIntl } from 'react-intl';
-import { logToServer } from '../../redux/actions/logs';
+import { FormattedMessage } from 'react-intl';
+import { logsApi } from '../../redux/api/logApi';
+import { useTranslate } from '../../redux/componentHooks';
+import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks';
+import { localEditsSlice } from '../../redux/slices/localEditsSlice';
import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map';
import { showErrorNotification } from '../../utils/notifications';
@@ -16,169 +19,163 @@ import { showErrorNotification } from '../../utils/notifications';
* Other configurations could also be selected during this phase;
*/
-interface MapInitiateProps {
- map: MapMetadata
- updateMapMode(nextMode: CalibrationModeTypes): any;
- onSourceChange(data: MapMetadata): any;
-}
-
-interface MapInitiateState {
- filename: string;
- mapName: string;
- angle: string;
-}
-
-type MapInitiatePropsWithIntl = MapInitiateProps & WrappedComponentProps;
-
-class MapCalibrationInitiateComponent extends React.Component {
- private readonly fileInput: any;
- private notifyBadNumber() {
- showErrorNotification(`${this.props.intl.formatMessage({id: 'map.bad.number'})}`);
- }
- private notifyBadDigit360() {
- showErrorNotification(`${this.props.intl.formatMessage({id: 'map.bad.digita'})}`);
- }
- private notifyBadDigit0() {
- showErrorNotification(`${this.props.intl.formatMessage({id: 'map.bad.digitb'})}`);
- }
- private notifyBadMapLoad() {
- showErrorNotification(`${this.props.intl.formatMessage({id: 'map.bad.load'})}`);
- }
- private notifyBadName() {
- showErrorNotification(`${this.props.intl.formatMessage({id: 'map.bad.name'})}`);
- }
-
- constructor(props: MapInitiatePropsWithIntl) {
- super(props);
- this.state = {
- filename: '',
- mapName: '',
- angle: ''
- };
- this.fileInput = React.createRef();
- this.handleInput = this.handleInput.bind(this);
- this.confirmUpload = this.confirmUpload.bind(this);
- this.handleNameInput = this.handleNameInput.bind(this);
- this.handleAngleInput = this.handleAngleInput.bind(this);
- this.handleAngle = this.handleAngle.bind(this);
- this.notifyBadNumber = this.notifyBadNumber.bind(this);
- this.notifyBadDigit360 = this.notifyBadDigit360.bind(this);
- this.notifyBadDigit0 = this.notifyBadDigit0.bind(this);
- this.notifyBadMapLoad = this.notifyBadMapLoad.bind(this);
- this.notifyBadName = this.notifyBadName.bind(this);
- }
-
- public render() {
- return (
-
- );
- }
-
- private async confirmUpload(event: any) {
- const bcheck = this.handleAngle(event);
+// interface MapInitiateProps {
+// map: MapMetadata
+// updateMapMode(nextMode: CalibrationModeTypes): any;
+// onSourceChange(data: MapMetadata): any;
+// }
+
+// interface MapInitiateState {
+// filename: string;
+// mapName: string;
+// angle: string;
+// }
+
+// type MapInitiatePropsWithIntl = MapInitiateProps & WrappedComponentProps;
+
+/**
+ * @returns TODO
+ */
+export default function MapCalibrationInitiateComponent() {
+ const translate = useTranslate();
+ const [logToServer] = logsApi.useLogToServerMutation();
+ const dispatch = useAppDispatch();
+ const [mapName, setMapName] = React.useState('');
+ const [angle, setAngle] = React.useState('');
+ const fileRef = React.useRef(null);
+ const mapData = useAppSelector(state => localEditsSlice.selectors.selectLocalEdit(state, localEditsSlice.selectors.selectCalibrationMapId(state)));
+ console.log('EmpyMapData>: ', mapData);
+
+ const notify = (key: 'map.bad.number' | 'map.bad.digita' | 'map.bad.digitb' | 'map.bad.load' | 'map.bad.name') => {
+ showErrorNotification(translate(key));
+ };
+ const confirmUpload = async (event: React.FormEvent) => {
+ const bcheck = handleAngle(event);
if (bcheck) {
- if (this.fileInput.current.files.length === 0) {
- this.notifyBadMapLoad();
+ if (!fileRef.current?.files || fileRef.current.files.length === 0) {
+ notify('map.bad.load');
}
- else if (this.state.mapName.trim() === '') {
- this.notifyBadName();
+ else if (mapName.trim() === '') {
+ notify('map.bad.name');
}
else {
- await this.handleInput(event);
- this.props.updateMapMode(CalibrationModeTypes.calibrate);
+ await processImgUpload(event);
}
}
- }
+ };
- private handleAngle(event: any) {
+ const handleAngle = (event: React.FormEvent) => {
event.preventDefault();
const pattern = /^[-+]?\d+(\.\d+)?$/;
- if (!pattern.test(this.state.angle)) {
- this.notifyBadNumber();
+ if (!pattern.test(angle)) {
+ notify('map.bad.number');
+
return false;
}
else {
- if (parseFloat(this.state.angle) > 360) {
- this.notifyBadDigit360();
+ if (parseFloat(angle) > 360) {
+ notify('map.bad.digita');
return false;
}
- else if (parseFloat(this.state.angle) < 0) {
- this.notifyBadDigit0();
+ else if (parseFloat(angle) < 0) {
+ notify('map.bad.digitb');
return false;
}
else {
return true;
}
}
- }
+ };
- private async handleInput(event: any) {
+ const processImgUpload = async (event: React.FormEvent) => {
event.preventDefault();
try {
- const imageURL = await this.getDataURL();
- this.setState({filename: this.fileInput.current.files[0].name});
- const image = new Image();
- image.src = imageURL;
- const source: MapMetadata = {
- ...this.props.map,
- name: this.state.mapName,
- filename: this.fileInput.current.files[0].name,
- image,
- northAngle: parseFloat(this.state.angle)
- };
- await this.props.onSourceChange(source);
+ const mapMetaData = await processImgMapMetaData();
+ dispatch(localEditsSlice.actions.setOneEdit(mapMetaData));
+ dispatch(localEditsSlice.actions.updateMapCalibrationMode({ id: mapData.id, mode: CalibrationModeTypes.calibrate }));
} catch (err) {
- logToServer('error', `Error, map source image uploading: ${err}`)();
+ logToServer({ level: 'error', message: `Error, map source image uploading: ${err}` });
}
- }
+ };
- private handleNameInput(event: ChangeEvent) {
- this.setState({
- mapName: event.target.value
- });
- }
+ const handleNameInput = (event: ChangeEvent) => { setMapName(event.target.value); };
- private handleAngleInput(event: React.FormEvent) {
- this.setState({
- angle: event.currentTarget.value
- });
- }
+ const handleAngleInput = (event: React.FormEvent) => { setAngle(event.currentTarget.value); };
- private getDataURL(): Promise {
+ // Takes image from upload, derives dimensions, and generates MapMetaData Object for redux state.
+ // No longer using Image element in Redux state for serializability purposes. Store img.src only.
+ const processImgMapMetaData = (): Promise => {
return new Promise((resolve, reject) => {
- const file = this.fileInput.current.files[0];
- const fileReader = new FileReader();
- fileReader.onloadend = () => {
- if (typeof fileReader.result === 'string') {
- resolve(fileReader.result);
- }
- };
- fileReader.onerror = reject;
- fileReader.readAsDataURL(file);
+ const file = fileRef.current?.files?.[0];
+ if (!file) {
+ reject('No File Found');
+
+ } else {
+
+ const fileReader = new FileReader();
+ // Fire when loading complete
+ fileReader.onloadend = () => {
+ // When file upload completed, use the result to create an image
+ // use image, to extract image dimensions;
+ if (typeof fileReader.result === 'string') {
+ img.src = fileReader.result;
+ }
+ };
+ fileReader.onerror = reject;
+ // begin file read
+ fileReader.readAsDataURL(file);
+ const img = new Image();
+ // Fire when image load complete.
+ img.onload = () => {
+ // resolve mapMetadata from image.
+ // Not storing image in state, instead extract relevant values
+ resolve({
+ ...mapData,
+ imgWidth: img.width,
+ imgHeight: img.height,
+ filename: file.name,
+ name: mapName,
+ northAngle: parseFloat(angle),
+ // Save the image source only
+ // Does not store the Image Obpect in redux for serializability reasons.
+ // use mapSource to recreate images when needed.
+ mapSource: img.src
+ });
+
+ };
+ // file when image error
+ img.onerror = error => {
+ reject(error);
+ };
+
+ }
+
});
- }
-}
+ };
-export default injectIntl(MapCalibrationInitiateComponent);
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx
index b8b91a698..f8e9d6650 100644
--- a/src/client/app/components/maps/MapViewComponent.tsx
+++ b/src/client/app/components/maps/MapViewComponent.tsx
@@ -2,436 +2,71 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-import * as moment from 'moment';
+import { parseZone } from 'moment';
import * as React from 'react';
-import { FormattedMessage, injectIntl, WrappedComponentProps } from 'react-intl';
-import { Link } from 'react-router-dom';
-import { Button } from 'reactstrap';
-import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map';
-import { showErrorNotification } from '../../utils/notifications';
-import { hasToken } from '../../utils/token';
-
+import { FormattedMessage } from 'react-intl';
+import { selectMapById } from '../../redux/api/mapsApi';
+import { useAppSelector } from '../../redux/reduxHooks';
+import { localEditsSlice } from '../../redux/slices/localEditsSlice';
+import '../../styles/card-page.css';
+import translate from '../../utils/translate';
+import EditMapModalComponent from './EditMapModalComponent';
interface MapViewProps {
- // The ID of the map to be displayed
- id: number;
- // The map metadata being displayed by this row
- map: MapMetadata;
- isEdited: boolean;
- isSubmitting: boolean;
- // The function used to dispatch the action to edit map details
- editMapDetails(map: MapMetadata): any;
- setCalibration(mode: CalibrationModeTypes, mapID: number): any;
- removeMap(id: number): any;
-}
-
-interface MapViewState {
- nameFocus: boolean;
- nameInput: string;
- circleFocus: boolean;
- circleInput: string;
- noteFocus: boolean;
- noteInput: string;
+ mapID: number;
}
-type MapViewPropsWithIntl = MapViewProps & WrappedComponentProps;
-
-class MapViewComponent extends React.Component {
- constructor(props: MapViewPropsWithIntl) {
- super(props);
- this.state = {
- nameFocus: false,
- nameInput: this.props.map.name,
- noteFocus: false,
- noteInput: (this.props.map.note) ? this.props.map.note : '',
- circleFocus: false,
- // circleSize should always be a valid string due to how stored and mapRow.
- circleInput: this.props.map.circleSize.toString()
- };
- this.handleCalibrationSetting = this.handleCalibrationSetting.bind(this);
- this.toggleMapDisplayable = this.toggleMapDisplayable.bind(this);
- this.toggleNameInput = this.toggleNameInput.bind(this);
- this.handleNameChange = this.handleNameChange.bind(this);
- this.toggleNoteInput = this.toggleNoteInput.bind(this);
- this.handleNoteChange = this.handleNoteChange.bind(this);
- this.toggleDelete = this.toggleDelete.bind(this);
- this.notifyCalibrationNeeded = this.notifyCalibrationNeeded.bind(this);
- this.handleSizeChange = this.handleSizeChange.bind(this);
- this.toggleCircleInput = this.toggleCircleInput.bind(this);
- }
-
- public render() {
- return (
-
-
{this.props.map.id} {this.formatStatus()}
-
{this.formatName()}
- {hasToken() &&
{this.formatDisplayable()}
}
- {hasToken() &&
{this.formatCircleSize()}
}
- {/* This was stored as UTC but with the local time at that point.
- Thus, moment will not modify the date/time given when done this way. */}
- {hasToken() &&