Skip to content
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
3 changes: 3 additions & 0 deletions map/src/assets/map/hover_point_circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions map/src/assets/map/hover_point_octagon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions map/src/assets/map/hover_point_square.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions map/src/assets/map/ic_pin_circle_outside_color.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions map/src/assets/map/ic_pin_circle_outside_light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions map/src/assets/map/ic_pin_circle_outside_stroke.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions map/src/map/layers/MapStateLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,30 @@ export function getVisibleBboxInfo(ctx, map) {
return params ? calcVisibleBbox(params.topLeft, params.bottomRight) : null;
}

export function visibleMapRectPx(ctx, map) {
const params = calcVisibleBboxParamsPx(map, ctx);
if (!params) {
return null;
}
const tl = map.latLngToContainerPoint(params.topLeft);
const br = map.latLngToContainerPoint(params.bottomRight);

return { left: tl.x, top: tl.y, right: br.x, bottom: br.y, cx: params.centerPx.x, cy: params.centerPx.y };
}

export function isOutsideVisibleMap({ ctx, map, latlng }) {
if (!ctx || !map || !latlng) {
return false;
}
const rect = visibleMapRectPx(ctx, map);
if (!rect) {
return false;
}
const p = map.latLngToContainerPoint(L.latLng(latlng));

return p.x < rect.left || p.x > rect.right || p.y < rect.top || p.y > rect.bottom;
}

export function getMapCenter(mtx, hash) {
return mtx.visibleBboxInfo?.center ?? getCenterMapLocByHash(hash);
}
Expand Down
4 changes: 3 additions & 1 deletion map/src/map/layers/NavigationLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { navigationObject } from '../../store/navigationObject/navigationObject'
import { pickNextRoutePoint } from '../../manager/NavigationManager';

const DRAG_DEBOUNCE_MS = 10;
const TURN_DOT_Z_INDEX_OFFSET = 1100;

function setMarkerIconHtml(marker, html) {
const el = marker?.getElement();
Expand Down Expand Up @@ -403,13 +404,14 @@ const NavigationLayer = ({ geocodingData, region }) => {
L.marker(latlng, {
icon: makeDotIcon(opts.fillColor, opts.fillOpacity),
interactive: true,
zIndexOffset: TURN_DOT_Z_INDEX_OFFSET,
})
);
};

const pointToLayerGeoData = (feature, latlng) => {
let opts = { ...geojsonMarkerOptions };
if (feature.properties && feature.properties.index) {
if (feature.properties?.index) {
opts.fillOpacity = Math.min(1 / Math.log(feature.properties.index + 2), 1);
let clrs = ['#6DD6DA', '#95D9DA', '#A2ABB5', '#AE8CA3', '#817F82'];
let indx = [2, 5, 7, 10, 20];
Expand Down
77 changes: 77 additions & 0 deletions map/src/map/markers/SelectedPinMarker.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,31 @@ import { ReactComponent as SquareStroke } from '../../assets/map/map_pin_square_
import { ReactComponent as HexagonColor } from '../../assets/map/map_pin_hexagon_color.svg';
import { ReactComponent as HexagonLight } from '../../assets/map/map_pin_hexagon_light.svg';
import { ReactComponent as HexagonStroke } from '../../assets/map/map_pin_hexagon_stroke.svg';
import { ReactComponent as HoverCircle } from '../../assets/map/hover_point_circle.svg';
import { ReactComponent as HoverSquare } from '../../assets/map/hover_point_square.svg';
import { ReactComponent as HoverOctagon } from '../../assets/map/hover_point_octagon.svg';
import { ReactComponent as DirectionColor } from '../../assets/map/ic_pin_circle_outside_color.svg';
import { ReactComponent as DirectionLight } from '../../assets/map/ic_pin_circle_outside_light.svg';
import { ReactComponent as DirectionStroke } from '../../assets/map/ic_pin_circle_outside_stroke.svg';
import { SELECTED_ICON_SIZE, SELECTED_PIN_COLOR, SELECTED_PIN_SIZE } from '../util/MarkerSelectionService';
import { DEFAULT_POI_SHAPE } from '../../manager/PoiManager';

/** Pin SVGs use viewBox 0 0 70 70; the map anchor must be the bottom dot center (y=67), not the box bottom. */
const PIN_VIEWBOX_SIZE = 70;
const PIN_TIP_CENTER_Y = 67;

export const HOVER_OUTLINE_SIZE = 34;
const DIRECTION_PIN_SIZE = 60;
const DIRECTION_PIN_ICON_SIZE = 26;
const DIRECTION_PIN_TAIL_TIP_RATIO = 74 / 82;

const HOVER_OUTLINE_SHAPES = {
circle: HoverCircle,
square: HoverSquare,
octagon: HoverOctagon,
hexagon: HoverOctagon,
};

const SHAPES = {
circle: {
color: CircleColor,
Expand Down Expand Up @@ -74,6 +92,65 @@ function prepareInnerIcon(html, iconSize) {
return changeIconSizeWpt(withoutShadow, iconSize, iconSize);
}

export function createHoverOutlineIcon({ shape, color, size } = {}) {
const px = Math.max(Number(size) || 0, HOVER_OUTLINE_SIZE);
const shapeKey = shape === 'octagon' || shape === 'hexagon' ? 'octagon' : shape;
const Component = HOVER_OUTLINE_SHAPES[shapeKey] ?? HOVER_OUTLINE_SHAPES.circle;
const svg = resizeSvg(renderToStaticMarkup(React.createElement(Component)), px);
const html = `<div class="map-hover-outline" style="width:${px}px;height:${px}px;color:${color};display:flex;align-items:center;justify-content:center;pointer-events:none;">${svg}</div>`;

return L.divIcon({
html,
className: '',
iconSize: [px, px],
iconAnchor: [px / 2, px / 2],
});
}

export function createDirectionPinIcon({
color,
iconHtml,
invertIcon,
angle = 0,
size = DIRECTION_PIN_SIZE,
iconSize = DIRECTION_PIN_ICON_SIZE,
} = {}) {
const colorSvg = renderToStaticMarkup(React.createElement(DirectionColor));
const lightSvg = renderToStaticMarkup(React.createElement(DirectionLight));
const strokeSvg = renderToStaticMarkup(React.createElement(DirectionStroke));

const coloredLayer = resizeSvg(changeSvgColor(colorSvg, color), size);
const strokeLayer = resizeSvg(strokeSvg, size);
const lightLayer = resizeSvg(lightSvg, size);

const markerIconHtml = prepareInnerIcon(iconHtml, iconSize);
const iconWrapperSize = Math.min(iconSize, size);
const tipX = size / 2;
const tipY = size * DIRECTION_PIN_TAIL_TIP_RATIO;

const html = `
<div class="map-layered-pin" style="position:relative;width:${size}px;height:${size}px;transform:rotate(${angle}deg);transform-origin:${tipX}px ${tipY}px;">
<div class="map-layered-pin__layer" style="position:absolute;inset:0;">${coloredLayer}</div>
<div class="map-layered-pin__layer" style="position:absolute;inset:0;">${strokeLayer}</div>
<div class="map-layered-pin__layer" style="position:absolute;inset:0;">${lightLayer}</div>
${
markerIconHtml
? `<div class="map-layered-pin__icon" style="position:absolute;left:50%;top:50%;transform:translate(-50%, -50%) rotate(${-angle}deg);width:${iconWrapperSize}px;height:${iconWrapperSize}px;display:flex;align-items:center;justify-content:center;${invertIcon ? 'filter:brightness(0) invert(1);' : ''}">
${markerIconHtml}
</div>`
: ''
}
</div>
`;

return L.divIcon({
html,
className: '',
iconSize: [size, size],
iconAnchor: [tipX, tipY],
});
}

export function createLayeredPinIcon(options = {}) {
const merged = {
size: SELECTED_PIN_SIZE,
Expand Down
2 changes: 1 addition & 1 deletion map/src/map/util/Clusterizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ export function addMarkerTooltip({

marker.on('mouseover', () => {
removeTooltip(map, tooltipRef);
setSelectedId?.({ id: marker.options.idObj, show: true, type });
setSelectedId?.({ id: marker.options.idObj, show: true, type, hoverFromMap: true });
if (text) {
const offset = mainStyle ? [5, iconSize * 0.8] : [0, iconSize * 0.8];
tooltipRef.current = createTooltip(Utils.truncateText(text, TOOLTIP_MAX_LENGTH), latlng, { offset });
Expand Down
86 changes: 85 additions & 1 deletion map/src/map/util/MarkerSelectionService.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { DEFAULT_WPT_COLOR } from '../markers/MarkerOptions';
import { createLayeredPinIcon } from '../markers/SelectedPinMarker';
import { createLayeredPinIcon, createHoverOutlineIcon, createDirectionPinIcon } from '../markers/SelectedPinMarker';
import L from 'leaflet';
import { hexToRgba } from '../../util/ColorUtil';
import { updateMarkerZIndex } from '../layers/ExploreLayer';
import { DEFAULT_POI_COLOR, DEFAULT_POI_SHAPE } from '../../manager/PoiManager';
import { visibleMapRectPx } from '../layers/MapStateLayer';

export const SELECTED_PIN_SIZE = 70;
export const SELECTED_ICON_SIZE = 36;
Expand All @@ -14,6 +16,8 @@ const SELECTED_MARKER_HIDE_MAX_ZOOM = 16;
const SELECTED_MARKER_HIDE_RADIUS_COEFF = 300 / 16;
const SELECTED_MARKER_HIDE_MIN_RADIUS_M = 50;

const DIRECTION_PIN_TIP_MARGIN = 6;

export const toShape = (s) => (s === 'octagon' || s === 'hexagon' ? 'hexagon' : s);

const toColor = (c) => {
Expand Down Expand Up @@ -137,6 +141,86 @@ export function restoreOriginalIcon(layer) {
}
}

// Shows an outline ring around a point hovered on the map
export function applyHoverOutline({ ctx, map, layer = null, latlng = null, shape, color, size }) {
if (!ctx || !map) {
return null;
}

resetSelectedPin({ ctx, map });

const ll = latlng ?? layer?.getLatLng();
if (!ll) {
return null;
}
const latlngObj = ll.lat !== undefined && ll.lng !== undefined ? L.latLng(ll.lat, ll.lng) : ll;

const outline = L.marker(latlngObj, {
icon: createHoverOutlineIcon({
shape: shape ?? DEFAULT_POI_SHAPE,
color: toColor(color ?? DEFAULT_POI_COLOR),
size,
}),
interactive: false,
zIndexOffset: SELECTED_MARKER_Z_INDEX,
});
outline.addTo(map);
ctx.selectedCreatedLayerRef.current = outline;

return outline;
}

function rayRectEdge(cx, cy, ux, uy, left, top, right, bottom) {
let t = Infinity;
if (ux > 0) t = Math.min(t, (right - cx) / ux);
else if (ux < 0) t = Math.min(t, (left - cx) / ux);
if (uy > 0) t = Math.min(t, (bottom - cy) / uy);
else if (uy < 0) t = Math.min(t, (top - cy) / uy);
if (!Number.isFinite(t)) {
t = 0;
}

return L.point(cx + ux * t, cy + uy * t);
}

export function applyDirectionPin({ ctx, map, latlng, markerData }) {
if (!ctx || !map || !latlng || !markerData) {
return null;
}

resetSelectedPin({ ctx, map });

const rect = visibleMapRectPx(ctx, map);
if (!rect) {
return null;
}
const target = map.latLngToContainerPoint(L.latLng(latlng));
let ux = target.x - rect.cx;
let uy = target.y - rect.cy;
if (ux === 0 && uy === 0) {
uy = 1;
}

const m = DIRECTION_PIN_TIP_MARGIN;
const tip = rayRectEdge(rect.cx, rect.cy, ux, uy, rect.left + m, rect.top + m, rect.right - m, rect.bottom - m);
const angle = (Math.atan2(-ux, uy) * 180) / Math.PI;

const pin = L.marker(map.containerPointToLatLng(tip), {
icon: createDirectionPinIcon({
color: toColor(markerData.color ?? DEFAULT_POI_COLOR),
iconHtml: markerData.iconHtml,
invertIcon: markerData.invertIcon,
angle,
}),
interactive: false,
zIndexOffset: SELECTED_MARKER_Z_INDEX,
});
pin.addTo(map);
ctx.selectedCreatedLayerRef.current = pin;

return pin;
}

// Removes the current selected/hover pin and restores hidden markers.
// The created pin is kept only if its idObj still matches the current selectedWpt id.
export function resetSelectedPin({ ctx, map, force = false }) {
Expand Down
Loading