Skip to content

Feature: validate image project #1038

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docker-compose.tc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ x-mapswipe-workers: &base_mapswipe_workers
SLACK_CHANNEL: '${SLACK_CHANNEL}'
SENTRY_DSN: '${SENTRY_DSN}'
OSMCHA_API_KEY: '${OSMCHA_API_KEY}'
MAPILLARY_API_KEY: '${MAPILLARY_API_KEY}'
depends_on:
- postgres
volumes:
Expand Down
2 changes: 2 additions & 0 deletions manager-dashboard/app/Base/configs/projectTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
PROJECT_TYPE_CHANGE_DETECTION,
PROJECT_TYPE_STREET,
PROJECT_TYPE_COMPLETENESS,
PROJECT_TYPE_VALIDATE_IMAGE,
} from '#utils/common';

const PROJECT_CONFIG_NAME = process.env.REACT_APP_PROJECT_CONFIG_NAME as string;
Expand All @@ -15,6 +16,7 @@ const mapswipeProjectTypeOptions: {
}[] = [
{ value: PROJECT_TYPE_BUILD_AREA, label: 'Find' },
{ value: PROJECT_TYPE_FOOTPRINT, label: 'Validate' },
{ value: PROJECT_TYPE_VALIDATE_IMAGE, label: 'Validate Image' },
{ value: PROJECT_TYPE_CHANGE_DETECTION, label: 'Compare' },
{ value: PROJECT_TYPE_STREET, label: 'Street' },
{ value: PROJECT_TYPE_COMPLETENESS, label: 'Completeness' },
Expand Down
1 change: 1 addition & 0 deletions manager-dashboard/app/Base/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ p {
--height-mobile-preview-builarea-content: 30rem;
--height-mobile-preview-footprint-content: 22rem;
--height-mobile-preview-change-detection-content: 14rem;
--height-mobile-preview-validate-image-content: 22rem;

--radius-popup-border: 0.25rem;
--radius-scrollbar-border: 0.25rem;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { _cs } from '@togglecorp/fujs';

import RawButton, { Props as RawButtonProps } from '../../RawButton';
import { ymdToDateString, typedMemo } from '../../../utils/common.tsx';
import { ymdToDateString, typedMemo } from '../../../utils/common';

import styles from './styles.css';

Expand Down
2 changes: 1 addition & 1 deletion manager-dashboard/app/components/Calendar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import Button from '../Button';
import NumberInput from '../NumberInput';
import SelectInput from '../SelectInput';
import useInputState from '../../hooks/useInputState';
import { typedMemo } from '../../utils/common.tsx';
import { typedMemo } from '../../utils/common';

import CalendarDate, { Props as CalendarDateProps } from './CalendarDate';

Expand Down
80 changes: 80 additions & 0 deletions manager-dashboard/app/components/CocoFileInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React from 'react';
import * as t from 'io-ts';
import { isRight } from 'fp-ts/Either';

import JsonFileInput, { Props as JsonFileInputProps } from '#components/JsonFileInput';

const Image = t.type({
id: t.number,
// width: t.number,
// height: t.number,
file_name: t.string,
// license: t.union([t.number, t.undefined]),
flickr_url: t.union([t.string, t.undefined]),
coco_url: t.union([t.string, t.undefined]),
// date_captured: DateFromISOString,
});

const CocoDataset = t.type({
// info: Info,
// licenses: t.array(License),
images: t.array(Image),
// annotations: t.array(Annotation),
// categories: t.array(Category)
});
export type CocoDatasetType = t.TypeOf<typeof CocoDataset>

interface Props<N> extends Omit<JsonFileInputProps<N, object>, 'onChange' | 'value'> {
value: CocoDatasetType | undefined;
maxLength: number;
onChange: (newValue: CocoDatasetType | undefined, name: N) => void;
}
function CocoFileInput<N>(props: Props<N>) {
const {
name,
onChange,
error,
maxLength,
...otherProps
} = props;

const [
internalErrorMessage,
setInternalErrorMessage,
] = React.useState<string>();

const handleChange = React.useCallback(
(val) => {
const result = CocoDataset.decode(val);
if (!isRight(result)) {
// eslint-disable-next-line no-console
console.error('Invalid COCO format', result.left);
setInternalErrorMessage('Invalid COCO format');
return;
}
if (result.right.images.length > maxLength) {
setInternalErrorMessage(`Too many images ${result.right.images.length} uploaded. Please do not exceed ${maxLength} images.`);
return;
}
const uniqueIdentifiers = new Set(result.right.images.map((item) => item.id));
if (uniqueIdentifiers.size < result.right.images.length) {
setInternalErrorMessage('Each image should have a unique id.');
return;
}
setInternalErrorMessage(undefined);
onChange(result.right, name);
},
[onChange, maxLength, name],
);

return (
<JsonFileInput
name={name}
onChange={handleChange}
error={internalErrorMessage ?? error}
{...otherProps}
/>
);
}

export default CocoFileInput;
2 changes: 1 addition & 1 deletion manager-dashboard/app/components/DateRangeInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import Button from '../Button';
import Popup from '../Popup';
import Calendar, { Props as CalendarProps } from '../Calendar';
import CalendarDate, { Props as CalendarDateProps } from '../Calendar/CalendarDate';
import { ymdToDateString, dateStringToDate } from '../../utils/common.tsx';
import { ymdToDateString, dateStringToDate } from '../../utils/common';

import {
predefinedDateRangeOptions,
Expand Down
2 changes: 1 addition & 1 deletion manager-dashboard/app/components/InputSection/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
display: flex;
flex-direction: column;
border-radius: var(--radius-card-border);
gap: var(--spacing-extra-large);
gap: var(--spacing-large);
background-color: var(--color-foreground);
padding: var(--spacing-large);
min-height: 14rem;
Expand Down
2 changes: 1 addition & 1 deletion manager-dashboard/app/components/JsonFileInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function readUploadedFileAsText(inputFile: File) {
const ONE_MB = 1024 * 1024;
const DEFAULT_MAX_FILE_SIZE = ONE_MB;

interface Props<N, T> extends Omit<FileInputProps<N>, 'value' | 'onChange' | 'accept'> {
export interface Props<N, T> extends Omit<FileInputProps<N>, 'value' | 'onChange' | 'accept'> {
maxFileSize?: number;
value: T | undefined | null;
onChange: (newValue: T | undefined, name: N) => void;
Expand Down
4 changes: 3 additions & 1 deletion manager-dashboard/app/utils/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@ export const PROJECT_TYPE_FOOTPRINT = 2;
export const PROJECT_TYPE_CHANGE_DETECTION = 3;
export const PROJECT_TYPE_COMPLETENESS = 4;
export const PROJECT_TYPE_STREET = 7;
export const PROJECT_TYPE_VALIDATE_IMAGE = 10;

export type ProjectType = 1 | 2 | 3 | 4 | 7;
export type ProjectType = 1 | 2 | 3 | 4 | 7 | 10;

export const projectTypeLabelMap: {
[key in ProjectType]: string
Expand All @@ -77,6 +78,7 @@ export const projectTypeLabelMap: {
[PROJECT_TYPE_CHANGE_DETECTION]: 'Compare',
[PROJECT_TYPE_COMPLETENESS]: 'Completeness',
[PROJECT_TYPE_STREET]: 'Street',
[PROJECT_TYPE_VALIDATE_IMAGE]: 'Validate Image',
};

export type IconKey = 'add-outline'
Expand Down
73 changes: 73 additions & 0 deletions manager-dashboard/app/views/NewProject/ImageInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react';

import {
SetValueArg,
Error,
useFormObject,
getErrorObject,
} from '@togglecorp/toggle-form';
import TextInput from '#components/TextInput';

import {
ImageType,
} from '../utils';

import styles from './styles.css';

const defaultImageValue: ImageType = {
sourceIdentifier: '<unique-identifier>',
};

interface Props {
value: ImageType;
onChange: (value: SetValueArg<ImageType>, index: number) => void | undefined;
index: number;
error: Error<ImageType> | undefined;
disabled?: boolean;
readOnly?: boolean;
}

export default function ImageInput(props: Props) {
const {
value,
onChange,
index,
error: riskyError,
disabled,
readOnly,
} = props;

const onImageChange = useFormObject(index, onChange, defaultImageValue);

const error = getErrorObject(riskyError);

return (
<div className={styles.imageInput}>
<TextInput
label="Identifier"
value={value?.sourceIdentifier}
name={'sourceIdentifier' as const}
// onChange={onImageChange}
error={error?.sourceIdentifier}
disabled={disabled}
readOnly
/>
<TextInput
label="File name"
value={value.fileName}
name={'fileName' as const}
onChange={onImageChange}
error={error?.fileName}
disabled={disabled || readOnly}
/>
<TextInput
label="URL"
value={value?.url}
name={'url' as const}
onChange={onImageChange}
error={error?.url}
disabled={disabled || readOnly}
/>
</div>
);
}
5 changes: 5 additions & 0 deletions manager-dashboard/app/views/NewProject/ImageInput/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.image-input {
display: flex;
flex-direction: column;
gap: var(--spacing-medium);
}
Loading