Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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_outisde_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_outisde_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_outisde_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_outisde_color.svg';
import { ReactComponent as DirectionLight } from '../../assets/map/ic_pin_circle_outisde_light.svg';
import { ReactComponent as DirectionStroke } from '../../assets/map/ic_pin_circle_outisde_stroke.svg';
Comment thread
alisa911 marked this conversation as resolved.
Outdated
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