Skip to content

Commit

Permalink
EditDialog: add new member to relation (#847)
Browse files Browse the repository at this point in the history
Co-authored-by: Pavel Zbytovský <[email protected]>
  • Loading branch information
jvaclavik and zbycz authored Jan 7, 2025
1 parent 1bdcfab commit 6ff742c
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 53 deletions.
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

0 comments on commit 6ff742c

Please sign in to comment.