Skip to content
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

EditDialog: add new member to relation #847

Merged
merged 5 commits into from
Jan 7, 2025
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { EditDataItem, Members } from '../useEditItems';
import { getApiId, getShortId } from '../../../../services/helpers';
import { getOsmElement } from '../../../../services/osmApi';
import { useEditContext } from '../EditContext';
import { useFeatureEditData } from './FeatureEditSection/SingleFeatureEditContext';
import React from 'react';
import { getNewNode } from '../../../../services/getCoordsFeature';
import { Button, TextField } from '@mui/material';
import { OsmId } from '../../../../services/types';

const hasAtLeastOneNode = (members: Members) => {
return members.some((member) => member.shortId.startsWith('n'));
};

const getLastNodeApiId = (members: Members) => {
const lastNode = members
.toReversed()
.find((member) => member.shortId.startsWith('n'));
return lastNode ? getApiId(lastNode.shortId) : null;
};

const findItem = (items: EditDataItem[], osmId: OsmId) =>
items.find((item) => item.shortId === getShortId(osmId));

const getLastNodeLocation = async (osmId: OsmId, items: EditDataItem[]) => {
if (osmId.id < 0) {
return findItem(items, osmId)?.newNodeLonLat;
}
const element = await getOsmElement(osmId);
return [element.lon, element.lat];
};

const getNewNodeLocation = async (items: EditDataItem[], members: Members) => {
const osmId = getLastNodeApiId(members);
if (!osmId) {
throw new Error('No node found');
}
const lonLat = await getLastNodeLocation(osmId, items);
return lonLat.map((x) => x + 0.0001);
};

export const AddMemberForm = () => {
const { addFeature, items } = useEditContext();
const { members, setMembers } = useFeatureEditData();
const [showInput, setShowInput] = React.useState(false);
const [label, setLabel] = React.useState('');

if (!hasAtLeastOneNode(members)) {
return; // TODO so far, we need a node (with coordinates) for adding a new node
}

const handleAddMember = async () => {
const lastNodeLocation = await getNewNodeLocation(items, members);
const newNode = getNewNode(lastNodeLocation, label);
addFeature(newNode);
setMembers((prev) => [
...prev,
{ shortId: getShortId(newNode.osmMeta), role: '', label },
]);
setShowInput(false);
setLabel('');
};

return (
<>
{showInput ? (
<>
<TextField
value={label}
size="small"
label="Name"
onChange={(e) => {
setLabel(e.target.value);
}}
/>
<Button onClick={handleAddMember} variant="text">
Add node
</Button>
</>
) : (
<Button onClick={() => setShowInput(true)} variant="text">
Add
</Button>
)}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { FeatureRow } from './FeatureRow';
import { t } from '../../../../services/intl';
import { useGetHandleClick } from './helpers';
import { AddMemberForm } from './AddMemberForm';

export const MembersEditor = () => {
const { members } = useFeatureEditData();
Expand Down Expand Up @@ -53,6 +54,8 @@ export const MembersEditor = () => {
/>
);
})}

<AddMemberForm />
</List>
</AccordionDetails>
</Accordion>
Expand Down
78 changes: 68 additions & 10 deletions src/components/FeaturePanel/EditDialog/useEditItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ import { publishDbgObject } from '../../../utils';

export type TagsEntries = [string, string][];

export type Members = Array<{
shortId: string;
role: string;
label: string; // cached from other dataItems, or from originalFeature
}>;

// internal type stored in the state
type DataItem = {
shortId: string;
tagsEntries: TagsEntries;
toBeDeleted: boolean;
members: {
shortId: string;
role: string;
label: string; // cached from other dataItems, or from originalFeature
}[];
members: Members | undefined;
version: number | undefined; // undefined for new item
newNodeLonLat?: LonLat;
};
Expand All @@ -26,7 +28,7 @@ export type EditDataItem = DataItem & {
tags: FeatureTags;
setTag: (k: string, v: string) => void;
toggleToBeDeleted: () => void;
// TODO add setMembers,
setMembers: SetMembers;
};

const buildDataItem = (feature: Feature): DataItem => {
Expand All @@ -51,13 +53,57 @@ const buildDataItem = (feature: Feature): DataItem => {
};
};

const getName = (d: DataItem): string | undefined =>
d.tagsEntries.find(([k]) => k === 'name')?.[1];

const someNameHasChanged = (prevData: DataItem[], newData: DataItem[]) => {
const prevNames = prevData.map((d) => getName(d));
const newNames = newData.map((d) => getName(d));
return prevNames.some((name, index) => name !== newNames[index]);
};

const updateAllMemberLabels = (newData: DataItem[], shortId: string) => {
// TODO this code is ugly, but we would have to remove the "one state"
const referencingParents = new Set<string>();
newData.forEach((dataItem) => {
dataItem.members?.forEach((member) => {
if (member.shortId === shortId) {
referencingParents.add(dataItem.shortId);
}
});
});

const currentItem = newData.find((dataItem) => dataItem.shortId === shortId);

return newData.map((dataItem) => {
if (referencingParents.has(dataItem.shortId)) {
const clone = JSON.parse(JSON.stringify(dataItem)) as DataItem;
const index = clone.members.findIndex(
(member) => member.shortId === shortId,
);
clone.members[index].label = getName(currentItem);
return clone;
} else {
return dataItem;
}
});
};

type SetDataItem = (updateFn: (prevValue: DataItem) => DataItem) => void;
const setDataItemFactory =
(setData: Setter<DataItem[]>, shortId: string): SetDataItem =>
(updateFn) => {
setData((prev) =>
prev.map((item) => (item.shortId === shortId ? updateFn(item) : item)),
);
setData((prevData) => {
const newData = prevData.map((item) =>
item.shortId === shortId ? updateFn(item) : item,
);

if (someNameHasChanged(prevData, newData)) {
// only current item can change, but this check is cheap
return updateAllMemberLabels(newData, shortId);
}
return newData;
});
};

type SetTagsEntries = (updateFn: (prev: TagsEntries) => TagsEntries) => void;
Expand All @@ -69,6 +115,15 @@ const setTagsEntriesFactory =
tagsEntries: updateFn(tagsEntries),
}));

type SetMembers = (updateFn: (prev: Members) => Members) => void;
const setMembersFactory =
(setDataItem: SetDataItem, members: Members): SetMembers =>
(updateFn) =>
setDataItem((prev) => ({
...prev,
members: updateFn(members),
}));

type SetTag = (k: string, v: string) => void;
const setTagFactory =
(setTagsEntries: SetTagsEntries): SetTag =>
Expand Down Expand Up @@ -97,16 +152,19 @@ export const useEditItems = (originalFeature: Feature) => {
const items = useMemo<Array<EditDataItem>>(
() =>
data.map((dataItem) => {
const { shortId, tagsEntries } = dataItem;
const { shortId, tagsEntries, members } = dataItem;
const setDataItem = setDataItemFactory(setData, shortId);
const setTagsEntries = setTagsEntriesFactory(setDataItem, tagsEntries);
const setMembers = setMembersFactory(setDataItem, members);
return {
...dataItem,
setTagsEntries,
tags: Object.fromEntries(tagsEntries),
setTag: setTagFactory(setTagsEntries),
toggleToBeDeleted: toggleToBeDeletedFactory(setDataItem),
setMembers,
};
// TODO maybe keep reference to original EditDataItem if DataItem didnt change? #performance
}),
[data],
);
Expand Down
18 changes: 17 additions & 1 deletion src/services/getCoordsFeature.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getImagesFromCenter } from './images/getImageDefs';
import { Feature, LonLatRounded, OsmType } from './types';
import { Feature, LonLat, LonLatRounded, OsmType } from './types';

let nextId = 0;

Expand All @@ -21,3 +21,19 @@ export const getCoordsFeature = ([lon, lat]: LonLatRounded): Feature => {
imageDefs: getImagesFromCenter({}, center),
};
};

export const getNewNode = ([lon, lat]: LonLat, name: string): Feature => {
nextId += 1;

return {
type: 'Feature',
point: true,
center: [lon, lat],
osmMeta: {
type: 'node',
id: nextId * -1, // negative id means "adding new point" in osmApiAuth#saveChanges()
},
tags: { name },
properties: { class: 'marker', subclass: 'point' },
};
};
1 change: 1 addition & 0 deletions src/services/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getIdFromShortener, getShortenerSlug } from './shortener';

type Xml2JsOsmItem = {
tag: { $: { k: string; v: string } }[];
member?: { $: { type: string; ref: string; role: string } }[];
$: {
id: string;
visible: string;
Expand Down
2 changes: 1 addition & 1 deletion src/services/osmApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ type OsmResponse = {
elements: OsmElement[];
};

const getOsmElement = async (apiId: OsmId) => {
export const getOsmElement = async (apiId: OsmId) => {
const { elements } = await fetchJson<OsmResponse>(getOsmUrl(apiId)); // TODO 504 gateway busy
return elements?.[0];
};
Expand Down
Loading
Loading