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

[Basemap-switcher] Use a image based basemap switcher #362

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

artmarks
Copy link
Contributor

See #361. Here is an example to show a image basemap switcher. I adjusted the test-basemap-switcher app to show the difference. Feedback and adjustments are welcome :)

Copy link

changeset-bot bot commented Sep 17, 2024

🦋 Changeset detected

Latest commit: 298fc6f

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@arnevogt arnevogt self-assigned this Sep 26, 2024
@@ -116,30 +152,60 @@ export const BasemapSwitcher: FC<BasemapSwitcherProps> = (props) => {
return (
<Box {...containerProps}>
{map ? (
<Select<SelectOption>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it better do have a separate component ImageBaseampSwitcher in the same package.
That would make the code more comprehensible and avoid the problem that some of the component's props only apply to either default or image mode.

console.error("Failed to find a layer");
return;
}
map?.layers.activateBaseLayer(layer.id);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual logic to switch the base map should be inside the basemap switcher.
I think the (Image)BasemapSwitcher should only get the configuration (e.g. which thumbnail for which basemap) through the component properties. The state (selection) and the actual code to switch the basemap belong to the component itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a previous talk, we wanted to combine them, but I think it would make more sense to add the ImageBaseampSwitcher as a separate component.

The switch-logic is intentionally outside, because in our case not all basemaps should be available. There is a map-model logic, that says: if appstate A: then basemaps B1 & B2 are selectable, but not B3, else if state [...]. This opens the question if this is maybe to spceial for the general use case, or something that we want to include. If it's too special i would agree to hide the switching logic to the component itself

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a simple basemapFilter predicate (return false for basemaps that should be excluded) would be sufficient?

const imageBasemapSwitcher = useMemo(() => {
return {
selectedImageLabel: imageLabel,
choosableImageLabel: currentSelection
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename to selectableImageLabel?

@arnevogt
Copy link
Member

@artmarks

Thanks for creating this PR.

I just had a look on your updates. Generally, I think this is quite nice and should be part of the base packages.
In my opinion there are a few points that can be improved before we merge it:

  • it would be cleaner to have two separate components (BasemapSwitcher and ImageBasemapSwitcher)
    • solves the issue that some of the comp. property only apply to either default or image mode
    • both should be in the same package
    • the hooks (like useBasemap) and the actual logic can be refactored and moved to a new, separate file
  • the actual code to switch the basemap should remain part of the component (and not in the properties)
    • currently you call map?.layers.activateBaseLayer(layer.id) and save the current selection in AppUI.tsx
  • configuration of thumbnails and labels
    • this could be a simple mapping from basemap id to image in the properties
    • ideally the thumbnail could be part of the layer itself
      • this is how it is done for the legend
        • each layer can provide a legend image or custom component in the layer attributes
        • see [0] and [1]
  • added/removed basemaps should be reflected in the UI
    • the behavior is better aligned with the current default basemap switcher
    • it is sufficient to display the label or a default thumbnail if nothing else is provided in the properties
      • if you really do not want to update the UI when the map model changes a flag syncWithMapModel could be introduced in the properties

[0] https://github.com/open-pioneer/trails-openlayers-base-packages/blob/main/src/samples/showcase/showcase-app/MapConfigProviderImpl.ts#L110
[1] https://github.com/open-pioneer/trails-openlayers-base-packages/blob/main/src/samples/showcase/showcase-app/CustomLegendItems.tsx

…ntern handling of baselayer, config of thumbnails
…ntern handling of baselayer, config of thumbnails
@artmarks
Copy link
Contributor Author

Thanks @arnevogt for your feedback. I have worked on the open points and i think we have a more common solutions.
There are still some open points i wanted to adress:

  • moving the useBaseLayers function to a more general place. Is there are common best practice in OP to move such a function
  • the current used default and example images are free to use and with the specifc link to the source, but are most likely not the most fitting. Are there any OP default images, that we could use ?

I'm still open for any improvements :)

@mbeckem
Copy link
Contributor

mbeckem commented Oct 23, 2024

@artmarks

* moving the `useBaseLayers` function to a more general place. Is there are common best practice in OP to move such a function

Just FYI useBaserLayers and similar hooks will become trivial with the full migration to the reactivity API (which is incoming). It's probably not necessary to share them in a central place. See

function useBaseLayers(mapModel: MapModel | undefined): Layer[] {

Sharing the hook in a shared file within the package could still be done, of course. But it's not deserving a real API.

* Alternatively, the image and label can be set in the layer attributes.
* If no images are set, the default image will be used.
*/
imageMap?: Map<string, BaselayerImageBasemapSwitcherProps>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using Map<> in React is not ideal:
Maps are mutable, if someone updates the Map (set, delete) from the outside this will not trigger a re-render of the component.
I suggest using a immutable type like Record, Array or a custom (immutable) object.

const { src, label, callback, width, height } = props;
return (
<MenuItem onClick={() => callback()}>
<Tooltip label={label}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thumbs up if aria-labels are supported

?.imageBasemapSwitcher as BaselayerImageBasemapSwitcherProps;
const parameter = imageMap?.get(selectedLayer?.id || NO_BASEMAP_ID);
const image =
basemap?.image || parameter?.image || (!selectedLayer ? emptyImage : defaultImage);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be more intuitive if parameter.image has precedence.
Typically (e.g. with CSS), properties directly on the component have precedents over definitions elsewhere.

const parameter = imageMap?.get(selectedLayer?.id || NO_BASEMAP_ID);
const image =
basemap?.image || parameter?.image || (!selectedLayer ? emptyImage : defaultImage);
const label =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

regarding precedence see comment above

Comment on lines +168 to +197
function useBaseLayers(mapModel: MapModel | undefined): Layer[] {
// Caches potentially expensive layers arrays.
// Not sure if this is a good idea, but getSnapshot() should always be fast.
// If this is a no-go, make getAllLayers() fast instead.
const baseLayers = useRef<Layer[] | undefined>();
const subscribe = useCallback(
(cb: () => void) => {
// Reset cache when (re-) subscribing
baseLayers.current = undefined;

if (!mapModel) {
return () => undefined;
}
const resource = mapModel.layers.on("changed", () => {
// Reset cache content so getSnapshot() fetches basemaps again.
baseLayers.current = undefined;
cb();
});
return () => resource.destroy();
},
[mapModel]
);
const getSnapshot = useCallback(() => {
if (baseLayers.current) {
return baseLayers.current;
}
return (baseLayers.current = mapModel?.layers.getBaseLayers() ?? []);
}, [mapModel]);
return useSyncExternalStore(subscribe, getSnapshot);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could hugely benefit from using the Reactivty API (see Comment).
If I am not mistake, caching current base layers and filtering + memoizing image labels could be packed into a single use of useReactiveSnapshot.

Comment on lines +110 to +137
const { selectedImageLabel, choosableImageLabel } = useMemo(() => {
const activeBaseLayer = map?.layers.getActiveBaseLayer();
const selectedLayer = baseLayers.find((l) => l === activeBaseLayer);

const selectedImageLabel = setImageLabelObject(selectedLayer);

const choosableImageLabel = [] as ImageLabelSwitchObject[];
baseLayers.forEach((layer) => {
if (
excludeBasemapWithIdFilter &&
!excludeBasemapWithIdFilter.includes(layer.id || "")
) {
return;
}
choosableImageLabel.push(setImageLabelObject(layer));
});
if (allowSelectingEmptyBasemap) {
choosableImageLabel.push(setImageLabelObject(undefined));
}

return { selectedImageLabel, choosableImageLabel };
}, [
map?.layers,
baseLayers,
setImageLabelObject,
allowSelectingEmptyBasemap,
excludeBasemapWithIdFilter
]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comment at useBaseLayers

/**
* Optional filter to exclude basemaps from the switcher.
*/
excludeBasemapWithIdFilter?: string[];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose using a filter function instead of a list of IDs.
This would allow greater flexibility.

@arnevogt
Copy link
Member

@artmarks
Thanks a lot for the update. I think this is almost there.
I added a few comments directly on the corresponding lines of code.
Of cause, a few unit test would be nice as well.

I will check how best to deal with the default thumbnails.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants