Skip to content
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
195 changes: 70 additions & 125 deletions src/features/map/EntityMap.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';

import { ObjectType } from '@features/params/PageParamTypes';
import usePageParams from '@features/params/usePageParams';
Expand All @@ -14,15 +14,9 @@ import { ObjectData } from '@entities/types/DataTypes';
import { uniqueBy } from '@shared/lib/setUtils';

import DrawableData from './DrawableData';
import { getRobinsonCoordinatesShifted } from './getRobinsonCoordinates';
import MapCard from './MapCard';
import MapCentroids from './MapCentroids';
import {
MAP_ASPECT_RATIO,
MAP_INTERNAL_WIDTH,
MAP_ROBINSON_X_SCALE,
MAP_ROBINSON_Y_SCALE,
} from './MapConsts';
import { MAP_ASPECT_RATIO, MAP_INTERNAL_WIDTH } from './MapConsts';
import MapSidebar from './MapSidebar';
import MapTerritories from './MapTerritories';
import useMapZoom from './UseMapZoom';
import ZoomControls from './ZoomControls';
Expand All @@ -32,17 +26,12 @@ type Props = {
maxWidth?: number;
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maps can be in the main data view panel but also in the Details panel for Languages and Territories (click on a language and scroll to the bottom of the details and you'll see maps). It's weird to have the siderbar in those places because the page width is too small. Can you add a parameter allowSidebar default set to false and in ViewMap.tsx only set it to true.

https://translation-commons.github.io/lang-nav/data?view=Map&pinned=bel&objectID=bel

Image


type FloatingCard = {
id: string;
entity: DrawableData;
x: number;
y: number;
};

const EntityMap: React.FC<Props> = ({ entities, maxWidth = 2000 }) => {
const mapHeight = MAP_INTERNAL_WIDTH / MAP_ASPECT_RATIO;
const { pageBrightness } = usePageParams().brightness;

const [hoveredId, setHoveredId] = useState<string | null>(null);

const [zoomFactor, setZoomFactor] = useState(1);

const { containerRef, contentRef, zoomIn, zoomOut, resetTransform } = useMapZoom({
Expand All @@ -52,14 +41,6 @@ const EntityMap: React.FC<Props> = ({ entities, maxWidth = 2000 }) => {
});

const { colorBy, objectType, pinned, updatePageParams } = usePageParams();
const [mapScale, setMapScale] = useState(1);

const updateMapScale = useCallback(() => {
const rect = contentRef.current?.getBoundingClientRect();
if (!rect) return;

setMapScale(rect.width / MAP_INTERNAL_WIDTH);
}, [contentRef]);

const drawableEntities = useMemo(() => {
if (objectType === ObjectType.Language) {
Expand All @@ -83,135 +64,99 @@ const EntityMap: React.FC<Props> = ({ entities, maxWidth = 2000 }) => {

const coloringFunctions = useColors({ objects: drawableEntities });

// Floating cards are derived from the pinned page param so they can be fully restored from the
// URL after a refresh. Each card is positioned at its entity's Robinson centroid.
const floatingCards = useMemo<FloatingCard[]>(() => {
const pinnedEntities = useMemo(() => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since this is only used in MapSidebar I would remove this code from EntityMap and pass drawableEntities to MapSidebar and do this memo there.

const drawableById = new Map(drawableEntities.map((entity) => [entity.ID, entity]));
return pinned
.map((id) => {
const entity = drawableById.get(id);
if (entity == null || entity.latitude == null || entity.longitude == null) return undefined;

const { x: robinsonX, y: robinsonY } = getRobinsonCoordinatesShifted(entity);
return {
id,
entity,
x: MAP_INTERNAL_WIDTH / 2 + robinsonX * MAP_ROBINSON_X_SCALE,
y: mapHeight / 2 - robinsonY * MAP_ROBINSON_Y_SCALE,
};
})
.filter((card): card is FloatingCard => card != null);
}, [pinned, drawableEntities, mapHeight]);

const openCard = useCallback(
.map((id) => drawableById.get(id))
.filter((entity): entity is DrawableData => entity != null);
}, [pinned, drawableEntities]);

const pinCard = useCallback(
(entity: DrawableData) => {
updateMapScale();
if (pinned.includes(entity.ID)) return;
updatePageParams({ pinned: [...pinned, entity.ID] });
if (pinned.includes(entity.ID)) {
updatePageParams({ pinned: pinned.filter((id) => id !== entity.ID) });
} else {
updatePageParams({ pinned: [...pinned, entity.ID] });
}
Comment on lines +74 to +80

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Given the above changes, let's change this method from always being pinCard to instead be called onClick. If the sidebar is allowed, then update pins, if the sidebar is not allowed then let's just open/or update the details page: updatePageParams({objectID: entity.ID})

},
[pinned, updateMapScale, updatePageParams],
[pinned, updatePageParams],
);

const closeCard = useCallback(
const unpinCard = useCallback(
(id: string) => {
updatePageParams({ pinned: pinned.filter((pin) => pin !== id) });
},
[pinned, updatePageParams],
);

const handleZoomIn = useCallback(() => {
zoomIn();
requestAnimationFrame(updateMapScale);
}, [zoomIn, updateMapScale]);

const handleZoomOut = useCallback(() => {
zoomOut();
requestAnimationFrame(updateMapScale);
}, [zoomOut, updateMapScale]);

const handleResetTransform = useCallback(() => {
resetTransform();
requestAnimationFrame(updateMapScale);
}, [resetTransform, updateMapScale]);

// Restore the map scale on mount so URL-restored cards are sized correctly before any zoom.
useEffect(() => {
requestAnimationFrame(updateMapScale);
}, [updateMapScale]);

return (
<div style={{ maxWidth, width: '100%', position: 'relative' }}>
<ZoomControls
zoomIn={handleZoomIn}
zoomOut={handleZoomOut}
resetTransform={handleResetTransform}
/>
<ZoomControls zoomIn={zoomIn} zoomOut={zoomOut} resetTransform={resetTransform} />

<div
ref={containerRef}
style={{
border: '1px solid var(--border-color)',
border: '1px solid var(--color-text-secondary)',
Comment on lines -153 to +98

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ah, this was a mistake I made when I used a color that does not exist.

However, tbh, I think we are better with no border at all.

width: '100%',
aspectRatio: MAP_ASPECT_RATIO,
display: 'flex',
overflow: 'hidden',
cursor: 'grab',
position: 'relative',
background: 'var(--color-background)',
}}
onClick={() => updatePageParams({ pinned: [] })}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

yeap, good idea. We no longer need to dismiss all pins with an off-card click.

>
<MapSidebar

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

only show this if allowSidebar is true

pinnedEntities={pinnedEntities}
objectType={objectType}
onClose={unpinCard}
hoveredId={hoveredId}
setHoveredId={setHoveredId}
/>

<div
ref={contentRef}
style={{ width: MAP_INTERNAL_WIDTH, height: mapHeight, position: 'relative' }}
ref={containerRef}
style={{
flex: 1,
overflow: 'hidden',
cursor: 'grab',
position: 'relative',
}}
>
<img
alt="World map"
src="./data/wiki/map_world.svg"
style={{
position: 'absolute',
width: '100%',
height: '100%',
top: 0,
left: 0,
filter: pageBrightness === 'dark' ? 'invert(100%)' : undefined,
}}
/>

{objectType !== ObjectType.Language && (
<MapTerritories
drawableEntities={drawableEntities}
openCard={openCard}
coloringFunctions={coloringFunctions}
/>
)}

<MapCentroids
drawableEntities={drawableEntities}
openCard={openCard}
scalar={1200 / maxWidth}
zoomFactor={zoomFactor}
coloringFunctions={coloringFunctions}
/>
{floatingCards.map((card) => (
<div
key={card.id}
<div
ref={contentRef}
style={{ width: MAP_INTERNAL_WIDTH, height: mapHeight, position: 'relative' }}
>
<img
alt="World map"
src="./data/wiki/map_world.svg"
style={{
position: 'absolute',
left: card.x,
top: card.y + 12 / mapScale,
transform: `translateX(-50%) scale(${1 / mapScale})`,
transformOrigin: 'top center',
zIndex: 10,
cursor: 'default',
width: '100%',
height: '100%',
top: 0,
left: 0,
filter: pageBrightness === 'dark' ? 'invert(100%)' : undefined,
}}
onClick={(e) => e.stopPropagation()}
>
<MapCard
drawnEntity={card.entity}
objectType={objectType}
onClose={() => closeCard(card.id)}
/>

{objectType !== ObjectType.Language && (
<MapTerritories
drawableEntities={drawableEntities}
pinCard={pinCard}
coloringFunctions={coloringFunctions}
hoveredId={hoveredId}
pinnedIds={pinned}
/>
</div>
))}
)}

<MapCentroids
drawableEntities={drawableEntities}
pinCard={pinCard}
scalar={1200 / maxWidth}
zoomFactor={zoomFactor}
coloringFunctions={coloringFunctions}
hoveredId={hoveredId}
pinnedIds={pinned}
/>
</div>
</div>
</div>

Expand Down
49 changes: 38 additions & 11 deletions src/features/map/MapCentroids.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,22 @@ import './map.css';

type Props = {
drawableEntities: DrawableData[];
openCard: (obj: DrawableData, x: number, y: number) => void;
pinCard: (obj: DrawableData) => void;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do onClick instead

scalar: number;
zoomFactor: number;
coloringFunctions: ColoringFunctions;
hoveredId?: string | null;
pinnedIds?: string[];
};

const MapCentroids: React.FC<Props> = ({
drawableEntities,
openCard,
pinCard,
scalar,
zoomFactor,
coloringFunctions: { getColor, colorBy },
hoveredId,
pinnedIds = [],
}) => {
const { scaleBy, objectType } = usePageParams();
const { getCurrentEntities } = usePagination<DrawableData>();
Expand Down Expand Up @@ -81,9 +85,11 @@ const MapCentroids: React.FC<Props> = ({
object={obj}
scale={scalar * getScale(obj)}
zoomFactor={zoomFactor}
openCard={openCard}
pinCard={pinCard}
onMouseEnter={buildOnMouseEnter(obj)}
onMouseLeave={onMouseLeaveTriggeringElement}
isHovered={hoveredId === obj.ID}
isPinned={pinnedIds.includes(obj.ID)}
/>
))}
</svg>
Expand All @@ -95,19 +101,23 @@ type NodeProps = {
object: DrawableData;
scale: number;
zoomFactor: number;
openCard: (obj: DrawableData, x: number, y: number) => void;
pinCard: (obj: DrawableData) => void;
onMouseEnter: (e: React.MouseEvent) => void;
onMouseLeave: () => void;
isHovered?: boolean;
isPinned?: boolean;
};

const ObjectNode: React.FC<NodeProps> = ({
object,
color,
scale,
zoomFactor,
openCard,
pinCard,
onMouseEnter,
onMouseLeave,
isHovered,
isPinned,
}) => {
if (object.type !== ObjectType.Language && object.type !== ObjectType.Territory) return null;
if (object.latitude == null || object.longitude == null) return null;
Expand All @@ -126,9 +136,11 @@ const ObjectNode: React.FC<NodeProps> = ({
object={object}
scale={scale}
zoomFactor={zoomFactor}
openCard={openCard}
pinCard={pinCard}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
isHovered={isHovered}
isPinned={isPinned}
/>
)}
<Text object={object} scale={scale} showCircle={showCircle} zoomFactor={zoomFactor} />
Expand All @@ -140,18 +152,33 @@ const Circle: React.FC<NodeProps> = ({
color,
object,
scale,
openCard,
pinCard,
onMouseEnter,
onMouseLeave,
isHovered,
isPinned,
}) => (
<circle
r={scale + 1.5}
strokeWidth={0.4}
r={scale + 1.5 + (isHovered ? 2 : 0) + (isPinned ? 0.8 : 0)}
strokeWidth={isPinned ? 2 : isHovered ? 1.5 : 0.4}
fill={color ?? 'transparent'}
stroke={color == null ? 'var(--color-button-primary)' : 'transparent'}
stroke={
isPinned
? 'var(--color-text)'
: isHovered
? 'var(--color-button-primary)'
: color == null
? 'var(--color-button-primary)'
: 'transparent'
}
style={
isHovered
? { filter: 'brightness(1.2)', transition: 'all 0.15s ease-in-out' }
: { transition: 'all 0.15s ease-in-out' }
}
Comment on lines 161 to +178

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There is a CSS file with some styles map.css -- if we need to do math or add overrides let's keep it here (eg. r, fill) but for others like scale, strokeWidth, filter, transition let's update that CSS. This seems like good values for map.css. This also helps keeps the mouse hover and sidebar hover consistent.

circle {
  transition:
    fill 0.25s,
    stroke 0.25s,
    scale 0.25s;
  pointer-events: fill;
  cursor: pointer;
  stroke-width: 1px;
}

circle:hover,
circle.hovered {
  filter: brightness(1.2);
  scale: 1.5;
  stroke: var(--color-button-primary);
}

circle.pinned {
  stroke: var(--color-button-primary);
}
Suggested change
<circle
r={scale + 1.5}
strokeWidth={0.4}
r={scale + 1.5 + (isHovered ? 2 : 0) + (isPinned ? 0.8 : 0)}
strokeWidth={isPinned ? 2 : isHovered ? 1.5 : 0.4}
fill={color ?? 'transparent'}
stroke={color == null ? 'var(--color-button-primary)' : 'transparent'}
stroke={
isPinned
? 'var(--color-text)'
: isHovered
? 'var(--color-button-primary)'
: color == null
? 'var(--color-button-primary)'
: 'transparent'
}
style={
isHovered
? { filter: 'brightness(1.2)', transition: 'all 0.15s ease-in-out' }
: { transition: 'all 0.15s ease-in-out' }
}
<circle
className={(isHovered ? 'hovered' : '') + (isPinned ? ' pinned' : '')}
r={scale + 1.5}
fill={color ?? 'transparent'}
stroke={color == null ? 'var(--color-button-primary)' : 'transparent'}

onClick={(e) => {
e.stopPropagation();
openCard(object, e.clientX, e.clientY);
pinCard(object);
}}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
Expand Down
Loading
Loading