Skip to content

Commit

Permalink
Merge pull request #73 from dapetcu21/map-zoom
Browse files Browse the repository at this point in the history
Fix map zooming on the overlay bounds when loaded
  • Loading branch information
RaduCStefanescu authored Oct 6, 2020
2 parents 0e5eb1f + 239a7c0 commit 49989a8
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 49 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@code4ro/reusable-components",
"version": "0.1.42",
"version": "0.1.43",
"description": "Component library for code4ro",
"keywords": [
"code4ro",
Expand Down
14 changes: 10 additions & 4 deletions src/components/ElectionMap/ElectionMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ElectionMapScope, ElectionMapWinner, ElectionScopeIncomplete } from "..
import { mergeClasses, themable } from "../../hooks/theme";
import RomaniaMap from "../../assets/romania-map.svg";
import { useDimensions } from "../../hooks/useDimensions";
import { HereMap, romaniaMapBounds, worldMapBounds } from "../HereMap/HereMap";
import { bucharestCenteredWorldZoom, HereMap, romaniaMapBounds } from "../HereMap/HereMap";
import { electionMapOverlayUrl } from "../../constants/servers";
import cssClasses from "./ElectionMap.module.scss";
import { ElectionMapAPI } from "../../util/electionApi";
Expand Down Expand Up @@ -175,13 +175,19 @@ export const ElectionMap = themable<Props>(
className={classes.hereMap}
width={width}
height={height}
scopeType={scope.type}
initialBounds={
scope.type === "diaspora" || scope.type === "diaspora_country" ? worldMapBounds : romaniaMapBounds
initialTransform={
scope.type === "diaspora" || scope.type === "diaspora_country"
? bucharestCenteredWorldZoom
: romaniaMapBounds
}
overlayLoadTransform={
scope.type === "diaspora" || scope.type === "diaspora_country" ? bucharestCenteredWorldZoom : "bounds"
}
allowZoomAndPan={scope.type === "diaspora" || scope.type === "diaspora_country"}
overlayUrl={overlayUrl}
maskOverlayUrl={maskUrl}
selectedFeature={selectedFeature}
centerOnSelectedFeatureBounds={scope.type === "diaspora_country"}
onFeatureSelect={onFeatureSelect}
getFeatureColor={getFeatureColor}
renderFeatureTooltip={renderFeatureTooltip}
Expand Down
9 changes: 8 additions & 1 deletion src/components/HereMap/HereMap.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@

.root {
position: relative;
-webkit-touch-callout:none;
-webkit-user-select:none;
-khtml-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
user-select:none;
-webkit-tap-highlight-color:rgba(0,0,0,0);
}

.tooltip {
position: absolute;
left: 0;
top: 0;
transform: translate(-50%, calc(-100% - 8px));
font-size: 12rem / 16;
font-size: 1rem;
color: #fff;
border-radius: 0.25rem;
padding: 0.5rem;
Expand Down
171 changes: 129 additions & 42 deletions src/components/HereMap/HereMap.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import React, { createContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { themable } from "../../hooks/theme";
import { themable, ThemedComponentProps } from "../../hooks/theme";
import cssClasses from "./HereMap.module.scss";
import Color from "color";

type OnFeatureSelect = (featureId: number) => unknown;
type RenderFeatureTooltip = (id: number, featureProps: any) => Node | string | null;

export type HereMapRect = {
top: number; // Latitude
left: number; // Longitude
bottom: number; // Latitude
right: number; // Longitude
};

export type HereMapTransform = {
center?: { lat: number; lng: number };
zoom?: number;
bounds?: HereMapRect;
};

type Props = {
className?: string;
scopeType: string;
width: number;
height: number;
overlayUrl?: string;
Expand All @@ -20,23 +32,28 @@ type Props = {
renderFeatureTooltip?: RenderFeatureTooltip; // Returns a HTML element or string
selectedFeature?: number | null | undefined;
onFeatureSelect?: OnFeatureSelect;
initialBounds?: {
top: number; // Latitude
left: number; // Longitude
bottom: number; // Latitude
right: number; // Longitude
};
centerOnOverlayBounds?: boolean; // Default true
initialTransform?: HereMapTransform;
overlayLoadTransform?: HereMapTransform | "bounds" | false; // Defaults to "bounds": the bounds of the loaded overlay
allowZoomAndPan?: boolean;
centerOnSelectedFeatureBounds?: boolean;
};

export const bucharestCenteredWorldZoom = {
center: { lat: 44.4268, lng: 26.1025 },
zoom: 2,
};

export const worldMapBounds = { top: 90, left: 0, bottom: -90, right: 180 };
export const romaniaMapBounds = {
top: 48.26534497800004,
bottom: 43.618995545000075,
left: 20.261959895000075,
right: 29.715232741000037,
bounds: {
top: 48.26534497800004,
bottom: 43.618995545000075,
left: 20.261959895000075,
right: 29.715232741000037,
},
};

const makeRect = (H: HereMapsAPI, r: HereMapRect): H.geo.Rect => new H.geo.Rect(r.top, r.left, r.bottom, r.right);

const loadJS = (src: string) =>
new Promise((resolve, reject) => {
const script = document.createElement("script");
Expand Down Expand Up @@ -107,9 +124,10 @@ type InstanceVars = {
tooltipClassName: string | null;
tooltipTop: number;
tooltipLeft: number;
centerOnOverlayBounds: boolean;
updateFeatureStyle: (feature: H.map.Polygon, selected: boolean, hover: boolean) => void;
renderFeatureTooltip: RenderFeatureTooltip | undefined;
overlayLoadTransform: HereMapTransform | "bounds" | false;
centeredFeature: number | null;
};

const stylesFromColor = (H: HereMapsAPI, color: string, featureSelectedDarken: number, featureHoverDarken: number) => {
Expand All @@ -132,6 +150,15 @@ const stylesFromColor = (H: HereMapsAPI, color: string, featureSelectedDarken: n
};
};

const setTooltipPosition = (self: InstanceVars, x: number, y: number) => {
self.tooltipLeft = x;
self.tooltipTop = y;
if (self.tooltipEl) {
self.tooltipEl.style.left = `${x}px`;
self.tooltipEl.style.top = `${y}px`;
}
};

export const HereMap = themable<Props>(
"HereMap",
cssClasses,
Expand All @@ -149,13 +176,21 @@ export const HereMap = themable<Props>(
renderFeatureTooltip,
selectedFeature,
onFeatureSelect,
initialBounds = worldMapBounds,
centerOnOverlayBounds = true,
scopeType,
}) => {
initialTransform = romaniaMapBounds,
overlayLoadTransform = "bounds",
allowZoomAndPan = true,
centerOnSelectedFeatureBounds = false,
}: ThemedComponentProps<Props>) => {
const H = useHereMaps();
const mapRef = useRef<HTMLDivElement>(null);
const [map, setMap] = useState<H.Map | null>(null);
const [mapObjects, setMapObjects] = useState<null | {
map: H.Map;
ui: H.ui.UI;
zoomControl: H.ui.ZoomControl;
behaviour: H.mapevents.Behavior;
}>(null);

const map = mapObjects?.map;

const { featureDefaultColor, selectedFeatureColor, featureSelectedDarken, featureHoverDarken } = constants;

Expand Down Expand Up @@ -193,9 +228,10 @@ export const HereMap = themable<Props>(
tooltipClassName: classes.tooltip ?? null,
tooltipTop: 0,
tooltipLeft: 0,
centerOnOverlayBounds,
updateFeatureStyle: updateFeatureStyle,
renderFeatureTooltip: renderFeatureTooltip,
overlayLoadTransform: overlayLoadTransform,
centeredFeature: (centerOnSelectedFeatureBounds ? selectedFeature : null) ?? null,
});

useLayoutEffect(() => {
Expand All @@ -207,39 +243,73 @@ export const HereMap = themable<Props>(

const blankLayer = new H.map.layer.Layer();
const hMap = new H.Map(mapRef.current, blankLayer, {
bounds: new H.geo.Rect(initialBounds.top, initialBounds.left, initialBounds.bottom, initialBounds.right),
bounds: initialTransform.bounds ? makeRect(H, initialTransform.bounds) : undefined,
center: initialTransform.center,
zoom: initialTransform.zoom,
noWrap: true,
pixelRatio: window.devicePixelRatio || 1,
});
setMap(hMap);

new H.mapevents.Behavior(new H.mapevents.MapEvents(hMap));
new H.ui.UI(hMap, { zoom: { alignment: H.ui.LayoutAlignment.RIGHT_BOTTOM } });
const hZoomControl = new H.ui.ZoomControl({ alignment: H.ui.LayoutAlignment.RIGHT_BOTTOM });
const hBehaviour = new H.mapevents.Behavior(new H.mapevents.MapEvents(hMap));
hBehaviour.disable();
const hUI = new H.ui.UI(hMap);

setMapObjects({
map: hMap,
zoomControl: hZoomControl,
behaviour: hBehaviour,
ui: hUI,
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
hMap.addEventListener("pointermove", (evt: any) => {
const { offsetX = 0, offsetY = 0 } = evt.originalEvent;
self.tooltipLeft = offsetX;
self.tooltipTop = offsetY;
if (self.tooltipEl) {
self.tooltipEl.style.left = `${offsetX}px`;
self.tooltipEl.style.top = `${offsetY}px`;
}
const { offsetX = 0, offsetY = 0, pointerType } = evt.originalEvent;
if (pointerType === "touch") return;
setTooltipPosition(self, offsetX, offsetY);
});

return () => {
hMap.dispose();
(hMap as any).disposed = true; // eslint-disable-line @typescript-eslint/no-explicit-any
setMap((state) => (state === hMap ? null : state));
setMapObjects((state) => (state?.map === hMap ? null : state));
};
}, [H, mapRef]);

useLayoutEffect(() => {
if (!H || !allowZoomAndPan || !mapObjects || (mapObjects.map as any).disposed) return;
const { ui, zoomControl, behaviour } = mapObjects;

behaviour.enable();
ui.addControl("zoomControl", zoomControl);

return () => {
behaviour.disable();
ui.removeControl("zoomControl");
};
}, [H, allowZoomAndPan, mapObjects]);

useLayoutEffect(() => {
if (map) {
map.getViewPort().resize();
}
}, [width, height, map]);

useLayoutEffect(() => {
const self = inst.current;
self.centeredFeature = (centerOnSelectedFeatureBounds ? selectedFeature : null) ?? null;

if (!map) return;

const { features, centeredFeature } = self;
if (!features || centeredFeature == null) return;

const feature = features.get(centeredFeature);
if (!feature) return;

map.getViewModel().setLookAtData({ bounds: feature.getBoundingBox() }, true);
}, [centerOnSelectedFeatureBounds, selectedFeature, map]);

// Whenever selectedFeature changes
useLayoutEffect(() => {
const self = inst.current;
Expand Down Expand Up @@ -268,8 +338,8 @@ export const HereMap = themable<Props>(
}, [onFeatureSelect]);

useLayoutEffect(() => {
inst.current.centerOnOverlayBounds = centerOnOverlayBounds;
}, [centerOnOverlayBounds]);
inst.current.overlayLoadTransform = overlayLoadTransform;
}, [overlayLoadTransform]);

useLayoutEffect(() => {
const self = inst.current;
Expand Down Expand Up @@ -340,6 +410,12 @@ export const HereMap = themable<Props>(
if (id == null) return;
if (self.hoveredFeature?.id !== id) setHoveredFeature(id, data);
self.updateFeatureStyle(mapObject, id === self.selectedFeature, true);

if ((evt as any)?.originalEvent?.pointerType === "touch") {
const bounds = mapObject.getBoundingBox();
const { x, y } = map.geoToScreen({ lat: bounds.getTop(), lng: bounds.getCenter().lng });
setTooltipPosition(self, x, y - 5);
}
}
};

Expand Down Expand Up @@ -405,14 +481,25 @@ export const HereMap = themable<Props>(
group.addEventListener("tap", onTap);
map.addObject(group);

if (self.centerOnOverlayBounds) {
const lookAtDataOptions: any = {
// bounds: group.getBoundingBox()
};
if (scopeType === "diaspora" || scopeType === "diaspora_country") {
lookAtDataOptions.zoom = 2;
if (self.centeredFeature != null) {
const feature = features.get(self.centeredFeature);
if (feature) {
map.getViewModel().setLookAtData({ bounds: feature.getBoundingBox() }, true);
}
} else {
const newTransform = self.overlayLoadTransform;
if (newTransform) {
map.getViewModel().setLookAtData(
newTransform === "bounds"
? { bounds: group.getBoundingBox() }
: {
bounds: newTransform.bounds ? makeRect(H, newTransform.bounds) : undefined,
position: newTransform.center,
zoom: newTransform.zoom,
},
true,
);
}
map.getViewModel().setLookAtData(lookAtDataOptions, true);
}
});
reader.parse();
Expand Down

1 comment on commit 49989a8

@vercel
Copy link

@vercel vercel bot commented on 49989a8 Oct 6, 2020

Choose a reason for hiding this comment

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

Please sign in to comment.