Merge pull request #670 from acelaya-forks/feature/rtl

Feature/rtl
This commit is contained in:
Alejandro Celaya 2022-06-11 18:40:40 +02:00 committed by GitHub
commit bcd3fa8ce4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 638 additions and 112 deletions

View file

@ -235,7 +235,7 @@ export const VisitsStats: FC<VisitsStatsProps> = ({
stats={cities}
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'city')}
highlightedLabel={highlightedLabel}
extraHeaderContent={(activeCities: string[]) => mapLocations.length > 0 && (
extraHeaderContent={(activeCities) => mapLocations.length > 0 && (
<OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
)}
sortingItems={{

View file

@ -1,4 +1,4 @@
import { FC, useState } from 'react';
import { FC, ReactNode, useState } from 'react';
import { fromPairs, pipe, reverse, sortBy, splitEvery, toLower, toPairs, type, zipObj } from 'ramda';
import { rangeOf } from '../../utils/utils';
import { Order } from '../../utils/helpers/ordering';
@ -14,7 +14,7 @@ interface SortableBarChartCardProps extends Omit<HorizontalBarChartProps, 'max'>
title: Function | string;
sortingItems: Record<string, string>;
withPagination?: boolean;
extraHeaderContent?: Function;
extraHeaderContent?: (activeCities?: string[]) => ReactNode;
}
const toLowerIfString = (value: any) => (type(value) === 'String' ? toLower(value) : value);

View file

@ -40,7 +40,7 @@ export const MapModal = ({ toggle, isOpen, title, locations = [] }: MapModalProp
<ModalBody className="map-modal__modal-body">
<h3 className="map-modal__modal-title">
{title}
<button type="button" className="btn-close float-end" onClick={toggle} />
<button type="button" className="btn-close float-end" aria-label="Close" onClick={toggle} />
</h3>
<MapContainer {...calculateMapProps(locations)}>
<OpenStreetMapTile />

View file

@ -9,7 +9,7 @@ import './OpenMapModalBtn.scss';
interface OpenMapModalBtnProps {
modalTitle: string;
activeCities: string[];
activeCities?: string[];
locations?: CityStats[];
}
@ -19,7 +19,9 @@ export const OpenMapModalBtn = ({ modalTitle, activeCities, locations = [] }: Op
const [locationsToShow, setLocationsToShow] = useState<CityStats[]>([]);
const id = useDomId();
const filterLocations = (cities: CityStats[]) => cities.filter(({ cityName }) => activeCities.includes(cityName));
const filterLocations = (cities: CityStats[]) => (
!activeCities ? cities : cities.filter(({ cityName }) => activeCities?.includes(cityName))
);
const onClick = () => {
if (!activeCities) {
setLocationsToShow(locations);

View file

@ -9,7 +9,7 @@ describe('<SimplePaginator />', () => {
it.each([-3, -2, 0, 1])('renders empty when the amount of pages is smaller than 2', (pagesCount) => {
const { container } = setUp(pagesCount);
expect(container.firstChild).toEqual(null);
expect(container.firstChild).toBeNull();
});
describe('ELLIPSIS are rendered where expected', () => {

View file

@ -14,7 +14,7 @@ import { NonReachableServer, NotFoundServer, RegularServer } from '../../../src/
describe('selectedServerReducer', () => {
describe('reducer', () => {
it('returns default when action is RESET_SELECTED_SERVER', () =>
expect(reducer(null, { type: RESET_SELECTED_SERVER, selectedServer: null })).toEqual(null));
expect(reducer(null, { type: RESET_SELECTED_SERVER, selectedServer: null })).toBeNull());
it('returns selected server when action is SELECT_SERVER', () => {
const selectedServer = Mock.of<RegularServer>({ id: 'abc123' });

View file

@ -1,38 +1,29 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { faCaretDown as caretDownIcon, faCaretUp as caretUpIcon } from '@fortawesome/free-solid-svg-icons';
import { render } from '@testing-library/react';
import { TableOrderIcon } from '../../../src/utils/table/TableOrderIcon';
import { OrderDir } from '../../../src/utils/helpers/ordering';
describe('<TableOrderIcon />', () => {
let wrapper: ShallowWrapper;
const createWrapper = (field: string, currentDir?: OrderDir, className?: string) => {
wrapper = shallow(
<TableOrderIcon currentOrder={{ dir: currentDir, field: 'foo' }} field={field} className={className} />,
);
return wrapper;
};
afterEach(() => wrapper?.unmount());
const setUp = (field: string, currentDir?: OrderDir, className?: string) => render(
<TableOrderIcon currentOrder={{ dir: currentDir, field: 'foo' }} field={field} className={className} />,
);
it.each([
['foo', undefined],
['bar', 'DESC' as OrderDir],
['bar', 'ASC' as OrderDir],
])('renders empty when not all conditions are met', (field, dir) => {
const wrapper = createWrapper(field, dir);
expect(wrapper.html()).toEqual(null);
const { container } = setUp(field, dir);
expect(container.firstChild).toBeNull();
});
it.each([
['DESC' as OrderDir, caretDownIcon],
['ASC' as OrderDir, caretUpIcon],
])('renders an icon when all conditions are met', (dir, expectedIcon) => {
const wrapper = createWrapper('foo', dir);
['DESC' as OrderDir],
['ASC' as OrderDir],
])('renders an icon when all conditions are met', (dir) => {
const { container } = setUp('foo', dir);
expect(wrapper.html()).not.toEqual(null);
expect(wrapper.prop('icon')).toEqual(expectedIcon);
expect(container.firstChild).not.toBeNull();
expect(container.firstChild).toMatchSnapshot();
});
it.each([
@ -40,8 +31,7 @@ describe('<TableOrderIcon />', () => {
['foo', 'foo'],
['bar', 'bar'],
])('renders expected classname', (className, expectedClassName) => {
const wrapper = createWrapper('foo', 'ASC', className);
expect(wrapper.prop('className')).toEqual(expectedClassName);
const { container } = setUp('foo', 'ASC', className);
expect(container.firstChild).toHaveClass(expectedClassName);
});
});

View file

@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<TableOrderIcon /> renders an icon when all conditions are met 1`] = `
<svg
aria-hidden="true"
class="svg-inline--fa fa-caret-down ms-1"
data-icon="caret-down"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 320 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M310.6 246.6l-127.1 128C176.4 380.9 168.2 384 160 384s-16.38-3.125-22.63-9.375l-127.1-128C.2244 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75S319.8 237.5 310.6 246.6z"
fill="currentColor"
/>
</svg>
`;
exports[`<TableOrderIcon /> renders an icon when all conditions are met 2`] = `
<svg
aria-hidden="true"
class="svg-inline--fa fa-caret-up ms-1"
data-icon="caret-up"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 320 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.39 265.4l127.1-128C143.6 131.1 151.8 128 160 128s16.38 3.125 22.63 9.375l127.1 128c9.156 9.156 11.9 22.91 6.943 34.88S300.9 320 287.1 320H32.01c-12.94 0-24.62-7.781-29.58-19.75S.2333 274.5 9.39 265.4z"
fill="currentColor"
/>
</svg>
`;

View file

@ -1,14 +1,9 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Marker, Popup } from 'react-leaflet';
import { Modal } from 'reactstrap';
import { render, screen } from '@testing-library/react';
import { MapModal } from '../../../src/visits/helpers/MapModal';
import { CityStats } from '../../../src/visits/types';
describe('<MapModal />', () => {
let wrapper: ShallowWrapper;
const toggle = () => '';
const isOpen = true;
const title = 'Foobar';
const toggle = jest.fn();
const zaragozaLat = 41.6563497;
const zaragozaLong = -0.876566;
const newYorkLat = 40.730610;
@ -26,36 +21,8 @@ describe('<MapModal />', () => {
},
];
beforeEach(() => {
wrapper = shallow(<MapModal toggle={toggle} isOpen={isOpen} title={title} locations={locations} />);
});
afterEach(() => wrapper.unmount());
it('renders modal with provided props', () => {
const modal = wrapper.find(Modal);
const header = wrapper.find('.map-modal__modal-title');
expect(modal.prop('toggle')).toEqual(toggle);
expect(modal.prop('isOpen')).toEqual(isOpen);
expect(header.find('.btn-close').prop('onClick')).toEqual(toggle);
expect(header.text()).toContain(title);
});
it('renders open street map tile', () => {
expect(wrapper.find('OpenStreetMapTile')).toHaveLength(1);
});
it('renders proper amount of markers', () => {
const markers = wrapper.find(Marker);
expect(markers).toHaveLength(locations.length);
locations.forEach(({ latLong, count, cityName }, index) => {
const marker = markers.at(index);
const popup = marker.find(Popup);
expect(marker.prop('position')).toEqual(latLong);
expect(popup.text()).toEqual(`${count} visits from ${cityName}`);
});
it('renders expected map', () => {
render(<MapModal toggle={toggle} isOpen title="Foobar" locations={locations} />);
expect(screen.getByRole('dialog')).toMatchSnapshot();
});
});

View file

@ -1,61 +1,54 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Dropdown, DropdownItem, UncontrolledTooltip } from 'reactstrap';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Mock } from 'ts-mockery';
import { OpenMapModalBtn } from '../../../src/visits/helpers/OpenMapModalBtn';
import { MapModal } from '../../../src/visits/helpers/MapModal';
import { CityStats } from '../../../src/visits/types';
describe('<OpenMapModalBtn />', () => {
let wrapper: ShallowWrapper;
const title = 'Foo';
const locations = [
Mock.of<CityStats>({ cityName: 'foo', count: 30 }),
Mock.of<CityStats>({ cityName: 'bar', count: 45 }),
Mock.of<CityStats>({ cityName: 'foo', count: 30, latLong: [5, 5] }),
Mock.of<CityStats>({ cityName: 'bar', count: 45, latLong: [88, 88] }),
];
const createWrapper = (activeCities: string[] = []) => {
wrapper = shallow(<OpenMapModalBtn modalTitle={title} locations={locations} activeCities={activeCities} />);
return wrapper;
};
afterEach(() => wrapper?.unmount());
it('renders expected content', () => {
const wrapper = createWrapper();
const button = wrapper.find('.open-map-modal-btn__btn');
const tooltip = wrapper.find(UncontrolledTooltip);
const dropdown = wrapper.find(Dropdown);
const modal = wrapper.find(MapModal);
expect(button).toHaveLength(1);
expect(tooltip).toHaveLength(1);
expect(dropdown).toHaveLength(1);
expect(modal).toHaveLength(1);
const setUp = (activeCities?: string[]) => ({
user: userEvent.setup(),
...render(<OpenMapModalBtn modalTitle={title} locations={locations} activeCities={activeCities} />),
});
it('opens dropdown instead of modal when a list of active cities has been provided', () => {
const wrapper = createWrapper(['bar']);
it('renders tooltip on button hover and opens modal on click', async () => {
const { user } = setUp();
wrapper.find('.open-map-modal-btn__btn').simulate('click');
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
expect(wrapper.find(Dropdown).prop('isOpen')).toEqual(true);
expect(wrapper.find(MapModal).prop('isOpen')).toEqual(false);
await user.click(screen.getByRole('button'));
await waitFor(() => expect(screen.getByRole('tooltip')).toBeInTheDocument());
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument());
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
});
it('filters out non-active cities from list of locations', () => {
const wrapper = createWrapper(['bar']);
it('opens dropdown instead of modal when a list of active cities has been provided', async () => {
const { user } = setUp(['bar']);
wrapper.find('.open-map-modal-btn__btn').simulate('click');
wrapper.find(Dropdown).find(DropdownItem).at(1).simulate('click');
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
const modal = wrapper.find(MapModal);
await user.click(screen.getByRole('button'));
expect(modal.prop('title')).toEqual(title);
expect(modal.prop('locations')).toEqual([
{
cityName: 'bar',
count: 45,
},
]);
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
it.each([
['Show all locations'],
['Show locations in current page'],
])('filters out non-active cities from list of locations', async (name) => {
const { user } = setUp(['bar']);
await user.click(screen.getByRole('button'));
await user.click(screen.getByRole('menuitem', { name }));
expect(await screen.findByRole('dialog')).toMatchSnapshot();
});
});

View file

@ -0,0 +1,184 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<MapModal /> renders expected map 1`] = `
<div
class="modal fade"
role="dialog"
style="display: block;"
tabindex="-1"
>
<div
class="modal-dialog map-modal__modal"
role="document"
>
<div
class="modal-content map-modal__modal-content"
>
<div
class="map-modal__modal-body modal-body"
>
<h3
class="map-modal__modal-title"
>
Foobar
<button
aria-label="Close"
class="btn-close float-end"
type="button"
/>
</h3>
<div
class="leaflet-container leaflet-touch leaflet-grab leaflet-touch-drag leaflet-touch-zoom"
style="position: relative;"
tabindex="0"
>
<div
class="leaflet-pane leaflet-map-pane"
style="left: 0px; top: 0px;"
>
<div
class="leaflet-pane leaflet-tile-pane"
>
<div
class="leaflet-layer "
style="z-index: 1;"
>
<div
class="leaflet-tile-container leaflet-zoom-animated"
style="z-index: 18; left: 0px; top: 0px;"
>
<img
alt=""
class="leaflet-tile"
role="presentation"
src="https://a.tile.openstreetmap.org/0/0/0.png"
style="width: 256px; height: 256px; left: -101px; top: -96px;"
/>
</div>
</div>
</div>
<div
class="leaflet-pane leaflet-overlay-pane"
/>
<div
class="leaflet-pane leaflet-shadow-pane"
>
<img
alt=""
class="leaflet-marker-shadow leaflet-zoom-hide"
src="marker-shadow.png"
style="margin-left: -12px; margin-top: -41px; width: 41px; height: 41px; left: 26px; top: -1px;"
/>
<img
alt=""
class="leaflet-marker-shadow leaflet-zoom-hide"
src="marker-shadow.png"
style="margin-left: -12px; margin-top: -41px; width: 41px; height: 41px; left: -26px; top: 0px;"
/>
</div>
<div
class="leaflet-pane leaflet-marker-pane"
>
<img
alt="Marker"
class="leaflet-marker-icon leaflet-zoom-hide leaflet-interactive"
role="button"
src="marker-icon.png"
style="margin-left: -12px; margin-top: -41px; width: 25px; height: 41px; left: 26px; top: -1px; z-index: -1;"
tabindex="0"
/>
<img
alt="Marker"
class="leaflet-marker-icon leaflet-zoom-hide leaflet-interactive"
role="button"
src="marker-icon.png"
style="margin-left: -12px; margin-top: -41px; width: 25px; height: 41px; left: -26px; top: 0px; z-index: 0;"
tabindex="0"
/>
</div>
<div
class="leaflet-pane leaflet-tooltip-pane"
/>
<div
class="leaflet-pane leaflet-popup-pane"
/>
</div>
<div
class="leaflet-control-container"
>
<div
class="leaflet-top leaflet-left"
>
<div
class="leaflet-control-zoom leaflet-bar leaflet-control"
>
<a
aria-disabled="false"
aria-label="Zoom in"
class="leaflet-control-zoom-in"
href="#"
role="button"
title="Zoom in"
>
<span
aria-hidden="true"
>
+
</span>
</a>
<a
aria-disabled="true"
aria-label="Zoom out"
class="leaflet-control-zoom-out leaflet-disabled"
href="#"
role="button"
title="Zoom out"
>
<span
aria-hidden="true"
>
</span>
</a>
</div>
</div>
<div
class="leaflet-top leaflet-right"
/>
<div
class="leaflet-bottom leaflet-left"
/>
<div
class="leaflet-bottom leaflet-right"
>
<div
class="leaflet-control-attribution leaflet-control"
>
<a
href="https://leafletjs.com"
title="A JavaScript library for interactive maps"
>
Leaflet
</a>
<span
aria-hidden="true"
>
|
</span>
©
<a
href="http://osm.org/copyright"
>
OpenStreetMap
</a>
contributors
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,353 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<OpenMapModalBtn /> filters out non-active cities from list of locations 1`] = `
<div
class="modal fade"
role="dialog"
style="display: block;"
tabindex="-1"
>
<div
class="modal-dialog map-modal__modal"
role="document"
>
<div
class="modal-content map-modal__modal-content"
>
<div
class="map-modal__modal-body modal-body"
>
<h3
class="map-modal__modal-title"
>
Foo
<button
aria-label="Close"
class="btn-close float-end"
type="button"
/>
</h3>
<div
class="leaflet-container leaflet-touch leaflet-grab leaflet-touch-drag leaflet-touch-zoom"
style="position: relative;"
tabindex="0"
>
<div
class="leaflet-pane leaflet-map-pane"
style="left: 0px; top: 0px;"
>
<div
class="leaflet-pane leaflet-tile-pane"
>
<div
class="leaflet-layer "
style="z-index: 1;"
>
<div
class="leaflet-tile-container leaflet-zoom-animated"
style="z-index: 18; left: 0px; top: 0px;"
>
<img
alt=""
class="leaflet-tile"
role="presentation"
src="https://a.tile.openstreetmap.org/0/0/0.png"
style="width: 256px; height: 256px; left: -161px; top: -62px;"
/>
</div>
</div>
</div>
<div
class="leaflet-pane leaflet-overlay-pane"
/>
<div
class="leaflet-pane leaflet-shadow-pane"
>
<img
alt=""
class="leaflet-marker-shadow leaflet-zoom-hide"
src="marker-shadow.png"
style="margin-left: -12px; margin-top: -41px; width: 41px; height: 41px; left: -29px; top: 62px;"
/>
<img
alt=""
class="leaflet-marker-shadow leaflet-zoom-hide"
src="marker-shadow.png"
style="margin-left: -12px; margin-top: -41px; width: 41px; height: 41px; left: 30px; top: -62px;"
/>
</div>
<div
class="leaflet-pane leaflet-marker-pane"
>
<img
alt="Marker"
class="leaflet-marker-icon leaflet-zoom-hide leaflet-interactive"
role="button"
src="marker-icon.png"
style="margin-left: -12px; margin-top: -41px; width: 25px; height: 41px; left: -29px; top: 62px; z-index: 62;"
tabindex="0"
/>
<img
alt="Marker"
class="leaflet-marker-icon leaflet-zoom-hide leaflet-interactive"
role="button"
src="marker-icon.png"
style="margin-left: -12px; margin-top: -41px; width: 25px; height: 41px; left: 30px; top: -62px; z-index: -62;"
tabindex="0"
/>
</div>
<div
class="leaflet-pane leaflet-tooltip-pane"
/>
<div
class="leaflet-pane leaflet-popup-pane"
/>
</div>
<div
class="leaflet-control-container"
>
<div
class="leaflet-top leaflet-left"
>
<div
class="leaflet-control-zoom leaflet-bar leaflet-control"
>
<a
aria-disabled="false"
aria-label="Zoom in"
class="leaflet-control-zoom-in"
href="#"
role="button"
title="Zoom in"
>
<span
aria-hidden="true"
>
+
</span>
</a>
<a
aria-disabled="true"
aria-label="Zoom out"
class="leaflet-control-zoom-out leaflet-disabled"
href="#"
role="button"
title="Zoom out"
>
<span
aria-hidden="true"
>
</span>
</a>
</div>
</div>
<div
class="leaflet-top leaflet-right"
/>
<div
class="leaflet-bottom leaflet-left"
/>
<div
class="leaflet-bottom leaflet-right"
>
<div
class="leaflet-control-attribution leaflet-control"
>
<a
href="https://leafletjs.com"
title="A JavaScript library for interactive maps"
>
Leaflet
</a>
<span
aria-hidden="true"
>
|
</span>
©
<a
href="http://osm.org/copyright"
>
OpenStreetMap
</a>
contributors
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`<OpenMapModalBtn /> filters out non-active cities from list of locations 2`] = `
<div
class="modal fade"
role="dialog"
style="display: block;"
tabindex="-1"
>
<div
class="modal-dialog map-modal__modal"
role="document"
>
<div
class="modal-content map-modal__modal-content"
>
<div
class="map-modal__modal-body modal-body"
>
<h3
class="map-modal__modal-title"
>
Foo
<button
aria-label="Close"
class="btn-close float-end"
type="button"
/>
</h3>
<div
class="leaflet-container leaflet-touch leaflet-grab leaflet-touch-drag leaflet-touch-zoom"
style="position: relative;"
tabindex="0"
>
<div
class="leaflet-pane leaflet-map-pane"
style="left: 0px; top: 0px;"
>
<div
class="leaflet-pane leaflet-tile-pane"
>
<div
class="leaflet-layer "
style="z-index: 1;"
>
<div
class="leaflet-tile-container leaflet-zoom-animated"
style="z-index: 18; left: 0px; top: 0px;"
>
<img
alt=""
class="leaflet-tile"
role="presentation"
src="https://a.tile.openstreetmap.org/10/762/0.png"
style="width: 256px; height: 256px; left: -80px; top: 0px;"
/>
</div>
</div>
</div>
<div
class="leaflet-pane leaflet-overlay-pane"
/>
<div
class="leaflet-pane leaflet-shadow-pane"
>
<img
alt=""
class="leaflet-marker-shadow leaflet-zoom-hide"
src="marker-shadow.png"
style="margin-left: -12px; margin-top: -41px; width: 41px; height: 41px; left: 0px; top: 0px;"
/>
</div>
<div
class="leaflet-pane leaflet-marker-pane"
>
<img
alt="Marker"
class="leaflet-marker-icon leaflet-zoom-hide leaflet-interactive"
role="button"
src="marker-icon.png"
style="margin-left: -12px; margin-top: -41px; width: 25px; height: 41px; left: 0px; top: 0px; z-index: 0;"
tabindex="0"
/>
</div>
<div
class="leaflet-pane leaflet-tooltip-pane"
/>
<div
class="leaflet-pane leaflet-popup-pane"
/>
</div>
<div
class="leaflet-control-container"
>
<div
class="leaflet-top leaflet-left"
>
<div
class="leaflet-control-zoom leaflet-bar leaflet-control"
>
<a
aria-disabled="false"
aria-label="Zoom in"
class="leaflet-control-zoom-in"
href="#"
role="button"
title="Zoom in"
>
<span
aria-hidden="true"
>
+
</span>
</a>
<a
aria-disabled="false"
aria-label="Zoom out"
class="leaflet-control-zoom-out"
href="#"
role="button"
title="Zoom out"
>
<span
aria-hidden="true"
>
</span>
</a>
</div>
</div>
<div
class="leaflet-top leaflet-right"
/>
<div
class="leaflet-bottom leaflet-left"
/>
<div
class="leaflet-bottom leaflet-right"
>
<div
class="leaflet-control-attribution leaflet-control"
>
<a
href="https://leafletjs.com"
title="A JavaScript library for interactive maps"
>
Leaflet
</a>
<span
aria-hidden="true"
>
|
</span>
©
<a
href="http://osm.org/copyright"
>
OpenStreetMap
</a>
contributors
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;