diff options
Diffstat (limited to 'src/client/views/nodes/MapBox')
| -rw-r--r-- | src/client/views/nodes/MapBox/AnimationSpeedIcons.tsx | 35 | ||||
| -rw-r--r-- | src/client/views/nodes/MapBox/AnimationUtility.ts | 447 | ||||
| -rw-r--r-- | src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx | 122 | ||||
| -rw-r--r-- | src/client/views/nodes/MapBox/GeocoderControl.tsx | 107 | ||||
| -rw-r--r-- | src/client/views/nodes/MapBox/MapAnchorMenu.scss | 79 | ||||
| -rw-r--r-- | src/client/views/nodes/MapBox/MapAnchorMenu.tsx | 471 | ||||
| -rw-r--r-- | src/client/views/nodes/MapBox/MapBox.scss | 134 | ||||
| -rw-r--r-- | src/client/views/nodes/MapBox/MapBox.tsx | 1416 | ||||
| -rw-r--r-- | src/client/views/nodes/MapBox/MapBox2.tsx | 1239 | ||||
| -rw-r--r-- | src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx | 184 | ||||
| -rw-r--r-- | src/client/views/nodes/MapBox/MapPushpinBox.tsx | 18 | ||||
| -rw-r--r-- | src/client/views/nodes/MapBox/MapboxApiUtility.ts | 139 | ||||
| -rw-r--r-- | src/client/views/nodes/MapBox/MarkerIcons.tsx | 103 | ||||
| -rw-r--r-- | src/client/views/nodes/MapBox/icon_images/mapbox-marker-icon-20px-blue.png | bin | 0 -> 1623 bytes |
14 files changed, 3439 insertions, 1055 deletions
diff --git a/src/client/views/nodes/MapBox/AnimationSpeedIcons.tsx b/src/client/views/nodes/MapBox/AnimationSpeedIcons.tsx new file mode 100644 index 000000000..d54a175b2 --- /dev/null +++ b/src/client/views/nodes/MapBox/AnimationSpeedIcons.tsx @@ -0,0 +1,35 @@ +import * as React from "react"; + +export const slowSpeedIcon: JSX.Element = ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 435.62"> + <defs> + <style type="text/css"> + {` + .fil0 { fill: black; fill-rule: nonzero; } + .fil1 { fill: #FE0000; fill-rule: nonzero; } + `} + </style> + </defs> + <path className="fil0" d="M174.84 343.06c-7.31,-13.12 -13.03,-27.28 -16.89,-42.18 -3.76,-14.56 -5.77,-29.71 -5.77,-45.17 0,-11.94 1.19,-23.66 3.43,-35.03 2.29,-11.57 5.74,-22.83 10.2,-33.63 13.7,-33.14 37.01,-61.29 66.42,-80.96 25.38,-16.96 55.28,-27.66 87.45,-29.87l0 -30.17c0,-0.46 0.02,-0.92 0.06,-1.37l-33.7 0c-5.53,0 -10.05,-4.52 -10.05,-10.04l0 -24.59c0,-5.53 4.52,-10.05 10.05,-10.05l101.27 0c5.53,0 10.05,4.52 10.05,10.05l0 24.59c0,5.52 -4.52,10.04 -10.05,10.04l-33.69 0c0.03,0.45 0.05,0.91 0.05,1.37l0 31.03 -0.1 0c41.1,4.89 77.94,23.63 105.73,51.42 32.56,32.55 52.7,77.54 52.7,127.21 0,49.67 -20.14,94.66 -52.7,127.21 -32.55,32.55 -77.54,52.7 -127.21,52.7 -33.16,0 -64.29,-9.04 -91.05,-24.78 -27.66,-16.27 -50.59,-39.73 -66.2,-67.78zm148.42 -36.62l-80.33 0 0 -25.71 28.6 0 0 -42.57 -28.6 1.93 0 -25.71 36.95 -8.35 25.38 0 0 74.7 18 0 0 25.71zm44.34 -100.41l11.08 26.83 1.61 0 11.09 -26.83 34.86 0 -22.33 48.52 22.33 51.89 -35.67 0 -12.05 -28.92 -1.44 0 -11.89 28.92 -34.06 0 21.85 -50.93 -21.85 -49.48 36.47 0zm126.08 -74.6c6.98,-16.66 6.15,-34.13 -3.84,-45.82 -12,-14.03 -33.67,-15.64 -53.8,-5.77 21.32,14.62 40.68,31.63 57.64,51.59zm-323.17 0c-6.98,-16.66 -6.16,-34.13 3.84,-45.82 11.99,-14.03 33.67,-15.64 53.79,-5.77 -21.32,14.62 -40.68,31.63 -57.63,51.59zm15.31 162.23c3.23,12.5 8.04,24.39 14.18,35.42 13.13,23.58 32.39,43.29 55.6,56.94 22.37,13.16 48.52,20.71 76.49,20.71 41.71,0 79.47,-16.9 106.8,-44.23 27.32,-27.32 44.23,-65.08 44.23,-106.79 0,-41.71 -16.91,-79.47 -44.23,-106.8 -27.33,-27.32 -65.09,-44.23 -106.8,-44.23 -31.07,0 -59.91,9.34 -83.84,25.33 -24.74,16.54 -44.33,40.19 -55.82,67.98 -3.68,8.91 -6.56,18.35 -8.5,28.22 -1.87,9.49 -2.86,19.36 -2.86,29.5 0,13.24 1.65,25.96 4.75,37.95z"/> + <path className="fil1" d="M55.23 188.52c-7.98,0 -14.45,-6.47 -14.45,-14.44 0,-7.98 6.47,-14.45 14.45,-14.45l63.94 0c7.98,0 14.45,6.47 14.45,14.45 0,7.97 -6.47,14.44 -14.45,14.44l-63.94 0zm0.72 167.68c-7.97,0 -14.44,-6.47 -14.44,-14.45 0,-7.97 6.47,-14.45 14.44,-14.45l64.58 0c7.97,0 14.45,6.48 14.45,14.45 0,7.98 -6.48,14.45 -14.45,14.45l-64.58 0zm-41.5 -84.94c-7.98,0 -14.45,-6.47 -14.45,-14.45 0,-7.97 6.47,-14.44 14.45,-14.44l89.12 0c7.98,0 14.45,6.47 14.45,14.44 0,7.98 -6.47,14.45 -14.45,14.45l-89.12 0z"/> + </svg> +); + +export const mediumSpeedIcon: JSX.Element = ( + <svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 122.88 104.55"> + <defs><style>{`.cls-1{fill:#fe0000;}`}</style></defs> + <path d="M42,82.34a42.82,42.82,0,0,1-4.05-10.13A43.2,43.2,0,0,1,76.72,18.29V11.05c0-.11,0-.22,0-.33H68.65a2.41,2.41,0,0,1-2.41-2.41V2.41A2.41,2.41,0,0,1,68.65,0H93a2.42,2.42,0,0,1,2.42,2.41v5.9A2.42,2.42,0,0,1,93,10.72H84.87c0,.11,0,.22,0,.33V18.5h0A43.17,43.17,0,1,1,42,82.34ZM88.22,49.45l2.66,6.44h.39l2.66-6.44h8.37L96.94,61.09l5.36,12.45H93.74L90.85,66.6H90.5l-2.85,6.94H79.47l5.25-12.22L79.47,49.45ZM58.65,56.08l-1-5.75a33.58,33.58,0,0,1,9.68-1.46c1.28,0,2.35,0,3.22.11a11.77,11.77,0,0,1,2.67.58,5.41,5.41,0,0,1,2.2,1.28c1.24,1.23,1.85,3.12,1.85,5.66s-.72,4.42-2.16,5.63S70.64,64.73,66,66.3v1.08H76.89v6.16H57.11V68.72a10.73,10.73,0,0,1,.81-4.12,8.4,8.4,0,0,1,2.43-2.7,12.13,12.13,0,0,1,2.79-1.7l3.32-1.52c1-.47,1.88-.87,2.52-1.17V55.42a28.59,28.59,0,0,0-3.2-.19,30.66,30.66,0,0,0-7.13.85Zm59.83-24.54c1.68-4,1.48-8.19-.92-11-2.88-3.37-8.08-3.76-12.91-1.39a69.74,69.74,0,0,1,13.83,12.38Zm-77.56,0c-1.67-4-1.48-8.19.92-11,2.88-3.37,8.08-3.76,12.91-1.39A70,70,0,0,0,40.92,31.54ZM44.6,70.48A36,36,0,0,0,48,79a35.91,35.91,0,1,0-3.4-8.5Z"/> + <path className="cls-1" d="M13.25,45.25a3.47,3.47,0,0,1,0-6.94H28.6a3.47,3.47,0,0,1,0,6.94Z"/> + <path className="cls-1" d="M3.47,65.1a3.47,3.47,0,1,1,0-6.93H24.86a3.47,3.47,0,0,1,0,6.93Z"/> + <path className="cls-1" d="M13.43,85.49a3.47,3.47,0,1,1,0-6.94h15.5a3.47,3.47,0,0,1,0,6.94Z"/> + </svg> +); + +export const fastSpeedIcon: JSX.Element = ( + <svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 122.88 104.55"> + <defs><style>{`.cls-1{fill:#fe0000;`}</style></defs> + <path d="M42,82.34a42.82,42.82,0,0,1-4.05-10.13A43.2,43.2,0,0,1,76.72,18.29V11.05c0-.11,0-.22,0-.33H68.65a2.41,2.41,0,0,1-2.41-2.41V2.41A2.41,2.41,0,0,1,68.65,0H93a2.42,2.42,0,0,1,2.42,2.41v5.9A2.42,2.42,0,0,1,93,10.72H84.87c0,.11,0,.22,0,.33V18.5h0A43.17,43.17,0,1,1,42,82.34ZM88.22,49.61l2.66,6.44h.39l2.66-6.44h8.37L96.94,61.26l5.36,12.45H93.74l-2.9-6.94H90.5l-2.86,6.94H79.47l5.24-12.22L79.47,49.61Zm-19,8.48v-2.5a24.92,24.92,0,0,0-3.74-.2A33.25,33.25,0,0,0,59,56.2l-1-5.7A30.47,30.47,0,0,1,67.13,49a22.86,22.86,0,0,1,5.48.47,6.91,6.91,0,0,1,2.5,1.11,5.62,5.62,0,0,1,1.78,4.55,5.84,5.84,0,0,1-3.2,5.56v.19a5.73,5.73,0,0,1,3.81,5.74,8.67,8.67,0,0,1-.63,3.49,6,6,0,0,1-1.6,2.24,7.15,7.15,0,0,1-2.55,1.25,25.64,25.64,0,0,1-6.61.66,37.78,37.78,0,0,1-8.54-1l1.08-6.37a27.22,27.22,0,0,0,6.21.89,35.79,35.79,0,0,0,4.35-.23V65.11l-6.63-.65V58.87l6.63-.78Zm49.27-26.55c1.68-4,1.48-8.19-.92-11-2.88-3.37-8.08-3.76-12.91-1.39a69.74,69.74,0,0,1,13.83,12.38Zm-77.56,0c-1.67-4-1.48-8.19.92-11,2.88-3.37,8.08-3.76,12.91-1.39A70,70,0,0,0,40.92,31.54ZM44.6,70.48A36,36,0,0,0,48,79a35.91,35.91,0,1,0-3.4-8.5Z"/> + <path className="cls-1" d="M13.25,45.25a3.47,3.47,0,0,1,0-6.94H28.6a3.47,3.47,0,0,1,0,6.94Zm.18,40.24a3.47,3.47,0,1,1,0-6.94h15.5a3.47,3.47,0,0,1,0,6.94ZM3.47,65.1a3.47,3.47,0,1,1,0-6.93H24.86a3.47,3.47,0,0,1,0,6.93Z"/> + </svg> +); + diff --git a/src/client/views/nodes/MapBox/AnimationUtility.ts b/src/client/views/nodes/MapBox/AnimationUtility.ts new file mode 100644 index 000000000..35153f439 --- /dev/null +++ b/src/client/views/nodes/MapBox/AnimationUtility.ts @@ -0,0 +1,447 @@ +import * as turf from '@turf/turf'; +import { Position } from '@turf/turf'; +import * as d3 from 'd3'; +import { Feature, GeoJsonProperties, Geometry } from 'geojson'; +import mapboxgl, { MercatorCoordinate } from 'mapbox-gl'; +import { action, computed, makeObservable, observable, runInAction } from 'mobx'; +import { MapRef } from 'react-map-gl'; + +export enum AnimationStatus { + START = 'start', + RESUME = 'resume', + RESTART = 'restart', +} + +export enum AnimationSpeed { + SLOW = '1x', + MEDIUM = '2x', + FAST = '3x', +} + +export class AnimationUtility { + private SMOOTH_FACTOR = 0.95; + private ROUTE_COORDINATES: Position[] = []; + + @observable + private PATH?: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties> = undefined; + + private PATH_DISTANCE: number = 0; + private FLY_IN_START_PITCH = 40; + private FIRST_LNG_LAT: { lng: number; lat: number } = { lng: 0, lat: 0 }; + private START_ALTITUDE = 3_000_000; + private MAP_REF: MapRef | null = null; + + @observable private isStreetViewAnimation: boolean = false; + @observable private animationSpeed: AnimationSpeed = AnimationSpeed.MEDIUM; + + @observable + private previousLngLat: { lng: number; lat: number }; + + private previousAltitude: number | null = null; + private previousPitch: number | null = null; + + private currentStreetViewBearing: number = 0; + + private terrainDisplayed: boolean; + + @computed get flyInEndBearing() { + return this.isStreetViewAnimation + ? this.calculateBearing( + { + lng: this.ROUTE_COORDINATES[0][0], + lat: this.ROUTE_COORDINATES[0][1], + }, + { + lng: this.ROUTE_COORDINATES[1][0], + lat: this.ROUTE_COORDINATES[1][1], + } + ) + : -20; + } + + @computed get currentAnimationAltitude(): number { + if (!this.isStreetViewAnimation) return 20_000; + if (!this.terrainDisplayed) return 50; + const coords: mapboxgl.LngLatLike = [this.previousLngLat.lng, this.previousLngLat.lat]; + // console.log('MAP REF: ', this.MAP_REF) + // console.log("current elevation: ", this.MAP_REF?.queryTerrainElevation(coords)); + let altitude = this.MAP_REF ? this.MAP_REF.queryTerrainElevation(coords) ?? 0 : 0; + if (altitude === 0) { + altitude += 50; + } + if (this.previousAltitude) { + return this.lerp(altitude, this.previousAltitude, 0.02); + } + return altitude; + } + + @computed get flyInStartBearing() { + return Math.max(0, Math.min(this.flyInEndBearing + 20, 360)); // between 0 and 360 + } + + @computed get flyInEndAltitude() { + // return this.isStreetViewAnimation ? (this.currentAnimationAltitude + 70 ): 10_000; + return this.currentAnimationAltitude; + } + + @computed get currentPitch(): number { + if (!this.isStreetViewAnimation) return 50; + if (!this.terrainDisplayed) return 80; + else { + // const groundElevation = 0; + const heightAboveGround = this.currentAnimationAltitude; + const horizontalDistance = 500; + + let pitch; + if (heightAboveGround >= 0) { + pitch = 90 - Math.atan(heightAboveGround / horizontalDistance) * (180 / Math.PI); + } else { + pitch = 80; + } + + console.log(Math.max(50, Math.min(pitch, 85))); + + if (this.previousPitch) { + return this.lerp(Math.max(50, Math.min(pitch, 85)), this.previousPitch, 0.02); + } + return Math.max(50, Math.min(pitch, 85)); + } + } + + @computed get flyInEndPitch() { + return this.currentPitch; + } + + @computed get flyToDuration() { + switch (this.animationSpeed) { + case AnimationSpeed.SLOW: + return 4_000; + case AnimationSpeed.MEDIUM: + return 2_500; + case AnimationSpeed.FAST: + return 1_250; + default: + return 2_500; + } + } + + @computed get animationDuration(): number { + let scalingFactor: number; + const MIN_DISTANCE = 0; + const MAX_DISTANCE = 3_000; // anything greater than 3000 is just set to 1 when normalized + const MAX_DURATION = this.isStreetViewAnimation ? 120_000 : 60_000; + + const normalizedDistance = Math.min(1, (this.PATH_DISTANCE - MIN_DISTANCE) / (MAX_DISTANCE - MIN_DISTANCE)); + const easedDistance = d3.easeExpOut(Math.min(normalizedDistance, 1)); + + switch (this.animationSpeed) { + case AnimationSpeed.SLOW: + scalingFactor = 250; + break; + case AnimationSpeed.MEDIUM: + scalingFactor = 150; + break; + case AnimationSpeed.FAST: + scalingFactor = 85; + break; + default: + scalingFactor = 150; + break; + } + + const duration = Math.min(MAX_DURATION, easedDistance * MAX_DISTANCE * (this.isStreetViewAnimation ? scalingFactor * 1.12 : scalingFactor)); + + return duration; + } + + @action + public updateAnimationSpeed(speed: AnimationSpeed) { + // calculate new flyToDuration and animationDuration + this.animationSpeed = speed; + } + + @action + public updateIsStreetViewAnimation(isStreetViewAnimation: boolean) { + this.isStreetViewAnimation = isStreetViewAnimation; + } + + @action + public setPath = (path: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties>) => { + this.PATH = path; + }; + + constructor(firstLngLat: { lng: number; lat: number }, routeCoordinates: Position[], isStreetViewAnimation: boolean, animationSpeed: AnimationSpeed, terrainDisplayed: boolean, mapRef: MapRef | null) { + makeObservable(this); + this.FIRST_LNG_LAT = firstLngLat; + this.previousLngLat = firstLngLat; + this.isStreetViewAnimation = isStreetViewAnimation; + this.MAP_REF = mapRef; + + this.ROUTE_COORDINATES = routeCoordinates; + this.PATH = turf.lineString(routeCoordinates); + this.PATH_DISTANCE = turf.lineDistance(this.PATH); + this.terrainDisplayed = terrainDisplayed; + + const bearing = this.calculateBearing( + { + lng: routeCoordinates[0][0], + lat: routeCoordinates[0][1], + }, + { + lng: routeCoordinates[1][0], + lat: routeCoordinates[1][1], + } + ); + this.currentStreetViewBearing = bearing; + this.animationSpeed = animationSpeed; + } + + public animatePath = async ({ + map, + // path, + // startBearing, + // startAltitude, + // pitch, + currentAnimationPhase, + updateAnimationPhase, + updateFrameId, + }: { + map: MapRef; + // path: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties>, + // startBearing: number, + // startAltitude: number, + // pitch: number, + currentAnimationPhase: number; + updateAnimationPhase: (newAnimationPhase: number) => void; + updateFrameId: (newFrameId: number) => void; + }) => { + return new Promise<void>(async resolve => { + let startTime: number | null = null; + + const frame = async (currentTime: number) => { + if (!startTime) startTime = currentTime; + const elapsedSinceLastFrame = currentTime - startTime; + const phaseIncrement = elapsedSinceLastFrame / this.animationDuration; + const animationPhase = currentAnimationPhase + phaseIncrement; + + // when the duration is complete, resolve the promise and stop iterating + if (animationPhase > 1) { + resolve(); + return; + } + + if (!this.PATH) return; + // calculate the distance along the path based on the animationPhase + const alongPath = turf.along(this.PATH, this.PATH_DISTANCE * animationPhase).geometry.coordinates; + + const lngLat = { + lng: alongPath[0], + lat: alongPath[1], + }; + + let bearing: number; + if (this.isStreetViewAnimation) { + bearing = this.lerp(this.currentStreetViewBearing, this.calculateBearing(this.previousLngLat, lngLat), 0.032); + this.currentStreetViewBearing = bearing; + // bearing = this.calculateBearing(this.previousLngLat, lngLat); // TODO: Calculate actual bearing + } else { + // slowly rotate the map at a constant rate + bearing = this.flyInEndBearing - animationPhase * 200.0; + // bearing = startBearing - animationPhase * 200.0; + } + + runInAction(() => { + this.previousLngLat = lngLat; + }); + + updateAnimationPhase(animationPhase); + + // compute corrected camera ground position, so that he leading edge of the path is in view + var correctedPosition = this.computeCameraPosition( + this.isStreetViewAnimation, + this.currentPitch, + bearing, + lngLat, + this.currentAnimationAltitude, + true // smooth + ); + + // set the pitch and bearing of the camera + const camera = map.getFreeCameraOptions(); + camera.setPitchBearing(this.currentPitch, bearing); + + // set the position and altitude of the camera + camera.position = MercatorCoordinate.fromLngLat(correctedPosition, this.currentAnimationAltitude); + + // apply the new camera options + map.setFreeCameraOptions(camera); + + this.previousAltitude = this.currentAnimationAltitude; + this.previousPitch = this.previousPitch; + + // repeat! + const innerFrameId = await window.requestAnimationFrame(frame); + updateFrameId(innerFrameId); + }; + + const outerFrameId = await window.requestAnimationFrame(frame); + updateFrameId(outerFrameId); + }); + }; + + public flyInAndRotate = async ({ map, updateFrameId }: { map: MapRef; updateFrameId: (newFrameId: number) => void }) => { + return new Promise<{ bearing: number; altitude: number }>(async resolve => { + let start: number | null; + + var currentAltitude; + var currentBearing; + var currentPitch; + + // the animation frame will run as many times as necessary until the duration has been reached + const frame = async (time: number) => { + if (!start) { + start = time; + } + + // otherwise, use the current time to determine how far along in the duration we are + let animationPhase = (time - start) / this.flyToDuration; + + // because the phase calculation is imprecise, the final zoom can vary + // if it ended up greater than 1, set it to 1 so that we get the exact endAltitude that was requested + if (animationPhase > 1) { + animationPhase = 1; + } + + currentAltitude = this.START_ALTITUDE + (this.flyInEndAltitude - this.START_ALTITUDE) * d3.easeCubicOut(animationPhase); + // rotate the camera between startBearing and endBearing + currentBearing = this.flyInStartBearing + (this.flyInEndBearing - this.flyInStartBearing) * d3.easeCubicOut(animationPhase); + + currentPitch = this.FLY_IN_START_PITCH + (this.flyInEndPitch - this.FLY_IN_START_PITCH) * d3.easeCubicOut(animationPhase); + + // compute corrected camera ground position, so the start of the path is always in view + var correctedPosition = this.computeCameraPosition(false, currentPitch, currentBearing, this.FIRST_LNG_LAT, currentAltitude); + + // set the pitch and bearing of the camera + const camera = map.getFreeCameraOptions(); + camera.setPitchBearing(currentPitch, currentBearing); + + // set the position and altitude of the camera + camera.position = MercatorCoordinate.fromLngLat(correctedPosition, currentAltitude); + + // apply the new camera options + map.setFreeCameraOptions(camera); + + // when the animationPhase is done, resolve the promise so the parent function can move on to the next step in the sequence + if (animationPhase === 1) { + resolve({ + bearing: currentBearing, + altitude: currentAltitude, + }); + + // return so there are no further iterations of this frame + return; + } + + const innerFrameId = await window.requestAnimationFrame(frame); + updateFrameId(innerFrameId); + }; + + const outerFrameId = await window.requestAnimationFrame(frame); + updateFrameId(outerFrameId); + }); + }; + + previousCameraPosition: { lng: number; lat: number } | null = null; + + lerp = (start: number, end: number, amt: number) => { + return (1 - amt) * start + amt * end; + }; + + computeCameraPosition = (isStreetViewAnimation: boolean, pitch: number, bearing: number, targetPosition: { lng: number; lat: number }, altitude: number, smooth = false) => { + const bearingInRadian = (bearing * Math.PI) / 180; + const pitchInRadian = ((90 - pitch) * Math.PI) / 180; + + let correctedLng = targetPosition.lng; + let correctedLat = targetPosition.lat; + + if (!isStreetViewAnimation) { + const lngDiff = ((altitude / Math.tan(pitchInRadian)) * Math.sin(-bearingInRadian)) / 70000; // ~70km/degree longitude + const latDiff = ((altitude / Math.tan(pitchInRadian)) * Math.cos(-bearingInRadian)) / 110000; // 110km/degree latitude + + correctedLng = targetPosition.lng + lngDiff; + correctedLat = targetPosition.lat - latDiff; + } + + const newCameraPosition = { + lng: correctedLng, + lat: correctedLat, + }; + + if (smooth) { + if (this.previousCameraPosition) { + newCameraPosition.lng = this.lerp(newCameraPosition.lng, this.previousCameraPosition.lng, this.SMOOTH_FACTOR); + newCameraPosition.lat = this.lerp(newCameraPosition.lat, this.previousCameraPosition.lat, this.SMOOTH_FACTOR); + } + } + + this.previousCameraPosition = newCameraPosition; + + return newCameraPosition; + }; + + public static createGeoJSONCircle = (center: number[], radiusInKm: number, points = 64): Feature<Geometry, GeoJsonProperties> => { + const coords = { + latitude: center[1], + longitude: center[0], + }; + const km = radiusInKm; + const ret = []; + const distanceX = km / (111.32 * Math.cos((coords.latitude * Math.PI) / 180)); + const distanceY = km / 110.574; + let theta; + let x; + let y; + for (let i = 0; i < points; i += 1) { + theta = (i / points) * (2 * Math.PI); + x = distanceX * Math.cos(theta); + y = distanceY * Math.sin(theta); + ret.push([coords.longitude + x, coords.latitude + y]); + } + ret.push(ret[0]); + return { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [ret], + }, + properties: {}, + }; + }; + + private calculateBearing(from: { lng: number; lat: number }, to: { lng: number; lat: number }): number { + const lon1 = from.lng; + const lat1 = from.lat; + const lon2 = to.lng; + const lat2 = to.lat; + + const lon1Rad = (lon1 * Math.PI) / 180; + const lon2Rad = (lon2 * Math.PI) / 180; + const lat1Rad = (lat1 * Math.PI) / 180; + const lat2Rad = (lat2 * Math.PI) / 180; + + const y = Math.sin(lon2Rad - lon1Rad) * Math.cos(lat2Rad); + const x = Math.cos(lat1Rad) * Math.sin(lat2Rad) - Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(lon2Rad - lon1Rad); + + let bearing = Math.atan2(y, x); + + // Convert bearing from radians to degrees + bearing = (bearing * 180) / Math.PI; + + // Ensure the bearing is within [0, 360) + if (bearing < 0) { + bearing += 360; + } + + return bearing; + } +} diff --git a/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx new file mode 100644 index 000000000..7e99795b5 --- /dev/null +++ b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx @@ -0,0 +1,122 @@ +import { IconLookup, faAdd, faCalendarDays, faRoute } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { IconButton } from 'browndash-components'; +import { IReactionDisposer, ObservableMap, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { returnFalse, unimplementedFunction } from '../../../../Utils'; +import { Doc, Opt } from '../../../../fields/Doc'; +import { NumCast, StrCast } from '../../../../fields/Types'; +import { SelectionManager } from '../../../util/SelectionManager'; +import { SettingsManager } from '../../../util/SettingsManager'; +import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; + +@observer +export class DirectionsAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { + static Instance: DirectionsAnchorMenu; + + private _disposer: IReactionDisposer | undefined; + + public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search + + public Center: () => void = unimplementedFunction; + public OnClick: (e: PointerEvent) => void = unimplementedFunction; + // public OnAudio: (e: PointerEvent) => void = unimplementedFunction; + public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; + public Highlight: (color: string, isTargetToggler: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => Opt<Doc> = (color: string, isTargetToggler: boolean) => undefined; + public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => undefined; + public Delete: () => void = unimplementedFunction; + // public MakeTargetToggle: () => void = unimplementedFunction; + // public ShowTargetTrail: () => void = unimplementedFunction; + public IsTargetToggler: () => boolean = returnFalse; + + private title: string | undefined = undefined; + + public setPinDoc(pinDoc: Doc) { + this.title = StrCast(pinDoc.title ? pinDoc.title : `${NumCast(pinDoc.longitude)}, ${NumCast(pinDoc.latitude)}`); + console.log('Title: ', this.title); + } + + public get Active() { + return this._left > 0; + } + + constructor(props: Readonly<{}>) { + super(props); + + DirectionsAnchorMenu.Instance = this; + DirectionsAnchorMenu.Instance._canFade = false; + } + + componentWillUnmount() { + this._disposer?.(); + } + + componentDidMount() { + this._disposer = reaction( + () => SelectionManager.Views.slice(), + sel => DirectionsAnchorMenu.Instance.fadeOut(true) + ); + } + // audioDown = (e: React.PointerEvent) => { + // setupMoveUpEvents(this, e, returnFalse, returnFalse, e => this.OnAudio?.(e)); + // }; + + // cropDown = (e: React.PointerEvent) => { + // setupMoveUpEvents( + // this, + // e, + // (e: PointerEvent) => { + // this.StartCropDrag(e, this._commentCont.current!); + // return true; + // }, + // returnFalse, + // e => this.OnCrop?.(e) + // ); + // }; + // notePointerDown = (e: React.PointerEvent) => { + // setupMoveUpEvent( + // this, + // e, + // (e: PointerEvent) => { + // this.StartDrag(e, this._commentRef.current!); + // return true; + // }, + // returnFalse, + // e => this.OnClick(e) + // ); + // }; + + static top = React.createRef<HTMLDivElement>(); + + // public get Top(){ + // return this.top + // } + + render() { + const buttons = ( + <div className="directions-menu-buttons" style={{ display: 'flex' }}> + <IconButton + tooltip="Add route" // + onPointerDown={this.Delete} + icon={<FontAwesomeIcon icon={faAdd as IconLookup} />} + color={SettingsManager.userColor} + /> + + <IconButton tooltip="Animate route" onPointerDown={this.Delete} /**TODO: fix */ icon={<FontAwesomeIcon icon={faRoute as IconLookup} />} color={SettingsManager.userColor} /> + <IconButton tooltip="Add to calendar" onPointerDown={this.Delete} /**TODO: fix */ icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup} />} color={SettingsManager.userColor} /> + </div> + ); + + return this.getElement( + <div ref={DirectionsAnchorMenu.top} style={{ height: 'max-content', width: '100%', display: 'flex', flexDirection: 'column' }}> + <div>{this.title}</div> + <div className="direction-inputs" style={{ display: 'flex', flexDirection: 'column' }}> + <input placeholder="Origin" /> + <input placeholder="Destination" /> + </div> + {buttons} + </div> + ); + } +} diff --git a/src/client/views/nodes/MapBox/GeocoderControl.tsx b/src/client/views/nodes/MapBox/GeocoderControl.tsx new file mode 100644 index 000000000..e4ba51316 --- /dev/null +++ b/src/client/views/nodes/MapBox/GeocoderControl.tsx @@ -0,0 +1,107 @@ +// import React from 'react'; +// import MapboxGeocoder , { GeocoderOptions} from '@mapbox/mapbox-gl-geocoder' +// import { ControlPosition, MarkerProps, useControl } from "react-map-gl"; + +// import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css' + + +// export type GeocoderControlProps = Omit<GeocoderOptions, 'accessToken' | 'mapboxgl' | 'marker'> & { +// mapboxAccessToken: string; +// marker?: Omit<MarkerProps, 'longitude' | 'latitude'>; +// position: ControlPosition; + +// onLoading: (...args: any[]) => void; +// onResults: (...args: any[]) => void; +// onResult: (...args: any[]) => void; +// onError: (...args: any[]) => void; +// } + +// export const GeocoderControl = (props: GeocoderControlProps) => { + +// console.log(props); + +// const geocoder = useControl<MapboxGeocoder>( +// () => { +// const ctrl = new MapboxGeocoder({ +// ...props, +// marker: false, +// accessToken: props.mapboxAccessToken +// }); +// ctrl.on('loading', props.onLoading); +// ctrl.on('results', props.onResults); +// ctrl.on('result', evt => { +// props.onResult(evt); + +// // const {result} = evt; +// // const location = +// // result && +// // (result.center || (result.geometry?.type === 'Point' && result.geometry.coordinates)); +// // if (location && props.marker) { +// // setMarker(<Marker {...props.marker} longitude={location[0]} latitude={location[1]} />); +// // } else { +// // setMarker(null); +// // } +// }); +// ctrl.on('error', props.onError); +// return ctrl; +// }, +// { +// position: props.position +// } +// ); + + +// // @ts-ignore (TS2339) private member +// if (geocoder._map) { +// if (geocoder.getProximity() !== props.proximity && props.proximity !== undefined) { +// geocoder.setProximity(props.proximity); +// } +// if (geocoder.getRenderFunction() !== props.render && props.render !== undefined) { +// geocoder.setRenderFunction(props.render); +// } +// if (geocoder.getLanguage() !== props.language && props.language !== undefined) { +// geocoder.setLanguage(props.language); +// } +// if (geocoder.getZoom() !== props.zoom && props.zoom !== undefined) { +// geocoder.setZoom(props.zoom); +// } +// if (geocoder.getFlyTo() !== props.flyTo && props.flyTo !== undefined) { +// geocoder.setFlyTo(props.flyTo); +// } +// if (geocoder.getPlaceholder() !== props.placeholder && props.placeholder !== undefined) { +// geocoder.setPlaceholder(props.placeholder); +// } +// if (geocoder.getCountries() !== props.countries && props.countries !== undefined) { +// geocoder.setCountries(props.countries); +// } +// if (geocoder.getTypes() !== props.types && props.types !== undefined) { +// geocoder.setTypes(props.types); +// } +// if (geocoder.getMinLength() !== props.minLength && props.minLength !== undefined) { +// geocoder.setMinLength(props.minLength); +// } +// if (geocoder.getLimit() !== props.limit && props.limit !== undefined) { +// geocoder.setLimit(props.limit); +// } +// if (geocoder.getFilter() !== props.filter && props.filter !== undefined) { +// geocoder.setFilter(props.filter); +// } +// if (geocoder.getOrigin() !== props.origin && props.origin !== undefined) { +// geocoder.setOrigin(props.origin); +// } +// } +// return ( +// <div> +// Geocoder +// </div> +// ) +// } + +// const noop = () => {}; + +// GeocoderControl.defaultProps = { +// marker: true, +// onLoading: noop, +// onResults: noop, +// onError: noop +// };
\ No newline at end of file diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.scss b/src/client/views/nodes/MapBox/MapAnchorMenu.scss index 6990bdcf1..c36d98afe 100644 --- a/src/client/views/nodes/MapBox/MapAnchorMenu.scss +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.scss @@ -51,4 +51,81 @@ border: 2px solid white; } } -}
\ No newline at end of file +} + +.map-anchor-menu-container { + display: flex; + flex-direction: column; + gap: 5px; + padding: 5px; + height: max-content; + min-width: 300px; + + .direction-inputs { + display: flex; + flex-direction: column; + gap: 5px; + + #get-routes-button { + padding: 8px 10px; + border-radius: 5px; + } + } + + .MuiInputBase-input{ + color: white !important; + } + + + .css-1t8l2tu-MuiInputBase-input-MuiOutlinedInput-input.Mui-disabled{ + -webkit-text-fill-color: #b3b2b2 !important; + } + + .current-route-info-container { + width: 100%; + + .transportation-icons-container { + display: flex; + justify-content: center; + align-items: center; + gap: 5px; + } + + .selected-route-details-container{ + display: flex; + flex-direction: column; + gap: 3px; + justify-content: center; + align-items: flex-start; + padding: 5px; + } + + + } + + .customized-marker-container{ + display: flex; + flex-direction: column; + gap: 10px; + + .current-marker-container{ + display: flex; + align-items: center; + gap: 5px; + } + + .all-markers-container{ + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + max-width: 400px; + } + } + + + + +} + + diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx index 7af4d9b59..08bea5d9d 100644 --- a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx @@ -1,15 +1,25 @@ -import React = require('react'); +import { IconLookup, faAdd, faArrowDown, faArrowLeft, faArrowsRotate, faBicycle, faCalendarDays, faCar, faDiamondTurnRight, faEdit, faPersonWalking, faRoute } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { IReactionDisposer, ObservableMap, reaction } from 'mobx'; +import { Autocomplete, Checkbox, FormControlLabel, TextField } from '@mui/material'; +import { IconButton } from 'browndash-components'; +import { Position } from 'geojson'; +import { IReactionDisposer, ObservableMap, action, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, Opt } from '../../../../fields/Doc'; +import * as React from 'react'; +import { CirclePicker, ColorResult } from 'react-color'; import { returnFalse, setupMoveUpEvents, unimplementedFunction } from '../../../../Utils'; +import { Doc, Opt } from '../../../../fields/Doc'; +import { NumCast, StrCast } from '../../../../fields/Types'; +import { CalendarManager } from '../../../util/CalendarManager'; import { SelectionManager } from '../../../util/SelectionManager'; -import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; -// import { GPTPopup, GPTPopupMode } from './../../GPTPopup/GPTPopup'; -import { IconButton } from 'browndash-components'; import { SettingsManager } from '../../../util/SettingsManager'; +import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import './MapAnchorMenu.scss'; +import { MapboxApiUtility, TransportationType } from './MapboxApiUtility'; +import { MarkerIcons } from './MarkerIcons'; +// import { GPTPopup, GPTPopupMode } from './../../GPTPopup/GPTPopup'; + +type MapAnchorMenuType = 'standard' | 'routeCreation' | 'calendar' | 'customize' | 'route'; @observer export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -17,6 +27,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { private _disposer: IReactionDisposer | undefined; private _commentRef = React.createRef<HTMLDivElement>(); + private _fileInputRef = React.createRef<HTMLInputElement>(); public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search @@ -30,25 +41,90 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { // public MakeTargetToggle: () => void = unimplementedFunction; // public ShowTargetTrail: () => void = unimplementedFunction; public IsTargetToggler: () => boolean = returnFalse; + + public DisplayRoute: (routeInfoMap: Record<TransportationType, any> | undefined, type: TransportationType) => void = unimplementedFunction; + public AddNewRouteToMap: (coordinates: Position[], origin: string, destination: any, createPinForDestination: boolean) => void = unimplementedFunction; + public CreatePin: (feature: any) => void = unimplementedFunction; + + public UpdateMarkerColor: (color: string) => void = unimplementedFunction; + public UpdateMarkerIcon: (iconKey: string) => void = unimplementedFunction; + + public Hide: () => void = unimplementedFunction; + + public OpenAnimationPanel: (routeDoc: Doc | undefined) => void = unimplementedFunction; + + @observable + menuType: MapAnchorMenuType = 'standard'; + + @action + public setMenuType = (menuType: MapAnchorMenuType) => { + this.menuType = menuType; + }; + + private allMapPinDocs: Doc[] = []; + + private pinDoc: Doc | undefined = undefined; + + private routeDoc: Doc | undefined = undefined; + + private title: string | undefined = undefined; + + public setPinDoc(pinDoc: Doc | undefined) { + if (pinDoc) { + this.pinDoc = pinDoc; + this.title = StrCast(pinDoc.title ? pinDoc.title : `${NumCast(pinDoc.longitude)}, ${NumCast(pinDoc.latitude)}`); + } + } + + public setRouteDoc(routeDoc: Doc | undefined) { + if (routeDoc) { + this.routeDoc = routeDoc; + this.title = StrCast(routeDoc.title ?? 'Map route'); + } + } + + @action + public Reset() { + this.destinationSelected = false; + this.currentRouteInfoMap = undefined; + this.destinationFeatures = []; + this.selectedDestinationFeature = undefined; + this.allMapPinDocs = []; + this.title = undefined; + this.routeDoc = undefined; + this.pinDoc = undefined; + } + + public setAllMapboxPins(pinDocs: Doc[]) { + this.allMapPinDocs = pinDocs; + pinDocs.forEach((p, idx) => { + console.log(`Pin ${idx}: ${p.title}`); + }); + } + public get Active() { return this._left > 0; } - constructor(props: Readonly<{}>) { + constructor(props: any) { super(props); - + makeObservable(this); MapAnchorMenu.Instance = this; MapAnchorMenu.Instance._canFade = false; } componentWillUnmount() { + this.destinationFeatures = []; + this.destinationSelected = false; + this.selectedDestinationFeature = undefined; + this.currentRouteInfoMap = undefined; this._disposer?.(); } componentDidMount() { this._disposer = reaction( - () => SelectionManager.Views().slice(), - selected => MapAnchorMenu.Instance.fadeOut(true) + () => SelectionManager.Views.slice(), + sel => MapAnchorMenu.Instance.fadeOut(true) ); } // audioDown = (e: React.PointerEvent) => { @@ -81,39 +157,267 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { }; static top = React.createRef<HTMLDivElement>(); + // public get Top(){ // return this.top // } + @action + DirectionsClick = () => { + this.menuType = 'routeCreation'; + }; + + @action + CustomizeClick = () => { + this.currentRouteInfoMap = undefined; + this.menuType = 'customize'; + }; + + @action + BackClick = () => { + this.currentRouteInfoMap = undefined; + this.menuType = 'standard'; + }; + + @action + TriggerFileInputClick = () => { + if (this._fileInputRef) { + this._fileInputRef.current?.click(); // Trigger the file input click event + } + }; + + @action + onMarkerColorChange = (color: ColorResult) => { + if (this.pinDoc) { + this.pinDoc.markerColor = color.hex; + } + }; + + revertToOriginalMarker = () => { + if (this.pinDoc) { + this.pinDoc.markerType = 'MAP_PIN'; + this.pinDoc.markerColor = '#ff5722'; + } + }; + + onMarkerIconChange = (iconKey: string) => { + if (this.pinDoc) { + this.pinDoc.markerType = iconKey; + } + }; + + @observable + destinationFeatures: any[] = []; + + @observable + destinationSelected: boolean = false; + + @observable + selectedDestinationFeature: any = undefined; + + @observable + createPinForDestination: boolean = true; + + @observable + currentRouteInfoMap: Record<TransportationType, any> | undefined = undefined; + + @observable + selectedTransportationType: TransportationType = 'driving'; + + @action + handleTransportationTypeChange = (newType: TransportationType) => { + if (newType !== this.selectedTransportationType) { + this.selectedTransportationType = newType; + this.DisplayRoute(this.currentRouteInfoMap, newType); + } + }; + + @action + handleSelectedDestinationFeature = (destinationFeature: any) => { + this.selectedDestinationFeature = destinationFeature; + }; + + @action + toggleCreatePinForDestinationCheckbox = () => { + this.createPinForDestination = !this.createPinForDestination; + }; + + @action + handleDestinationSearchChange = async (searchText: string) => { + if (this.selectedDestinationFeature !== undefined) this.selectedDestinationFeature = undefined; + const features = await MapboxApiUtility.forwardGeocodeForFeatures(searchText); + if (features) { + runInAction(() => { + this.destinationFeatures = features; + }); + } + }; + + getRoutes = async (destinationFeature: any) => { + const currentPinLong: number = NumCast(this.pinDoc?.longitude); + const currentPinLat: number = NumCast(this.pinDoc?.latitude); + + if (currentPinLong && currentPinLat && destinationFeature.center) { + const routeInfoMap = await MapboxApiUtility.getDirections([currentPinLong, currentPinLat], destinationFeature.center); + if (routeInfoMap) { + runInAction(() => { + this.currentRouteInfoMap = routeInfoMap; + }); + this.DisplayRoute(routeInfoMap, 'driving'); + } + } + + // get route menu, set it equal to here + // create a temporary route + // create pin if createPinForDestination was clicked + }; + + HandleAddRouteClick = () => { + if (this.currentRouteInfoMap && this.selectedTransportationType && this.selectedDestinationFeature) { + const coordinates = this.currentRouteInfoMap[this.selectedTransportationType].coordinates; + console.log(coordinates); + console.log(this.selectedDestinationFeature); + this.AddNewRouteToMap(coordinates, this.title ?? '', this.selectedDestinationFeature, this.createPinForDestination); + } + }; + + getMarkerIcon = (): JSX.Element | undefined => { + if (this.pinDoc) { + const markerType = StrCast(this.pinDoc.markerType); + const markerColor = StrCast(this.pinDoc.markerColor); + + return MarkerIcons.getFontAwesomeIcon(markerType, '2x', markerColor); + } + return undefined; + }; + + getDirectionsButton: JSX.Element = (<IconButton tooltip="Get directions" onPointerDown={this.DirectionsClick} icon={<FontAwesomeIcon icon={faDiamondTurnRight as IconLookup} />} color={SettingsManager.userColor} />); + + getAddToCalendarButton = (docType: string): JSX.Element => { + return ( + <IconButton + tooltip="Add to calendar" + onPointerDown={() => { + CalendarManager.Instance.open(undefined, docType === 'pin' ? this.pinDoc : this.routeDoc); + }} + icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup} />} + color={SettingsManager.userColor} + /> + ); + }; + addToCalendarButton: JSX.Element = ( + <IconButton tooltip="Add to calendar" onPointerDown={() => CalendarManager.Instance.open(undefined, this.pinDoc)} icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup} />} color={SettingsManager.userColor} /> + ); + + getLinkNoteToDocButton = (docType: string): JSX.Element => { + return ( + <div ref={this._commentRef}> + <IconButton + tooltip={`Link Note to ${docType === 'pin' ? 'Pin' : 'Route'}`} // + onPointerDown={this.notePointerDown} + icon={<FontAwesomeIcon icon="sticky-note" />} + color={SettingsManager.userColor} + /> + </div> + ); + }; + + linkNoteToPinOrRoutenButton: JSX.Element = ( + <div ref={this._commentRef}> + <IconButton + tooltip="Link Note to Pin" // + onPointerDown={this.notePointerDown} + icon={<FontAwesomeIcon icon="sticky-note" />} + color={SettingsManager.userColor} + /> + </div> + ); + + customizePinButton: JSX.Element = (<IconButton tooltip="Customize pin" onPointerDown={this.CustomizeClick} icon={<FontAwesomeIcon icon={faEdit as IconLookup} />} color={SettingsManager.userColor} />); + + centerOnPinButton: JSX.Element = ( + <IconButton + tooltip="Center on pin" // + onPointerDown={this.Center} + icon={<FontAwesomeIcon icon="compress-arrows-alt" />} + color={SettingsManager.userColor} + /> + ); + + backButton: JSX.Element = ( + <IconButton + tooltip="Go back" // + onPointerDown={this.BackClick} + icon={<FontAwesomeIcon icon={faArrowLeft as IconLookup} />} + color={SettingsManager.userColor} + /> + ); + + addRouteButton: JSX.Element = ( + <IconButton + tooltip="Add route" // + onPointerDown={this.HandleAddRouteClick} + icon={<FontAwesomeIcon icon={faAdd as IconLookup} />} + color={SettingsManager.userColor} + /> + ); + + getDeleteButton = (type: string) => { + return ( + <IconButton + tooltip={`Delete ${type === 'pin' ? 'Pin' : 'Route'}`} // + onPointerDown={this.Delete} + icon={<FontAwesomeIcon icon="trash-alt" />} + color={SettingsManager.userColor} + /> + ); + }; + + animateRouteButton: JSX.Element = (<IconButton tooltip="Animate route" onPointerDown={() => this.OpenAnimationPanel(this.routeDoc)} icon={<FontAwesomeIcon icon={faRoute as IconLookup} />} color={SettingsManager.userColor} />); + + revertToOriginalMarkerButton = ( + <IconButton + tooltip="Revert to original" // + onPointerDown={() => this.revertToOriginalMarker()} + icon={<FontAwesomeIcon icon={faArrowsRotate as IconLookup} />} + color={SettingsManager.userColor} + /> + ); + render() { const buttons = ( - <> - { - <IconButton - tooltip="Delete Pin" // - onPointerDown={this.Delete} - icon={<FontAwesomeIcon icon="trash-alt" />} - color={SettingsManager.userColor} - /> - } - { - <div ref={this._commentRef}> - <IconButton - tooltip="Link Note to Pin" // - onPointerDown={this.notePointerDown} - icon={<FontAwesomeIcon icon="sticky-note" />} - color={SettingsManager.userColor} - /> - </div> - } - { - <IconButton - tooltip="Center on pin" // - onPointerDown={this.Center} - icon={<FontAwesomeIcon icon="compress-arrows-alt" />} - color={SettingsManager.userColor} - /> - } + <div className="menu-buttons" style={{ display: 'flex' }}> + {this.menuType === 'standard' && ( + <> + {this.getDeleteButton('pin')} + {this.getDirectionsButton} + {this.getAddToCalendarButton('pin')} + {this.getLinkNoteToDocButton('pin')} + {this.customizePinButton} + {this.centerOnPinButton} + </> + )} + {this.menuType === 'routeCreation' && ( + <> + {this.backButton} + {this.addRouteButton} + </> + )} + {this.menuType === 'route' && ( + <> + {this.getDeleteButton('route')} + {this.animateRouteButton} + {this.getAddToCalendarButton('route')} + {this.getLinkNoteToDocButton('route')} + </> + )} + {this.menuType === 'customize' && ( + <> + {this.backButton} + {this.revertToOriginalMarkerButton} + </> + )} + {/* {this.IsTargetToggler !== returnFalse && ( <Toggle tooltip={'Make target visibility toggle on click'} @@ -125,13 +429,100 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { color={SettingsManager.userColor} /> )} */} - </> + </div> ); return this.getElement( - <div ref={MapAnchorMenu.top} style={{ width: '100%', display: 'flex' }}> + <div ref={MapAnchorMenu.top} className="map-anchor-menu-container"> + {this.menuType === 'standard' && <div>{this.title}</div>} + {this.menuType === 'routeCreation' && ( + <div className="direction-inputs" style={{ display: 'flex', flexDirection: 'column' }}> + <TextField fullWidth disabled value={this.title} /> + <FontAwesomeIcon icon={faArrowDown as IconLookup} size="xs" /> + <Autocomplete + fullWidth + id="route-destination-searcher" + onInputChange={(e: any, searchText: any) => this.handleDestinationSearchChange(searchText)} + onChange={(e: any, feature: any, reason: any) => { + if (reason === 'clear') { + this.handleSelectedDestinationFeature(undefined); + } else if (reason === 'selectOption') { + this.handleSelectedDestinationFeature(feature); + } + }} + options={this.destinationFeatures.filter(feature => feature.place_name).map(feature => feature)} + getOptionLabel={(feature: any) => feature.place_name} + renderInput={(params: any) => <TextField {...params} placeholder="Enter a destination" />} + /> + {this.selectedDestinationFeature && ( + <> + {!this.allMapPinDocs.some(pinDoc => pinDoc.title === this.selectedDestinationFeature.place_name) && ( + <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '5px' }}> + <FormControlLabel label="Create pin for destination?" control={<Checkbox color="success" checked={this.createPinForDestination} onChange={this.toggleCreatePinForDestinationCheckbox} />} /> + </div> + )} + </> + )} + <button id="get-routes-button" disabled={this.selectedDestinationFeature ? false : true} onClick={() => this.getRoutes(this.selectedDestinationFeature)}> + Get routes + </button> + + {/* <input + placeholder="Origin" + /> */} + </div> + )} + {this.currentRouteInfoMap && ( + <div className="current-route-info-container"> + <div className="transportation-icons-container"> + <IconButton + tooltip="Driving route" + onPointerDown={() => this.handleTransportationTypeChange('driving')} + icon={<FontAwesomeIcon icon={faCar as IconLookup} />} + color={this.selectedTransportationType === 'driving' ? 'lightblue' : 'grey'} + /> + <IconButton + tooltip="Cycling route" + onPointerDown={() => this.handleTransportationTypeChange('cycling')} + icon={<FontAwesomeIcon icon={faBicycle as IconLookup} />} + color={this.selectedTransportationType === 'cycling' ? 'lightblue' : 'grey'} + /> + <IconButton + tooltip="Walking route" + onPointerDown={() => this.handleTransportationTypeChange('walking')} + icon={<FontAwesomeIcon icon={faPersonWalking as IconLookup} />} + color={this.selectedTransportationType === 'walking' ? 'lightblue' : 'grey'} + /> + </div> + <div className="selected-route-details-container"> + <div>Duration: {this.currentRouteInfoMap[this.selectedTransportationType].duration}</div> + <div>Distance: {this.currentRouteInfoMap[this.selectedTransportationType].distance}</div> + </div> + </div> + )} + {this.menuType === 'customize' && ( + <div className="customized-marker-container"> + <div className="current-marker-container"> + <div>Current Marker: </div> + <div>{this.getMarkerIcon()}</div> + </div> + <div className="color-picker-container" style={{ marginBottom: '10px' }}> + <CirclePicker circleSize={15} circleSpacing={7} width="100%" onChange={color => this.onMarkerColorChange(color)} /> + </div> + <div className="all-markers-container"> + {Object.keys(MarkerIcons.FAMarkerIconsMap).map(iconKey => ( + <div key={iconKey} className="marker-icon"> + <IconButton onPointerDown={() => this.onMarkerIconChange(iconKey)} icon={MarkerIcons.getFontAwesomeIcon(iconKey, '1x', 'white')} /> + </div> + ))} + </div> + <div style={{ width: '100%', height: '3px', color: 'white' }}></div> + </div> + )} + {this.menuType === 'route' && this.routeDoc && <div>{StrCast(this.routeDoc.title)}</div>} {buttons} - </div> + </div>, + true ); } } diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss index 242677231..25b4587a5 100644 --- a/src/client/views/nodes/MapBox/MapBox.scss +++ b/src/client/views/nodes/MapBox/MapBox.scss @@ -1,9 +1,17 @@ -@import '../../global/globalCssVariables.scss'; +@import '../../global/globalCssVariables.module.scss'; .mapBox { width: 100%; height: 100%; overflow: hidden; display: flex; + position: absolute; + + .mapboxgl-map { + overflow: unset !important; + } + .mapboxgl-ctrl { + display: none !important; + } .mapBox-infoWindow { background-color: white; @@ -14,15 +22,133 @@ .mapBox-searchbar { display: flex; flex-direction: row; + gap: 5px; + align-items: center; width: calc(100% - 40px); - .editableText-container { + + // .editableText-container { + // width: 100%; + // font-size: 16px !important; + // } + // input { + // width: 100%; + // } + } + + .mapbox-settings-panel { + z-index: 900; + padding: 10px 20px; + display: flex; + background-color: rgb(187, 187, 187); + font-size: 1.3em; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 7px; + position: absolute; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + + .mapbox-style-select { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 4px; + } + + .mapbox-terrain-selection { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 4px; + } + } + + .mapbox-geocoding-search-results { + z-index: 900; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + position: absolute; + background-color: rgb(187, 187, 187); + font-size: 1.4em; + padding: 10px; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + + .search-result-container { width: 100%; - font-size: 16px !important; + padding: 10px; + &:hover { + background-color: lighten(rgb(187, 187, 187), 10%); + } } - input { + } + + .animation-panel { + z-index: 900; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + background-color: rgb(187, 187, 187); + padding: 10px; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + position: absolute; + + #route-to-animate-title { + font-size: 1.25em; + font-weight: bold; + } + + .route-animation-options { + display: flex; + justify-content: flex-start; + align-items: center; width: 100%; + + .animation-suboptions { + display: flex; + justify-content: flex-start; + align-items: center; + gap: 7px; + width: 100%; + + label { + margin-bottom: 0; + } + + .speed-label { + margin-right: 5px; + } + + #divider { + margin-left: 10px; + margin-right: 10px; + } + } } } + + .zoom-box { + position: absolute; + z-index: 900; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: white; + font-size: 1.4em; + border-radius: 5px; + bottom: 5px; + left: 5px; + padding: 3px; + } + .mapBox-topbar { display: flex; flex-direction: row; diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index 50b070e7f..c185c66fc 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -1,34 +1,44 @@ +import { IconLookup, faCircleXmark, faGear, faPause, faPlay, faRotate } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import BingMapsReact from 'bingmaps-react'; -import { Button, EditableText, IconButton, Type } from 'browndash-components'; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; +import { Checkbox, FormControlLabel, TextField } from '@mui/material'; +import * as turf from '@turf/turf'; +import { IconButton, Size, Type } from 'browndash-components'; +import * as d3 from 'd3'; +import { Feature, FeatureCollection, GeoJsonProperties, Geometry, LineString, Position } from 'geojson'; +import mapboxgl, { LngLat, LngLatBoundsLike, MapLayerMouseEvent } from 'mapbox-gl'; +import { IReactionDisposer, ObservableMap, action, autorun, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +import { CirclePicker, ColorResult } from 'react-color'; +import { Layer, MapProvider, MapRef, Map as MapboxMap, Marker, Source, ViewState, ViewStateChangeEvent } from 'react-map-gl'; +import { MarkerEvent } from 'react-map-gl/dist/esm/types'; +import { Utils, emptyFunction, setupMoveUpEvents } from '../../../../Utils'; import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc'; -import { DocCss, Highlight, Width } from '../../../../fields/DocSymbols'; -import { Id } from '../../../../fields/FieldSymbols'; -import { InkTool } from '../../../../fields/InkField'; +import { DocCss, Highlight } from '../../../../fields/DocSymbols'; import { DocCast, NumCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils'; -import { Docs, DocUtils } from '../../../documents/Documents'; import { DocumentType } from '../../../documents/DocumentTypes'; +import { DocUtils, Docs } from '../../../documents/Documents'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; import { LinkManager } from '../../../util/LinkManager'; import { SnappingManager } from '../../../util/SnappingManager'; -import { Transform } from '../../../util/Transform'; -import { undoable, UndoManager } from '../../../util/UndoManager'; +import { UndoManager, undoable } from '../../../util/UndoManager'; +import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent'; +import { SidebarAnnos } from '../../SidebarAnnos'; import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm'; -import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; -import { MarqueeAnnotator } from '../../MarqueeAnnotator'; -import { SidebarAnnos } from '../../SidebarAnnos'; import { DocumentView } from '../DocumentView'; -import { FieldView, FieldViewProps } from '../FieldView'; +import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView'; import { FormattedTextBox } from '../formattedText/FormattedTextBox'; import { PinProps, PresBox } from '../trails'; +import { fastSpeedIcon, mediumSpeedIcon, slowSpeedIcon } from './AnimationSpeedIcons'; +import { AnimationSpeed, AnimationStatus, AnimationUtility } from './AnimationUtility'; import { MapAnchorMenu } from './MapAnchorMenu'; import './MapBox.scss'; +import { MapboxApiUtility, TransportationType } from './MapboxApiUtility'; +import { MarkerIcons } from './MarkerIcons'; +// import { GeocoderControl } from './GeocoderControl'; + // amongus /** * MapBox architecture: @@ -44,6 +54,30 @@ import './MapBox.scss'; */ const bingApiKey = process.env.BING_MAPS; // if you're running local, get a Bing Maps api key here: https://www.bingmapsportal.com/ and then add it to the .env file in the Dash-Web root directory as: _CLIENT_BING_MAPS=<your apikey> +const MAPBOX_ACCESS_TOKEN = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNscHgwNDd1MDA3MXIydm92ODdianp6cGYifQ.WFAqbhwxtMHOWSPtu0l2uQ'; +const MAPBOX_FORWARD_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/'; + +const MAPBOX_REVERSE_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/'; + +type PopupInfo = { + longitude: number; + latitude: number; + title: string; + description: string; +}; + +// export type GeocoderControlProps = Omit<GeocoderOptions, 'accessToken' | 'mapboxgl' | 'marker'> & { +// mapboxAccessToken: string; +// marker?: Omit<MarkerProps, 'longitude' | 'latitude'>; +// position: ControlPosition; + +// onResult: (...args: any[]) => void; +// }; + +type MapMarker = { + longitude: number; + latitude: number; +}; /** * Consider integrating later: allows for drawing, circling, making shapes on map @@ -62,19 +96,22 @@ const bingApiKey = process.env.BING_MAPS; // if you're running local, get a Bing // }); @observer -export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { +export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapBox, fieldKey); } private _dragRef = React.createRef<HTMLDivElement>(); - private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); - private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _sidebarRef = React.createRef<SidebarAnnos>(); private _ref: React.RefObject<HTMLDivElement> = React.createRef(); + private _mapRef: React.RefObject<MapRef> = React.createRef(); private _disposers: { [key: string]: IReactionDisposer } = {}; private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); - @observable private _marqueeing: number[] | undefined; + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + } + @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); @@ -86,6 +123,116 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @computed get allPushpins() { return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); } + @computed get allRoutes() { + return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE); + } + @computed get updatedRouteCoordinates(): Feature<Geometry, GeoJsonProperties> { + if (this.routeToAnimate?.routeCoordinates) { + const originalCoordinates: Position[] = JSON.parse(StrCast(this.routeToAnimate.routeCoordinates)); + // const index = Math.floor(this.animationPhase * originalCoordinates.length); + const index = this.animationPhase * (originalCoordinates.length - 1); // Calculate the fractional index + console.log('Animation phase', this.animationPhase); + const startIndex = Math.floor(index); + const endIndex = Math.ceil(index); + let feature: Feature<Geometry, GeoJsonProperties>; + + let geometry: LineString; + if (startIndex === endIndex) { + // AnimationPhase is at a whole number (no interpolation needed) + const coordinates = [originalCoordinates[startIndex]]; + geometry = { + type: 'LineString', + coordinates, + }; + feature = { + type: 'Feature', + properties: { + routeTitle: StrCast(this.routeToAnimate.title), + }, + geometry: geometry, + }; + } else { + // Interpolate between two coordinates + const startCoord = originalCoordinates[startIndex]; + const endCoord = originalCoordinates[endIndex]; + const fraction = index - startIndex; + + const interpolator = d3.interpolateArray(startCoord, endCoord); + + const interpolatedCoord = interpolator(fraction); + + const coordinates = originalCoordinates.slice(0, startIndex + 1).concat([interpolatedCoord]); + + geometry = { + type: 'LineString', + coordinates, + }; + feature = { + type: 'Feature', + properties: { + routeTitle: StrCast(this.routeToAnimate.title), + }, + geometry: geometry, + }; + } + + autorun(() => { + const animationUtil = this.animationUtility; + const concattedCoordinates = geometry.coordinates.concat(originalCoordinates.slice(endIndex)); + const newFeature: Feature<LineString, turf.Properties> = { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: concattedCoordinates, + }, + }; + if (animationUtil) { + animationUtil.setPath(newFeature); + } + }); + return feature; + } + console.log('ERROR'); + return { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: [], + }, + }; + } + @computed get selectedRouteCoordinates(): Position[] { + let coordinates: Position[] = []; + if (this.routeToAnimate?.routeCoordinates) { + coordinates = JSON.parse(StrCast(this.routeToAnimate.routeCoordinates)); + } + return coordinates; + } + + @computed get allRoutesGeoJson(): FeatureCollection { + const features: Feature<Geometry, GeoJsonProperties>[] = this.allRoutes.map((routeDoc: Doc) => { + console.log('Route coords: ', routeDoc.routeCoordinates); + const geometry: LineString = { + type: 'LineString', + coordinates: JSON.parse(StrCast(routeDoc.routeCoordinates)), + }; + return { + type: 'Feature', + properties: { + routeTitle: routeDoc.title, + }, + geometry: geometry, + }; + }); + + return { + type: 'FeatureCollection', + features: features, + }; + } + @computed get SidebarShown() { return this.layoutDoc._layout_showSidebar ? true : false; } @@ -93,7 +240,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } @computed get sidebarColor() { - return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4')); + return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4')); } @computed get SidebarKey() { return this.fieldKey + '_sidebar'; @@ -101,13 +248,13 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps componentDidMount() { this._unmounting = false; - this.props.setContentView?.(this); + this._props.setContentViewBox?.(this); } _unmounting = false; componentWillUnmount(): void { this._unmounting = true; - this.deselectPin(); + this.deselectPinOrRoute(); this._rerenderTimeout && clearTimeout(this._rerenderTimeout); Object.keys(this._disposers).forEach(key => this._disposers[key]?.()); } @@ -122,7 +269,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); const docs = doc instanceof Doc ? [doc] : doc; docs.forEach(doc => { - let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this.selectedPin; + let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this.selectedPinOrRoute; if (doc.latitude !== undefined && doc.longitude !== undefined && !existingPin) { existingPin = this.createPushpin(NumCast(doc.latitude), NumCast(doc.longitude), StrCast(doc.map)); } @@ -166,11 +313,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps e, (e, down, delta) => runInAction(() => { - const localDelta = this.props + const localDelta = this._props .ScreenToLocalTransform() - .scale(this.props.NativeDimScaling?.() || 1) + .scale(this._props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); - const fullWidth = this.layoutDoc[Width](); + const fullWidth = NumCast(this.layoutDoc._width); const mapWidth = fullWidth - this.sidebarWidth(); if (this.sidebarWidth() + localDelta[0] > 0) { this.layoutDoc._layout_showSidebar = true; @@ -187,7 +334,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map') ); }; - sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); + sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); /** * Handles toggle of sidebar on click the little comment button @@ -199,8 +346,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps key="sidebar" title="Toggle Sidebar" style={{ - display: !this.props.isContentActive() ? 'none' : undefined, - top: StrCast(this.rootDoc._layout_showTitle) === 'title' ? 20 : 5, + display: !this._props.isContentActive() ? 'none' : undefined, + top: StrCast(this.Document._layout_showTitle) === 'title' ? 20 : 5, backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={this.sidebarBtnDown}> @@ -223,25 +370,25 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const sourceAnchorCreator = action(() => { const note = this.getAnchor(true); - if (note && this.selectedPin) { - note.latitude = this.selectedPin.latitude; - note.longitude = this.selectedPin.longitude; - note.map = this.selectedPin.map; + if (note && this.selectedPinOrRoute) { + note.latitude = this.selectedPinOrRoute.latitude; + note.longitude = this.selectedPinOrRoute.longitude; + note.map = this.selectedPinOrRoute.map; } return note as Doc; }); const targetCreator = (annotationOn: Doc | undefined) => { - const target = DocUtils.GetNewTextDoc('Note linked to ' + this.rootDoc.title, 0, 0, 100, 100, undefined, annotationOn, undefined, 'yellow'); - FormattedTextBox.SelectOnLoad = target[Id]; + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn, 'yellow'); + FormattedTextBox.SetSelectOnLoad(target); return target; }; - const docView = this.props.DocumentView?.(); + const docView = this.DocumentView?.(); docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, { dragComplete: e => { if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) { - e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.Document; + e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.Document; e.annoDragData.linkSourceDoc.followLinkZoom = false; } }, @@ -252,10 +399,10 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const createFunc = undoable( action(() => { const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', LinkedTo]); - if (note && this.selectedPin) { - note.latitude = this.selectedPin.latitude; - note.longitude = this.selectedPin.longitude; - note.map = this.selectedPin.map; + if (note && this.selectedPinOrRoute) { + note.latitude = this.selectedPinOrRoute.latitude; + note.longitude = this.selectedPinOrRoute.longitude; + note.map = this.selectedPinOrRoute.map; } }), 'create note annotation' @@ -278,39 +425,17 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func); - @action - onMarqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { - setupMoveUpEvents( - this, - e, - action(e => { - MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeing = [e.clientX, e.clientY]; - return true; - }), - returnFalse, - () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), - false - ); - } - }; - @action finishMarquee = (x?: number, y?: number) => { - this._marqueeing = undefined; - x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false, this.rootDoc); - }; - addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => this.addDocument(doc, annotationKey); - pointerEvents = () => (this.props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none'); + pointerEvents = () => (this._props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none'); - panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth(); - panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); - scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); - transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.childFilters(), Utils.IsOpaqueFilter()]; - infoWidth = () => this.props.PanelWidth() / 5; - infoHeight = () => this.props.PanelHeight() / 5; + panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1) - this.sidebarWidth(); + panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); + scrollXf = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); + transparentFilter = () => [...this._props.childFilters(), Utils.TransparentBackgroundFilter]; + opaqueFilter = () => [...this._props.childFilters(), Utils.OpaqueBackgroundFilter]; + infoWidth = () => this._props.PanelWidth() / 5; + infoHeight = () => this._props.PanelHeight() / 5; anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; savedAnnotations = () => this._savedAnnotations; @@ -348,60 +473,42 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; @observable - bingSearchBarContents: any = this.rootDoc.map; // For Bing Maps: The contents of the Bing search bar (string) + bingSearchBarContents: any = this.Document.map; // For Bing Maps: The contents of the Bing search bar (string) geoDataRequestOptions = { entityType: 'PopulatedPlace', }; - // incrementer: number = 0; - /* - * Creates Pushpin doc and adds it to the list of annotations - */ - @action - createPushpin = undoable((latitude: number, longitude: number, map?: string) => { - // Stores the pushpin as a MapMarkerDocument - const pushpin = Docs.Create.PushpinDocument( - NumCast(latitude), - NumCast(longitude), - false, - [], - { title: map ?? `lat=${latitude},lng=${longitude}`, map: map } - // ,'pushpinIDamongus'+ this.incrementer++ - ); - this.addDocument(pushpin, this.annotationKey); - return pushpin; - // mapMarker.infoWindowOpen = true; - }, 'createpin'); - // The pin that is selected - @observable selectedPin: Doc | undefined; + @observable selectedPinOrRoute: Doc | undefined = undefined; @action - deselectPin = () => { - if (this.selectedPin) { - // Removes filter - Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'remove'); - Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'remove'); - Doc.setDocFilter(this.rootDoc, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); - - const temp = this.selectedPin; - if (!this._unmounting) { - this._bingMap.current.entities.remove(this.map_docToPinMap.get(temp)); - } - const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(temp.latitude, temp.longitude)); - this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(temp as Doc)); - if (!this._unmounting) { - this._bingMap.current.entities.push(newpin); - } - this.map_docToPinMap.set(temp, newpin); - this.selectedPin = undefined; - this.bingSearchBarContents = this.rootDoc.map; + deselectPinOrRoute = () => { + if (this.selectedPinOrRoute) { + // // Removes filter + // Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'remove'); + // Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'remove'); + // Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); + // const temp = this.selectedPin; + // if (!this._unmounting) { + // this._bingMap.current.entities.remove(this.map_docToPinMap.get(temp)); + // } + // const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(temp.latitude, temp.longitude)); + // this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(temp as Doc)); + // if (!this._unmounting) { + // this._bingMap.current.entities.push(newpin); + // } + // this.map_docToPinMap.set(temp, newpin); + // this.selectedPin = undefined; + // this.bingSearchBarContents = this.Document.map; } }; - getView = async (doc: Doc) => { - if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(); + getView = async (doc: Doc, options: FocusViewOptions) => { + if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) { + this.toggleSidebar(); + options.didMove = true; + } return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); }; /* @@ -409,25 +516,25 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps */ @action pushpinClicked = (pinDoc: Doc) => { - this.deselectPin(); - this.selectedPin = pinDoc; + this.deselectPinOrRoute(); + this.selectedPinOrRoute = pinDoc; this.bingSearchBarContents = pinDoc.map; - // Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'match'); - // Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'match'); - Doc.setDocFilter(this.rootDoc, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPin)}`, 'check'); + // Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'match'); + // Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'match'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check'); - this.recolorPin(this.selectedPin, 'green'); + this.recolorPin(this.selectedPinOrRoute, 'green'); - MapAnchorMenu.Instance.Delete = this.deleteSelectedPin; + MapAnchorMenu.Instance.Delete = this.deleteSelectedPinOrRoute; MapAnchorMenu.Instance.Center = this.centerOnSelectedPin; MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation; MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag; - const point = this._bingMap.current.tryLocationToPixel(new this.MicrosoftMaps.Location(this.selectedPin.latitude, this.selectedPin.longitude)); - const x = point.x + (this.props.PanelWidth() - this.sidebarWidth()) / 2; - const y = point.y + this.props.PanelHeight() / 2 + 32; - const cpt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y); + const point = this._bingMap.current.tryLocationToPixel(new this.MicrosoftMaps.Location(this.selectedPinOrRoute.latitude, this.selectedPinOrRoute.longitude)); + const x = point.x + (this._props.PanelWidth() - this.sidebarWidth()) / 2; + const y = point.y + this._props.PanelHeight() / 2 + 32; + const cpt = this.ScreenToLocalBoxXf().inverse().transformPoint(x, y); MapAnchorMenu.Instance.jumpTo(cpt[0], cpt[1], true); document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true); @@ -438,8 +545,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps */ @action mapOnClick = (e: { location: { latitude: any; longitude: any } }) => { - this.props.select(false); - this.deselectPin(); + this._props.select(false); + this.deselectPinOrRoute(); }; /* * Updates values of layout doc to match the current map @@ -484,24 +591,24 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps, existingPin?: Doc) => { /// this should use SELECTED pushpin for lat/long if there is a selection, otherwise CENTER const anchor = Docs.Create.ConfigDocument({ - title: 'MapAnchor:' + this.rootDoc.title, - text: StrCast(this.selectedPin?.map) || StrCast(this.rootDoc.map) || 'map location', - config_latitude: NumCast((existingPin ?? this.selectedPin)?.latitude ?? this.dataDoc.latitude), - config_longitude: NumCast((existingPin ?? this.selectedPin)?.longitude ?? this.dataDoc.longitude), + title: 'MapAnchor:' + this.Document.title, + text: StrCast(this.selectedPinOrRoute?.map) || StrCast(this.Document.map) || 'map location', + config_latitude: NumCast((existingPin ?? this.selectedPinOrRoute)?.latitude ?? this.dataDoc.latitude), + config_longitude: NumCast((existingPin ?? this.selectedPinOrRoute)?.longitude ?? this.dataDoc.longitude), config_map_zoom: NumCast(this.dataDoc.map_zoom), - config_map_type: StrCast(this.dataDoc.map_type), - config_map: StrCast((existingPin ?? this.selectedPin)?.map) || StrCast(this.dataDoc.map), + // config_map_type: StrCast(this.dataDoc.map_type), + config_map: StrCast((existingPin ?? this.selectedPinOrRoute)?.map) || StrCast(this.dataDoc.map), layout_unrendered: true, - mapPin: existingPin ?? this.selectedPin, - annotationOn: this.rootDoc, + mapPin: existingPin ?? this.selectedPinOrRoute, + annotationOn: this.Document, }); if (anchor) { if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; addAsAnnotation && this.addDocument(anchor); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.rootDoc); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.Document); return anchor; } - return this.rootDoc; + return this.Document; }; map_docToPinMap = new Map<Doc, any>(); @@ -531,7 +638,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps * Removes pin from annotations */ @action - removePushpin = (pinDoc: Doc) => this.removeMapDocument(pinDoc, this.annotationKey); + removePushpinOrRoute = (pinOrRouteDoc: Doc) => this.removeMapDocument(pinOrRouteDoc, this.annotationKey); /* * Removes pushpin from map render @@ -541,18 +648,19 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._bingMap.current.entities.remove(this.map_docToPinMap.get(pinDoc)); } this.map_docToPinMap.delete(pinDoc); - this.selectedPin = undefined; + this.selectedPinOrRoute = undefined; }; @action - deleteSelectedPin = undoable(() => { - if (this.selectedPin) { + deleteSelectedPinOrRoute = undoable(() => { + console.log('deleting'); + if (this.selectedPinOrRoute) { // Removes filter - Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'remove'); - Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'remove'); - Doc.setDocFilter(this.rootDoc, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); + Doc.setDocFilter(this.Document, 'latitude', this.selectedPinOrRoute.latitude, 'remove'); + Doc.setDocFilter(this.Document, 'longitude', this.selectedPinOrRoute.longitude, 'remove'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPinOrRoute))}`, 'remove'); - this.removePushpin(this.selectedPin); + this.removePushpinOrRoute(this.selectedPinOrRoute); } MapAnchorMenu.Instance.fadeOut(true); document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); @@ -561,23 +669,36 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps tryHideMapAnchorMenu = (e: PointerEvent) => { let target = document.elementFromPoint(e.x, e.y); while (target) { + if (target.id === 'route-destination-searcher-listbox') return; if (target === MapAnchorMenu.top.current) return; target = target.parentElement; } e.stopPropagation(); e.preventDefault(); MapAnchorMenu.Instance.fadeOut(true); + runInAction(() => { + this.temporaryRouteSource = { + type: 'FeatureCollection', + features: [], + }; + }); + document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); }; @action centerOnSelectedPin = () => { - if (this.selectedPin) { - this.dataDoc.latitude = this.selectedPin.latitude; - this.dataDoc.longitude = this.selectedPin.longitude; - this.dataDoc.map = this.selectedPin.map ?? ''; - this.bingSearchBarContents = this.selectedPin.map; + if (this.selectedPinOrRoute) { + this._mapRef.current?.flyTo({ + center: [NumCast(this.selectedPinOrRoute.longitude), NumCast(this.selectedPinOrRoute.latitude)], + }); } + // if (this.selectedPin) { + // this.dataDoc.latitude = this.selectedPin.latitude; + // this.dataDoc.longitude = this.selectedPin.longitude; + // this.dataDoc.map = this.selectedPin.map ?? ''; + // this.bingSearchBarContents = this.selectedPin.map; + // } MapAnchorMenu.Instance.fadeOut(true); document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu); }; @@ -609,16 +730,13 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }, }; - @action - searchbarOnEdit = (newText: string) => (this.bingSearchBarContents = newText); - recolorPin = (pin: Doc, color?: string) => { - this._bingMap.current.entities.remove(this.map_docToPinMap.get(pin)); - this.map_docToPinMap.delete(pin); - const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), color ? { color } : {}); - this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(pin)); - this._bingMap.current.entities.push(newpin); - this.map_docToPinMap.set(pin, newpin); + // this._bingMap.current.entities.remove(this.map_docToPinMap.get(pin)); + // this.map_docToPinMap.delete(pin); + // const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), color ? { color } : {}); + // this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(pin)); + // this._bingMap.current.entities.push(newpin); + // this.map_docToPinMap.set(pin, newpin); }; /* @@ -638,7 +756,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'maptypechanged', undoable(this.updateMapType, 'Map ViewType Change')); this._disposers.mapLocation = reaction( - () => this.rootDoc.map, + () => this.Document.map, mapLoc => (this.bingSearchBarContents = mapLoc), { fireImmediately: true } ); @@ -663,7 +781,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); this._disposers.location = reaction( - () => ({ lat: this.rootDoc.latitude, lng: this.rootDoc.longitude, zoom: this.rootDoc.map_zoom, mapType: this.rootDoc.map_type }), + () => ({ lat: this.Document.latitude, lng: this.Document.longitude, zoom: this.Document.map_zoom, mapType: this.Document.map_type }), locationObject => { // if (this._bingMap.current) try { @@ -688,24 +806,26 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps e, e, e => { + // move event if (!dragClone) { - dragClone = this._dragRef.current?.cloneNode(true) as HTMLDivElement; + dragClone = this._dragRef.current?.cloneNode(true) as HTMLDivElement; // copy draggable pin dragClone.style.position = 'absolute'; dragClone.style.zIndex = '10000'; - DragManager.Root().appendChild(dragClone); + DragManager.Root().appendChild(dragClone); // add clone to root } dragClone.style.transform = `translate(${e.clientX - 15}px, ${e.clientY - 15}px)`; return false; }, e => { + // up event if (!dragClone) return; DragManager.Root().removeChild(dragClone); - let target = document.elementFromPoint(e.x, e.y); + let target = document.elementFromPoint(e.x, e.y); // element for specified x and y coordinates while (target) { if (target === this._ref.current) { - const cpt = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); - const x = cpt[0] - (this.props.PanelWidth() - this.sidebarWidth()) / 2; - const y = cpt[1] - 32 /* height of search bar */ - this.props.PanelHeight() / 2; + const cpt = this.ScreenToLocalBoxXf().transformPoint(e.clientX, e.clientY); + const x = cpt[0] - (this._props.PanelWidth() - this.sidebarWidth()) / 2; + const y = cpt[1] - 20 /* height of search bar */ - this._props.PanelHeight() / 2; const location = this._bingMap.current.tryPixelToLocation(new this.MicrosoftMaps.Point(x, y)); this.createPushpin(location.latitude, location.longitude); break; @@ -714,7 +834,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } }, e => { - const createPin = () => this.createPushpin(this.rootDoc.latitude, this.rootDoc.longitude, this.rootDoc.map); + const createPin = () => this.createPushpin(this.Document.latitude, this.Document.longitude, this.Document.map); if (this.bingSearchBarContents) { this.bingSearch().then(createPin); } else createPin(); @@ -722,15 +842,703 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); }; + // incrementer: number = 0; + /* + * Creates Pushpin doc and adds it to the list of annotations + */ + @action + createPushpin = undoable((latitude: number, longitude: number, location?: string, wikiData?: string) => { + // Stores the pushpin as a MapMarkerDocument + const pushpin = Docs.Create.PushpinDocument( + NumCast(latitude), + NumCast(longitude), + false, + [], + { + title: location ?? `lat=${NumCast(latitude)},lng=${NumCast(longitude)}`, + map: location, + description: '', + wikiData: wikiData, + markerType: 'MAP_PIN', + markerColor: '#ff5722', + } + // { title: map ?? `lat=${latitude},lng=${longitude}`, map: map }, + // ,'pushpinIDamongus'+ this.incrementer++ + ); + this.addDocument(pushpin, this.annotationKey); + console.log(pushpin); + return pushpin; + + // mapMarker.infoWindowOpen = true; + }, 'createpin'); + + @action + createMapRoute = undoable((coordinates: Position[], originName: string, destination: any, createPinForDestination: boolean) => { + if (originName !== destination.place_name) { + const mapRoute = Docs.Create.MapRouteDocument(false, [], { title: `${originName} --> ${destination.place_name}`, routeCoordinates: JSON.stringify(coordinates) }); + this.addDocument(mapRoute, this.annotationKey); + if (createPinForDestination) { + this.createPushpin(destination.center[1], destination.center[0], destination.place_name); + } + this.temporaryRouteSource = { + type: 'FeatureCollection', + features: [], + }; + MapAnchorMenu.Instance.fadeOut(true); + return mapRoute; + } + // TODO: Display error that can't create route to same location + }, 'createmaproute'); + searchbarKeyDown = (e: any) => e.key === 'Enter' && this.bingSearch(); + @observable + featuresFromGeocodeResults: any[] = []; + + @action + addMarkerForFeature = (feature: any) => { + const location = feature.place_name; + if (feature.center) { + const longitude = feature.center[0]; + const latitude = feature.center[1]; + const wikiData = feature.properties?.wikiData; + + this.createPushpin(latitude, longitude, location, wikiData); + + if (this._mapRef.current) { + this._mapRef.current.flyTo({ + center: feature.center, + }); + } + this.featuresFromGeocodeResults = []; + } else { + // TODO: handle error + } + }; + + /** + * Makes a forward geocoding API call to Mapbox to retrieve locations based on the search input + * @param searchText the search input (presumably a location) + */ + handleSearchChange = async (searchText: string) => { + const features = await MapboxApiUtility.forwardGeocodeForFeatures(searchText); + if (features && !this.isAnimating) { + runInAction(() => { + this.settingsOpen = false; + this.featuresFromGeocodeResults = features; + this.routeToAnimate = undefined; + }); + } + // try { + // const url = MAPBOX_FORWARD_GEOCODE_BASE_URL + encodeURI(searchText) +'.json' +`?access_token=${MAPBOX_ACCESS_TOKEN}`; + // const response = await fetch(url); + // const data = await response.json(); + // runInAction(() => { + // this.featuresFromGeocodeResults = data.features; + // }) + // } catch (error: any){ + // // TODO: handle error in better way + // console.log(error); + // } + }; + // @action + // debouncedCall = React.useCallback(debounce(this.debouncedOnSearchBarChange, 300), []); + + @action + handleMapClick = (e: MapLayerMouseEvent) => { + this.featuresFromGeocodeResults = []; + this.settingsOpen = false; + if (this._mapRef.current) { + const features = this._mapRef.current.queryRenderedFeatures(e.point, { + layers: ['map-routes-layer'], + }); + + console.error(features); + if (features && features.length > 0 && features[0].properties && features[0].geometry) { + const geometry = features[0].geometry as LineString; + const routeTitle: string = features[0].properties['routeTitle']; + const routeDoc: Doc | undefined = this.allRoutes.find(routeDoc => routeDoc.title === routeTitle); + this.deselectPinOrRoute(); // TODO: Also deselect route if selected + if (routeDoc) { + this.selectedPinOrRoute = routeDoc; + Doc.setDocFilter(this.Document, LinkedTo, `mapRoute=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check'); + + // TODO: Recolor route + + MapAnchorMenu.Instance.Delete = this.deleteSelectedPinOrRoute; + MapAnchorMenu.Instance.Center = this.centerOnSelectedPin; + MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation; + MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag; + + MapAnchorMenu.Instance.Reset(); + + MapAnchorMenu.Instance.setRouteDoc(routeDoc); + + // TODO: Subject to change + MapAnchorMenu.Instance.setAllMapboxPins(this.allAnnotations.filter(anno => !anno.layout_unrendered)); + + MapAnchorMenu.Instance.DisplayRoute = this.displayRoute; + MapAnchorMenu.Instance.AddNewRouteToMap = this.createMapRoute; + MapAnchorMenu.Instance.CreatePin = this.addMarkerForFeature; + MapAnchorMenu.Instance.OpenAnimationPanel = this.openAnimationPanel; + + // this.selectedRouteCoordinates = geometry.coordinates; + + MapAnchorMenu.Instance.setMenuType('route'); + + MapAnchorMenu.Instance.jumpTo(e.originalEvent.clientX, e.originalEvent.clientY, true); + + document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true); + } + } + } + }; + + /** + * Makes a reverse geocoding API call to retrieve features corresponding to a map click (based on longitude + * and latitude). Sets the search results accordingly. + * @param e + */ + handleMapDblClick = async (e: MapLayerMouseEvent) => { + e.preventDefault(); + const lngLat: LngLat = e.lngLat; + const longitude: number = lngLat.lng; + const latitude: number = lngLat.lat; + + const features = await MapboxApiUtility.reverseGeocodeForFeatures(longitude, latitude); + if (features) { + runInAction(() => { + this.featuresFromGeocodeResults = features; + }); + } + + // // REVERSE GEOCODE TO GET LOCATION DETAILS + // try { + // const url = MAPBOX_REVERSE_GEOCODE_BASE_URL + encodeURI(longitude.toString() + "," + latitude.toString()) + '.json' + + // `?access_token=${MAPBOX_ACCESS_TOKEN}`; + // const response = await fetch(url); + // const data = await response.json(); + // console.log("REV GEOCODE DATA: ", data); + // runInAction(() => { + // this.featuresFromGeocodeResults = data.features; + // }) + // } catch (error: any){ + // // TODO: handle error in better way + // console.log(error); + // } + }; + + @observable + currentPopup: PopupInfo | undefined = undefined; + + @action + handleMarkerClick = (e: MarkerEvent<mapboxgl.Marker, MouseEvent>, pinDoc: Doc) => { + this.featuresFromGeocodeResults = []; + this.deselectPinOrRoute(); // TODO: check this method + this.selectedPinOrRoute = pinDoc; + // this.bingSearchBarContents = pinDoc.map; + + // Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'match'); + // Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'match'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check'); + + this.recolorPin(this.selectedPinOrRoute, 'green'); // TODO: check this method + + MapAnchorMenu.Instance.Delete = this.deleteSelectedPinOrRoute; + MapAnchorMenu.Instance.Center = this.centerOnSelectedPin; + MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation; + MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag; + + MapAnchorMenu.Instance.Reset(); + + // pass in the pinDoc + MapAnchorMenu.Instance.setPinDoc(pinDoc); + MapAnchorMenu.Instance.setAllMapboxPins(this.allAnnotations.filter(anno => !anno.layout_unrendered)); + + MapAnchorMenu.Instance.DisplayRoute = this.displayRoute; + MapAnchorMenu.Instance.AddNewRouteToMap = this.createMapRoute; + MapAnchorMenu.Instance.CreatePin = this.addMarkerForFeature; + + MapAnchorMenu.Instance.setMenuType('standard'); + + // MapAnchorMenu.Instance.jumpTo(NumCast(pinDoc.longitude), NumCast(pinDoc.latitude)-3, true); + + MapAnchorMenu.Instance.jumpTo(e.originalEvent.clientX, e.originalEvent.clientY, true); + + document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true); + + // this._mapRef.current.flyTo({ + // center: [NumCast(pinDoc.longitude), NumCast(pinDoc.latitude)-3] + // }) + }; + + @observable + temporaryRouteSource: FeatureCollection = { + type: 'FeatureCollection', + features: [], + }; + + @action + displayRoute = (routeInfoMap: Record<TransportationType, any> | undefined, type: TransportationType) => { + if (routeInfoMap) { + const newTempRouteSource: FeatureCollection = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: routeInfoMap[type].coordinates, + }, + }, + ], + }; + // TODO: Create pin for destination + // TODO: Fly to point where full route will be shown + this.temporaryRouteSource = newTempRouteSource; + } + }; + + @observable + isAnimating: boolean = false; + + @observable + routeToAnimate: Doc | undefined = undefined; + + @observable + animationPhase: number = 0; + + @observable + finishedFlyTo: boolean = false; + + @action + setAnimationPhase = (newValue: number) => { + this.animationPhase = newValue; + }; + + @observable + frameId: number | null = null; + + @action + setFrameId = (frameId: number) => { + this.frameId = frameId; + }; + + @observable + animationUtility: AnimationUtility | null = null; + + @action + setAnimationUtility = (util: AnimationUtility) => { + this.animationUtility = util; + }; + + @action + openAnimationPanel = (routeDoc: Doc | undefined) => { + if (routeDoc) { + MapAnchorMenu.Instance.fadeOut(true); + document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); + this.featuresFromGeocodeResults = []; + this.routeToAnimate = routeDoc; + } + }; + + @computed get mapboxMapViewState(): ViewState { + return { + zoom: NumCast(this.dataDoc.map_zoom, 8), + longitude: NumCast(this.dataDoc.longitude, -71.4128), + latitude: NumCast(this.dataDoc.latitude, 41.824), + pitch: NumCast(this.dataDoc.map_pitch), + bearing: NumCast(this.dataDoc.map_bearing), + padding: { + top: 0, + bottom: 0, + left: 0, + right: 0, + }, + }; + } + + @computed + get preAnimationViewState() { + if (!this.isAnimating) { + return this.mapboxMapViewState; + } + } + + @observable + isStreetViewAnimation: boolean = false; + + @observable + animationSpeed: AnimationSpeed = AnimationSpeed.MEDIUM; + + @observable + animationLineColor: string = '#ffff00'; + + @action + setAnimationLineColor = (color: ColorResult) => { + this.animationLineColor = color.hex; + }; + + @action + updateAnimationSpeed = () => { + let newAnimationSpeed: AnimationSpeed; + switch (this.animationSpeed) { + case AnimationSpeed.SLOW: + newAnimationSpeed = AnimationSpeed.MEDIUM; + break; + case AnimationSpeed.MEDIUM: + newAnimationSpeed = AnimationSpeed.FAST; + break; + case AnimationSpeed.FAST: + newAnimationSpeed = AnimationSpeed.SLOW; + break; + default: + newAnimationSpeed = AnimationSpeed.MEDIUM; + break; + } + this.animationSpeed = newAnimationSpeed; + if (this.animationUtility) { + this.animationUtility.updateAnimationSpeed(newAnimationSpeed); + } + }; + @computed get animationSpeedTooltipText(): string { + switch (this.animationSpeed) { + case AnimationSpeed.SLOW: + return '1x speed'; + case AnimationSpeed.MEDIUM: + return '2x speed'; + case AnimationSpeed.FAST: + return '3x speed'; + default: + return '2x speed'; + } + } + @computed get animationSpeedIcon(): JSX.Element { + switch (this.animationSpeed) { + case AnimationSpeed.SLOW: + return slowSpeedIcon; + case AnimationSpeed.MEDIUM: + return mediumSpeedIcon; + case AnimationSpeed.FAST: + return fastSpeedIcon; + default: + return mediumSpeedIcon; + } + } + + @action + toggleIsStreetViewAnimation = () => { + const newVal = !this.isStreetViewAnimation; + this.isStreetViewAnimation = newVal; + if (this.animationUtility) { + this.animationUtility.updateIsStreetViewAnimation(newVal); + } + }; + + @observable + dynamicRouteFeature: Feature<Geometry, GeoJsonProperties> = { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: [], + }, + }; + + @observable + path: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties> = { + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: [], + }, + properties: {}, + }; + + getFeatureFromRouteDoc = (routeDoc: Doc): Feature<Geometry, GeoJsonProperties> => { + const geometry: LineString = { + type: 'LineString', + coordinates: JSON.parse(StrCast(routeDoc.routeCoordinates)), + }; + return { + type: 'Feature', + properties: { + routeTitle: routeDoc.title, + }, + geometry: geometry, + }; + }; + + @action + playAnimation = (status: AnimationStatus) => { + if (!this._mapRef.current || !this.routeToAnimate) { + return; + } + + this.animationPhase = status === AnimationStatus.RESUME ? this.animationPhase : 0; + this.frameId = AnimationStatus.RESUME ? this.frameId : null; + this.finishedFlyTo = AnimationStatus.RESUME ? this.finishedFlyTo : false; + + const path = turf.lineString(this.selectedRouteCoordinates); + + this.settingsOpen = false; + this.path = path; + this.isAnimating = true; + + runInAction(() => { + return new Promise<void>(async resolve => { + const targetLngLat = { + lng: this.selectedRouteCoordinates[0][0], + lat: this.selectedRouteCoordinates[0][1], + }; + + const animationUtil = new AnimationUtility(targetLngLat, this.selectedRouteCoordinates, this.isStreetViewAnimation, this.animationSpeed, this.showTerrain, this._mapRef.current); + runInAction(() => { + this.setAnimationUtility(animationUtil); + }); + + const updateFrameId = (newFrameId: number) => { + this.setFrameId(newFrameId); + }; + + const updateAnimationPhase = (newAnimationPhase: number) => { + this.setAnimationPhase(newAnimationPhase); + }; + + if (status !== AnimationStatus.RESUME) { + const result = await animationUtil.flyInAndRotate({ + map: this._mapRef.current!, + // targetLngLat, + // duration 3000 + // startAltitude: 3000000, + // endAltitude: this.isStreetViewAnimation ? 80 : 12000, + // startBearing: 0, + // endBearing: -20, + // startPitch: 40, + // endPitch: this.isStreetViewAnimation ? 80 : 50, + updateFrameId, + }); + + console.log('Bearing: ', result.bearing); + console.log('Altitude: ', result.altitude); + } + + runInAction(() => { + this.finishedFlyTo = true; + }); + + // follow the path while slowly rotating the camera, passing in the camera bearing and altitude from the previous animation + await animationUtil.animatePath({ + map: this._mapRef.current!, + // path: this.path, + // startBearing: -20, + // startAltitude: this.isStreetViewAnimation ? 80 : 12000, + // pitch: this.isStreetViewAnimation ? 80: 50, + currentAnimationPhase: this.animationPhase, + updateAnimationPhase, + updateFrameId, + }); + + // get the bounds of the linestring, use fitBounds() to animate to a final view + const bbox3d = turf.bbox(this.path); + + const bbox2d: LngLatBoundsLike = [bbox3d[0], bbox3d[1], bbox3d[2], bbox3d[3]]; + + this._mapRef.current!.fitBounds(bbox2d, { + duration: 3000, + pitch: 30, + bearing: 0, + padding: 120, + }); + + setTimeout(() => { + this.isStreetViewAnimation = false; + resolve(); + }, 10000); + }); + }); + }; + + @action + pauseAnimation = () => { + if (this.frameId && this.animationPhase > 0) { + window.cancelAnimationFrame(this.frameId); + this.frameId = null; + this.isAnimating = false; + } + }; + + @action + stopAnimation = (close: boolean) => { + if (this.frameId) { + window.cancelAnimationFrame(this.frameId); + } + this.animationPhase = 0; + this.frameId = null; + this.finishedFlyTo = false; + this.isAnimating = false; + if (close) { + this.animationSpeed = AnimationSpeed.MEDIUM; + this.isStreetViewAnimation = false; + this.routeToAnimate = undefined; + this.animationUtility = null; + } + }; + + getRouteAnimationOptions = (): JSX.Element => { + return ( + <> + <IconButton + tooltip={this.isAnimating && this.finishedFlyTo ? 'Pause Animation' : 'Play Animation'} + onPointerDown={() => { + if (this.isAnimating && this.finishedFlyTo) { + this.pauseAnimation(); + } else if (this.animationPhase > 0) { + this.playAnimation(AnimationStatus.RESUME); // Resume from the current phase + } else { + this.playAnimation(AnimationStatus.START); // Play from the beginning + } + }} + icon={this.isAnimating && this.finishedFlyTo ? <FontAwesomeIcon icon={faPause as IconLookup} /> : <FontAwesomeIcon icon={faPlay as IconLookup} />} + color="black" + size={Size.MEDIUM} + /> + {this.isAnimating && this.finishedFlyTo && ( + <IconButton + tooltip="Restart animation" + onPointerDown={() => { + this.stopAnimation(false); + this.playAnimation(AnimationStatus.START); + }} + icon={<FontAwesomeIcon icon={faRotate as IconLookup} />} + color="black" + size={Size.MEDIUM} + /> + )} + <IconButton style={{ marginRight: '10px' }} tooltip="Stop and close animation" onPointerDown={() => this.stopAnimation(true)} icon={<FontAwesomeIcon icon={faCircleXmark as IconLookup} />} color="black" size={Size.MEDIUM} /> + <> + <div className="animation-suboptions"> + <div>|</div> + <FormControlLabel className="first-person-label" label="1st person animation:" labelPlacement="start" control={<Checkbox color="success" checked={this.isStreetViewAnimation} onChange={this.toggleIsStreetViewAnimation} />} /> + <div id="divider">|</div> + <IconButton tooltip={this.animationSpeedTooltipText} onPointerDown={this.updateAnimationSpeed} icon={this.animationSpeedIcon} size={Size.MEDIUM} /> + <div id="divider">|</div> + <div style={{ display: 'flex', alignItems: 'center' }}> + <div>Select Line Color: </div> + <CirclePicker circleSize={12} circleSpacing={5} width="100%" colors={['#ffff00', '#03a9f4', '#ff0000', '#ff5722', '#000000', '#673ab7']} onChange={(color: any) => this.setAnimationLineColor(color)} /> + </div> + </div> + </> + </> + ); + }; + + @action + hideRoute = () => { + this.temporaryRouteSource = { + type: 'FeatureCollection', + features: [], + }; + }; + + @observable + settingsOpen: boolean = false; + + @observable + mapStyle: string = 'mapbox://styles/mapbox/standard'; + + @observable + showTerrain: boolean = true; + + @action + toggleSettings = () => { + if (!this.isAnimating && this.animationPhase == 0) { + this.featuresFromGeocodeResults = []; + this.settingsOpen = !this.settingsOpen; + } + }; + + @action + changeMapStyle = (e: React.ChangeEvent<HTMLSelectElement>) => { + this.dataDoc.map_style = e.target.value; + // this.mapStyle = `mapbox://styles/mapbox/${e.target.value}` + }; + + @action + onBearingChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const bearing = parseInt(e.target.value); + if (!isNaN(bearing) && this._mapRef.current) { + console.log('bearing change'); + const fixedBearing = Math.max(0, Math.min(360, bearing)); + this._mapRef.current.setBearing(fixedBearing); + this.dataDoc.map_bearing = fixedBearing; + } + }; + + @action + onPitchChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const pitch = parseInt(e.target.value); + if (!isNaN(pitch) && this._mapRef.current) { + console.log('pitch change'); + const fixedPitch = Math.max(0, Math.min(85, pitch)); + this._mapRef.current.setPitch(fixedPitch); + this.dataDoc.map_pitch = fixedPitch; + } + }; + + @action + onZoomChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const zoom = parseInt(e.target.value); + if (!isNaN(zoom) && this._mapRef.current) { + const fixedZoom = Math.max(0, Math.min(16, zoom)); + this._mapRef.current.setZoom(fixedZoom); + this.dataDoc.map_zoom = fixedZoom; + } + }; + + @action + onStepZoomChange = (increment: boolean) => { + if (this._mapRef.current) { + let newZoom: number; + if (increment) { + console.log('inc'); + newZoom = Math.min(16, this.mapboxMapViewState.zoom + 1); + } else { + console.log('dec'); + newZoom = Math.max(0, this.mapboxMapViewState.zoom - 1); + } + this._mapRef.current.setZoom(newZoom); + this.dataDoc.map_zoom = newZoom; + } + }; + + @action + onMapZoom = (e: ViewStateChangeEvent) => (this.dataDoc.map_zoom = e.viewState.zoom); + + @action + onMapMove = (e: ViewStateChangeEvent) => { + this.dataDoc.longitude = e.viewState.longitude; + this.dataDoc.latitude = e.viewState.latitude; + }; + + @action + toggleShowTerrain = () => (this.showTerrain = !this.showTerrain); + + getMarkerIcon = (pinDoc: Doc): JSX.Element | null => { + const markerType = StrCast(pinDoc.markerType); + const markerColor = StrCast(pinDoc.markerColor); + + return MarkerIcons.getFontAwesomeIcon(markerType, '2x', markerColor) ?? null; + }; + static _firstRender = true; static _rerenderDelay = 500; _rerenderTimeout: any; render() { // bcz: no idea what's going on here, but bings maps have some kind of bug // such that we need to delay rendering a second map on startup until the first map is rendered. - this.rootDoc[DocCss]; + this.Document[DocCss]; if (MapBox._rerenderDelay) { // prettier-ignore this._rerenderTimeout = this._rerenderTimeout ?? @@ -739,10 +1547,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps MapBox._rerenderDelay = 0; } this._rerenderTimeout = undefined; - this.rootDoc[DocCss] = this.rootDoc[DocCss] + 1; + this.Document[DocCss] = this.Document[DocCss] + 1; }), MapBox._rerenderDelay); return null; } + const scale = this._props.NativeDimScaling?.() || 1; + const parscale = scale === 1 ? 1 : this.ScreenToLocalBoxXf().Scale ?? 1; const renderAnnotations = (childFilters?: () => string[]) => null; return ( @@ -753,110 +1563,188 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps onPointerDown={async e => { e.button === 0 && !e.ctrlKey && e.stopPropagation(); }} - style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> + style={{ transformOrigin: 'top left', transform: `scale(${scale})`, width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> {renderAnnotations(this.opaqueFilter)} - {SnappingManager.GetIsDragging() ? null : renderAnnotations()} - - <div className="mapBox-searchbar"> - <EditableText - // editing - setVal={(newText: string | number) => typeof newText === 'string' && this.searchbarOnEdit(newText)} - onEnter={e => this.bingSearch()} - placeholder={this.bingSearchBarContents || 'enter city/zip/...'} - textAlign="center" - /> - <IconButton - icon={ - <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="magnifying-glass" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#DFDFDF"> - <path - fill="currentColor" - d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"></path> - </svg> - } - onClick={this.bingSearch} - type={Type.TERT} - /> - <div style={{ width: 30, height: 30 }} ref={this._dragRef} onPointerDown={this.dragToggle}> - <Button tooltip="drag to place a pushpin" icon={<FontAwesomeIcon size={'lg'} icon={'bullseye'} />} /> + {SnappingManager.IsDragging ? null : renderAnnotations()} + {!this.routeToAnimate && ( + <div className="mapBox-searchbar" style={{ width: `${100 / scale}%`, zIndex: 1, position: 'relative', background: 'lightGray' }}> + <TextField fullWidth placeholder="Enter a location" onChange={(e: any) => this.handleSearchChange(e.target.value)} /> + <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={e => this.toggleSettings()} /> + </div> + )} + {this.settingsOpen && !this.routeToAnimate && ( + <div className="mapbox-settings-panel" style={{ right: `${0 + this.sidebarWidth()}px` }}> + <div className="mapbox-style-select"> + <div>Map Style:</div> + <div> + <select onChange={this.changeMapStyle} value={StrCast(this.dataDoc.map_style)}> + <option value="mapbox://styles/mapbox/standard">Standard</option> + <option value="mapbox://styles/mapbox/streets-v11">Streets</option> + <option value="mapbox://styles/mapbox/outdoors-v12">Outdoors</option> + <option value="mapbox://styles/mapbox/light-v11">Light</option> + <option value="mapbox://styles/mapbox/dark-v11">Dark</option> + <option value="mapbox://styles/mapbox/satellite-v9">Satellite</option> + <option value="mapbox://styles/mapbox/satellite-streets-v12">Satellite Streets</option> + <option value="mapbox://styles/mapbox/navigation-day-v1">Navigation Day</option> + <option value="mapbox://styles/mapbox/navigation-night-v1">Navigation Night</option> + </select> + </div> + </div> + <div className="mapbox-bearing-selection"> + <div>Bearing: </div> + <input value={NumCast(this.mapboxMapViewState.bearing).toFixed(0)} type="number" onChange={this.onBearingChange} /> + </div> + <div className="mapbox-pitch-selection"> + <div>Pitch: </div> + <input value={NumCast(this.mapboxMapViewState.pitch).toFixed(0)} type="number" onChange={this.onPitchChange} /> + </div> + <div className="mapbox-pitch-selection"> + <div>Zoom: </div> + <input value={NumCast(this.mapboxMapViewState.zoom).toFixed(0)} type="number" onChange={this.onZoomChange} /> + </div> + <div className="mapbox-terrain-selection"> + <div>Show terrain: </div> + <input type="checkbox" checked={this.showTerrain} onChange={this.toggleShowTerrain} /> + </div> + </div> + )} + {this.routeToAnimate && ( + <div className="animation-panel" style={{ width: this.sidebarWidth() === 0 ? '100%' : `calc(100% - ${this.sidebarWidth()}px)` }}> + <div id="route-to-animate-title">{StrCast(this.routeToAnimate.title)}</div> + <div className="route-animation-options">{this.getRouteAnimationOptions()}</div> + </div> + )} + {this.featuresFromGeocodeResults.length > 0 && ( + <div className="mapbox-geocoding-search-results"> + <React.Fragment> + <h4>Choose a location for your pin: </h4> + {this.featuresFromGeocodeResults + .filter(feature => feature.place_name) + .map((feature, idx) => ( + <div + key={idx} + className="search-result-container" + onClick={() => { + this.handleSearchChange(''); + this.addMarkerForFeature(feature); + }}> + <div className="search-result-place-name">{feature.place_name}</div> + </div> + ))} + </React.Fragment> </div> - </div> - - <BingMapsReact - onMapReady={this.bingMapReady} // - bingMapsKey={bingApiKey} - height="100%" - mapOptions={this.bingMapOptions} - width="100%" - viewOptions={this.bingViewOptions} - /> - <div> - {!this._mapReady - ? null - : this.allAnnotations - .filter(anno => !anno.layout_unrendered) - .map((pushpin, i) => ( - <DocumentView - key={i} - {...this.props} - renderDepth={this.props.renderDepth + 1} - Document={pushpin} - DataDoc={undefined} - PanelWidth={returnOne} - PanelHeight={returnOne} - NativeWidth={returnOne} - NativeHeight={returnOne} - onKey={undefined} - onDoubleClick={undefined} - onBrowseClick={undefined} - childFilters={returnEmptyFilter} - childFiltersByRanges={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - isDocumentActive={returnFalse} - isContentActive={returnFalse} - addDocTab={returnFalse} - ScreenToLocalTransform={Transform.Identity} - fitContentsToBox={undefined} - focus={returnOne} - /> - ))} - </div> - {/* <MapBoxInfoWindow - key={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})[Id]} - {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} - place={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})} - markerMap={this.markerMap} - PanelWidth={this.infoWidth} - PanelHeight={this.infoHeight} - moveDocument={this.moveDocument} - isAnyChildContentActive={this.isAnyChildContentActive} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - /> */} - - {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( - <MarqueeAnnotator - rootDoc={this.rootDoc} - anchorMenuClick={this.anchorMenuClick} - scrollTop={0} - down={this._marqueeing} - scaling={returnOne} - addDocument={this.addDocumentWrapper} - docView={this.props.docViewPath().lastElement()} - finishMarquee={this.finishMarquee} - savedAnnotations={this.savedAnnotations} - annotationLayer={this._annotationLayer.current} - selectionText={returnEmptyString} - mainCont={this._mainCont.current} - /> )} + <MapProvider> + <MapboxMap + ref={this._mapRef} + mapboxAccessToken={MAPBOX_ACCESS_TOKEN} + viewState={this.isAnimating || this.routeToAnimate ? undefined : { ...this.mapboxMapViewState, width: NumCast(this.layoutDoc._width), height: NumCast(this.layoutDoc._height) }} + mapStyle={this.dataDoc.map_style ? StrCast(this.dataDoc.map_style) : 'mapbox://styles/mapbox/streets-v11'} + style={{ + position: 'absolute', + top: 0, + left: 0, + zIndex: '0', + width: NumCast(this.layoutDoc._width) * parscale, + height: NumCast(this.layoutDoc._height) * parscale, + }} + initialViewState={this.isAnimating ? undefined : this.mapboxMapViewState} + onZoom={this.onMapZoom} + onMove={this.onMapMove} + onClick={this.handleMapClick} + onDblClick={this.handleMapDblClick} + terrain={this.showTerrain ? { source: 'mapbox-dem', exaggeration: 2.0 } : undefined}> + <Source id="mapbox-dem" type="raster-dem" url="mapbox://mapbox.mapbox-terrain-dem-v1" tileSize={512} maxzoom={14} /> + <Source id="temporary-route" type="geojson" data={this.temporaryRouteSource} /> + <Source id="map-routes" type="geojson" data={this.allRoutesGeoJson} /> + <Layer id="temporary-route-layer" type="line" source="temporary-route" layout={{ 'line-join': 'round', 'line-cap': 'round' }} paint={{ 'line-color': '#36454F', 'line-width': 4, 'line-dasharray': [1, 1] }} /> + {!this.isAnimating && this.animationPhase == 0 && <Layer id="map-routes-layer" type="line" source="map-routes" layout={{ 'line-join': 'round', 'line-cap': 'round' }} paint={{ 'line-color': '#FF0000', 'line-width': 4 }} />} + {this.routeToAnimate && (this.isAnimating || this.animationPhase > 0) && ( + <> + {!this.isStreetViewAnimation && ( + <> + <Source id="animated-route" type="geojson" data={this.updatedRouteCoordinates} /> + <Layer + id="dynamic-animation-line" + type="line" + source="animated-route" + paint={{ + 'line-color': this.animationLineColor, + 'line-width': 5, + }} + /> + </> + )} + <Source id="start-pin-base" type="geojson" data={AnimationUtility.createGeoJSONCircle(this.selectedRouteCoordinates[0], 0.04)} /> + <Source id="start-pin-top" type="geojson" data={AnimationUtility.createGeoJSONCircle(this.selectedRouteCoordinates[0], 0.25)} /> + <Source id="end-pin-base" type="geojson" data={AnimationUtility.createGeoJSONCircle(this.selectedRouteCoordinates.slice(-1)[0], 0.04)} /> + <Source id="end-pin-top" type="geojson" data={AnimationUtility.createGeoJSONCircle(this.selectedRouteCoordinates.slice(-1)[0], 0.25)} /> + <Layer + id="start-fill-pin-base" + type="fill-extrusion" + source="start-pin-base" + paint={{ + 'fill-extrusion-color': '#0bfc03', + 'fill-extrusion-height': 1000, + }} + /> + <Layer + id="start-fill-pin-top" + type="fill-extrusion" + source="start-pin-top" + paint={{ + 'fill-extrusion-color': '#0bfc03', + 'fill-extrusion-base': 1000, + 'fill-extrusion-height': 1200, + }} + /> + <Layer + id="end-fill-pin-base" + type="fill-extrusion" + source="end-pin-base" + paint={{ + 'fill-extrusion-color': '#eb1c1c', + 'fill-extrusion-height': 1000, + }} + /> + <Layer + id="end-fill-pin-top" + type="fill-extrusion" + source="end-pin-top" + paint={{ + 'fill-extrusion-color': '#eb1c1c', + 'fill-extrusion-base': 1000, + 'fill-extrusion-height': 1200, + }} + /> + </> + )} + + <> + {!this.isAnimating && + this.animationPhase == 0 && + this.allPushpins + // .filter(anno => !anno.layout_unrendered) + .map((pushpin, idx) => ( + <Marker key={idx} longitude={NumCast(pushpin.longitude)} latitude={NumCast(pushpin.latitude)} anchor="bottom" onClick={(e: MarkerEvent<mapboxgl.Marker, MouseEvent>) => this.handleMarkerClick(e, pushpin)}> + {this.getMarkerIcon(pushpin)} + </Marker> + ))} + </> + + {/* {this.mapMarkers.length > 0 && this.mapMarkers.map((marker, idx) => ( + <Marker key={idx} longitude={marker.longitude} latitude={marker.latitude}/> + ))} */} + </MapboxMap> + </MapProvider> </div> - {/* </LoadScript > */} <div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> <SidebarAnnos ref={this._sidebarRef} - {...this.props} + {...this._props} fieldKey={this.fieldKey} - rootDoc={this.rootDoc} + Document={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} usePanelWidth={true} diff --git a/src/client/views/nodes/MapBox/MapBox2.tsx b/src/client/views/nodes/MapBox/MapBox2.tsx index 407a91dd0..9825824bd 100644 --- a/src/client/views/nodes/MapBox/MapBox2.tsx +++ b/src/client/views/nodes/MapBox/MapBox2.tsx @@ -1,642 +1,597 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-maps/api'; -import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; -import { Width } from '../../../../fields/DocSymbols'; -import { Id } from '../../../../fields/FieldSymbols'; -import { InkTool } from '../../../../fields/InkField'; -import { NumCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils'; -import { Docs } from '../../../documents/Documents'; -import { DragManager } from '../../../util/DragManager'; -import { SnappingManager } from '../../../util/SnappingManager'; -import { UndoManager } from '../../../util/UndoManager'; -import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm'; -import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent'; -import { Colors } from '../../global/globalEnums'; -import { MarqueeAnnotator } from '../../MarqueeAnnotator'; -import { AnchorMenu } from '../../pdf/AnchorMenu'; -import { Annotation } from '../../pdf/Annotation'; -import { SidebarAnnos } from '../../SidebarAnnos'; -import { FieldView, FieldViewProps } from '../FieldView'; -import { PinProps } from '../trails'; -import './MapBox2.scss'; -import { MapBoxInfoWindow } from './MapBoxInfoWindow'; - -/** - * MapBox2 architecture: - * Main component: MapBox2.tsx - * Supporting Components: SidebarAnnos, CollectionStackingView - * - * MapBox2 is a node that extends the ViewBoxAnnotatableComponent. Similar to PDFBox and WebBox, it supports interaction between sidebar content and document content. - * The main body of MapBox2 uses Google Maps API to allow location retrieval, adding map markers, pan and zoom, and open street view. - * Dash Document architecture is integrated with Maps API: When drag and dropping documents with ExifData (gps Latitude and Longitude information) available, - * sidebarAddDocument function checks if the document contains lat & lng information, if it does, then the document is added to both the sidebar and the infowindow (a pop up corresponding to a map marker--pin on map). - * The lat and lng field of the document is filled when importing (spec see ConvertDMSToDD method and processFileUpload method in Documents.ts). - * A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps - */ - -// const _global = (window /* browser */ || global /* node */) as any; - -const mapContainerStyle = { - height: '100%', -}; - -const defaultCenter = { - lat: 42.360081, - lng: -71.058884, -}; - -const mapOptions = { - fullscreenControl: false, -}; - -const apiKey = process.env.GOOGLE_MAPS; - -const script = document.createElement('script'); -script.defer = true; -script.async = true; -script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,drawing`; -console.log(script.src); -document.head.appendChild(script); - -/** - * Consider integrating later: allows for drawing, circling, making shapes on map - */ -// const drawingManager = new window.google.maps.drawing.DrawingManager({ -// drawingControl: true, -// drawingControlOptions: { -// position: google.maps.ControlPosition.TOP_RIGHT, -// drawingModes: [ -// google.maps.drawing.OverlayType.MARKER, -// // currently we are not supporting the following drawing mode on map, a thought for future development -// google.maps.drawing.OverlayType.CIRCLE, -// google.maps.drawing.OverlayType.POLYLINE, -// ], -// }, -// }); - -// options for searchbox in Google Maps Places Autocomplete API -const options = { - fields: ['formatted_address', 'geometry', 'name'], // note: level of details is charged by item per retrieval, not recommended to return all fields - strictBounds: false, - types: ['establishment'], // type pf places, subject of change according to user need -} as google.maps.places.AutocompleteOptions; - -@observer -export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps & Partial<GoogleMapProps>>() { - private _dropDisposer?: DragManager.DragDropDisposer; - private _disposers: { [name: string]: IReactionDisposer } = {}; - private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); - @observable private _overlayAnnoInfo: Opt<Doc>; - showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno)); - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(MapBox2, fieldKey); - } - public get SidebarKey() { - return this.fieldKey + '_sidebar'; - } - private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); - @computed get inlineTextAnnotations() { - return this.allMapMarkers.filter(a => a.text_inlineAnnotations); - } - - @observable private _map: google.maps.Map = null as unknown as google.maps.Map; - @observable private selectedPlace: Doc | undefined; - @observable private markerMap: { [id: string]: google.maps.Marker } = {}; - @observable private center = navigator.geolocation ? navigator.geolocation.getCurrentPosition : defaultCenter; - @observable private _marqueeing: number[] | undefined; - @observable private _isAnnotating = false; - @observable private inputRef = React.createRef<HTMLInputElement>(); - @observable private searchMarkers: google.maps.Marker[] = []; - @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, options); - @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); - @computed get allSidebarDocs() { - return DocListCast(this.dataDoc[this.SidebarKey]); - } - @computed get allMapMarkers() { - return DocListCast(this.dataDoc[this.annotationKey]); - } - @observable private toggleAddMarker = false; - private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); - - @observable _showSidebar = false; - @computed get SidebarShown() { - return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; - } - - static _canAnnotate = true; - static _hadSelection: boolean = false; - private _sidebarRef = React.createRef<SidebarAnnos>(); - private _ref: React.RefObject<HTMLDivElement> = React.createRef(); - - componentDidMount() { - this.props.setContentView?.(this); - } - - @action - private setSearchBox = (searchBox: any) => { - this.searchBox = searchBox; - }; - - // iterate allMarkers to size, center, and zoom map to contain all markers - private fitBounds = (map: google.maps.Map) => { - const curBounds = map.getBounds() ?? new window.google.maps.LatLngBounds(); - const isFitting = this.allMapMarkers.reduce((fits, place) => fits && curBounds?.contains({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), true as boolean); - !isFitting && map.fitBounds(this.allMapMarkers.reduce((bounds, place) => bounds.extend({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), new window.google.maps.LatLngBounds())); - }; - - /** - * Custom control for add marker button - * @param controlDiv - * @param map - */ - private CenterControl = () => { - const controlDiv = document.createElement('div'); - controlDiv.className = 'MapBox2-addMarker'; - // Set CSS for the control border. - const controlUI = document.createElement('div'); - controlUI.style.backgroundColor = '#fff'; - controlUI.style.borderRadius = '3px'; - controlUI.style.cursor = 'pointer'; - controlUI.style.marginTop = '10px'; - controlUI.style.borderRadius = '4px'; - controlUI.style.marginBottom = '22px'; - controlUI.style.textAlign = 'center'; - controlUI.style.position = 'absolute'; - controlUI.style.width = '32px'; - controlUI.style.height = '32px'; - controlUI.title = 'Click to toggle marker mode. In marker mode, click on map to place a marker.'; - - const plIcon = document.createElement('img'); - plIcon.src = 'https://cdn4.iconfinder.com/data/icons/wirecons-free-vector-icons/32/add-256.png'; - plIcon.style.color = 'rgb(25,25,25)'; - plIcon.style.fontFamily = 'Roboto,Arial,sans-serif'; - plIcon.style.fontSize = '16px'; - plIcon.style.lineHeight = '32px'; - plIcon.style.left = '18'; - plIcon.style.top = '15'; - plIcon.style.position = 'absolute'; - plIcon.width = 14; - plIcon.height = 14; - plIcon.innerHTML = 'Add'; - controlUI.appendChild(plIcon); - - // Set CSS for the control interior. - const markerIcon = document.createElement('img'); - markerIcon.src = 'https://cdn0.iconfinder.com/data/icons/small-n-flat/24/678111-map-marker-1024.png'; - markerIcon.style.color = 'rgb(25,25,25)'; - markerIcon.style.fontFamily = 'Roboto,Arial,sans-serif'; - markerIcon.style.fontSize = '16px'; - markerIcon.style.lineHeight = '32px'; - markerIcon.style.left = '-2'; - markerIcon.style.top = '1'; - markerIcon.width = 30; - markerIcon.height = 30; - markerIcon.style.position = 'absolute'; - markerIcon.innerHTML = 'Add'; - controlUI.appendChild(markerIcon); - - // Setup the click event listeners - controlUI.addEventListener('click', () => { - if (this.toggleAddMarker === true) { - this.toggleAddMarker = false; - console.log('add marker button status:' + this.toggleAddMarker); - controlUI.style.backgroundColor = '#fff'; - markerIcon.style.color = 'rgb(25,25,25)'; - } else { - this.toggleAddMarker = true; - console.log('add marker button status:' + this.toggleAddMarker); - controlUI.style.backgroundColor = '#4476f7'; - markerIcon.style.color = 'rgb(255,255,255)'; - } - }); - controlDiv.appendChild(controlUI); - return controlDiv; - }; - - /** - * Place the marker on google maps & store the empty marker as a MapMarker Document in allMarkers list - * @param position - the LatLng position where the marker is placed - * @param map - */ - @action - private placeMarker = (position: google.maps.LatLng, map: google.maps.Map) => { - const marker = new google.maps.Marker({ - position: position, - map: map, - }); - map.panTo(position); - const mapMarker = Docs.Create.PushpinDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {}); - this.addDocument(mapMarker, this.annotationKey); - }; - - _loadPending = true; - /** - * store a reference to google map instance - * setup the drawing manager on the top right corner of map - * fit map bounds to contain all markers - * @param map - */ - @action - private loadHandler = (map: google.maps.Map) => { - this._map = map; - this._loadPending = true; - const centerControlDiv = this.CenterControl(); - map.controls[google.maps.ControlPosition.TOP_RIGHT].push(centerControlDiv); - //drawingManager.setMap(map); - // if (navigator.geolocation) { - // navigator.geolocation.getCurrentPosition( - // (position: Position) => { - // const pos = { - // lat: position.coords.latitude, - // lng: position.coords.longitude, - // }; - // this._map.setCenter(pos); - // } - // ); - // } else { - // alert("Your geolocation is not supported by browser.") - // }; - map.setZoom(NumCast(this.dataDoc.map_zoom, 2.5)); - map.setCenter(new google.maps.LatLng(NumCast(this.dataDoc.mapLat), NumCast(this.dataDoc.mapLng))); - setTimeout(() => { - if (this._loadPending && this._map.getBounds()) { - this._loadPending = false; - this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); - } - }, 250); - // listener to addmarker event - this._map.addListener('click', (e: MouseEvent) => { - if (this.toggleAddMarker === true) { - this.placeMarker((e as any).latLng, map); - } - }); - }; - - @action - centered = () => { - if (this._loadPending && this._map.getBounds()) { - this._loadPending = false; - this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); - } - this.dataDoc.mapLat = this._map.getCenter()?.lat(); - this.dataDoc.mapLng = this._map.getCenter()?.lng(); - }; - - @action - zoomChanged = () => { - if (this._loadPending && this._map.getBounds()) { - this._loadPending = false; - this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); - } - this.dataDoc.map_zoom = this._map.getZoom(); - }; - - /** - * Load and render all map markers - * @param marker - * @param place - */ - @action - private markerLoadHandler = (marker: google.maps.Marker, place: Doc) => { - place[Id] ? (this.markerMap[place[Id]] = marker) : null; - }; - - /** - * on clicking the map marker, set the selected place to the marker document & set infowindowopen to be true - * @param e - * @param place - */ - @action - private markerClickHandler = (e: google.maps.MapMouseEvent, place: Doc) => { - // set which place was clicked - this.selectedPlace = place; - place.infoWindowOpen = true; - }; - - /** - * Called when dragging documents into map sidebar or directly into infowindow; to create a map marker, ref to MapMarkerDocument in Documents.ts - * @param doc - * @param sidebarKey - * @returns - */ - sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { - console.log('print all sidebar Docs'); - if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); - const docs = doc instanceof Doc ? [doc] : doc; - docs.forEach(doc => { - if (doc.lat !== undefined && doc.lng !== undefined) { - const existingMarker = this.allMapMarkers.find(marker => marker.lat === doc.lat && marker.lng === doc.lng); - if (existingMarker) { - Doc.AddDocToList(existingMarker, 'data', doc); - } else { - const marker = Docs.Create.PushpinDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {}); - this.addDocument(marker, this.annotationKey); - } - } - }); //add to annotation list - - return this.addDocument(doc, sidebarKey); // add to sidebar list - }; - - /** - * Removing documents from the sidebar - * @param doc - * @param sidebarKey - * @returns - */ - sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => { - if (this.layoutDoc._layout_showSidebar) this.toggleSidebar(); - const docs = doc instanceof Doc ? [doc] : doc; - return this.removeDocument(doc, sidebarKey); - }; - - /** - * Toggle sidebar onclick the tiny comment button on the top right corner - * @param e - */ - sidebarBtnDown = (e: React.PointerEvent) => { - setupMoveUpEvents( - this, - e, - (e, down, delta) => - runInAction(() => { - const localDelta = this.props - .ScreenToLocalTransform() - .scale(this.props.NativeDimScaling?.() || 1) - .transformDirection(delta[0], delta[1]); - const fullWidth = this.layoutDoc[Width](); - const mapWidth = fullWidth - this.sidebarWidth(); - if (this.sidebarWidth() + localDelta[0] > 0) { - this._showSidebar = true; - this.layoutDoc._width = fullWidth + localDelta[0]; - this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%'; - } else { - this._showSidebar = false; - this.layoutDoc._width = mapWidth; - this.layoutDoc._layout_sidebarWidthPercent = '0%'; - } - return false; - }), - emptyFunction, - () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map') - ); - }; - - sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); - @computed get layout_sidebarWidthPercent() { - return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); - } - @computed get sidebarColor() { - return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4')); - } - - /** - * function that reads the place inputed from searchbox, then zoom in on the location that's been autocompleted; - * add a customized temporary marker on the map - */ - @action - private handlePlaceChanged = () => { - const place = this.searchBox.getPlace(); - - if (!place.geometry || !place.geometry.location) { - // user entered the name of a place that wasn't suggested & pressed the enter key, or place details request failed - window.alert("No details available for input: '" + place.name + "'"); - return; - } - - // zoom in on the location of the search result - if (place.geometry.viewport) { - this._map.fitBounds(place.geometry.viewport); - } else { - this._map.setCenter(place.geometry.location); - this._map.setZoom(17); - } - - // customize icon => customized icon for the nature of the location selected - const icon = { - url: place.icon as string, - size: new google.maps.Size(71, 71), - origin: new google.maps.Point(0, 0), - anchor: new google.maps.Point(17, 34), - scaledSize: new google.maps.Size(25, 25), - }; - - // put temporary cutomized marker on searched location - this.searchMarkers.forEach(marker => { - marker.setMap(null); - }); - this.searchMarkers = []; - this.searchMarkers.push( - new window.google.maps.Marker({ - map: this._map, - icon, - title: place.name, - position: place.geometry.location, - }) - ); - }; - - /** - * Handles toggle of sidebar on click the little comment button - */ - @computed get sidebarHandle() { - return ( - <div - className="MapBox2-overlayButton-sidebar" - key="sidebar" - title="Toggle Sidebar" - style={{ - display: !this.props.isContentActive() ? 'none' : undefined, - top: StrCast(this.rootDoc._layout_showTitle) === 'title' ? 20 : 5, - backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, - }} - onPointerDown={this.sidebarBtnDown}> - <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> - </div> - ); - } - - // TODO: Adding highlight box layer to Maps - @action - toggleSidebar = () => { - //1.2 * w * ? = .2 * w .2/1.2 - const prevWidth = this.sidebarWidth(); - this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%'; - this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); - }; - - sidebarDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true); - }; - sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { - const bounds = this._ref.current!.getBoundingClientRect(); - this.layoutDoc._layout_sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%'; - this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%'; - e.preventDefault(); - return false; - }; - - setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func); - - @action - onMarqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { - setupMoveUpEvents( - this, - e, - action(e => { - MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeing = [e.clientX, e.clientY]; - return true; - }), - returnFalse, - () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), - false - ); - } - }; - @action finishMarquee = (x?: number, y?: number) => { - this._marqueeing = undefined; - this._isAnnotating = false; - x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false, this.props.Document); - }; - - addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { - return this.addDocument(doc, annotationKey); - }; - - pointerEvents = () => { - return this.props.isContentActive() === false ? 'none' : this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'; - }; - @computed get annotationLayer() { - return ( - <div className="MapBox2-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> - {this.inlineTextAnnotations - .sort((a, b) => NumCast(a.y) - NumCast(b.y)) - .map(anno => ( - <Annotation key={`${anno[Id]}-annotation`} {...this.props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} /> - ))} - </div> - ); - } - - getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.rootDoc; - - /** - * render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker - * @returns - */ - private renderMarkers = () => { - return this.allMapMarkers.map(place => ( - <Marker key={place[Id]} position={{ lat: NumCast(place.lat), lng: NumCast(place.lng) }} onLoad={marker => this.markerLoadHandler(marker, place)} onClick={(e: google.maps.MapMouseEvent) => this.markerClickHandler(e, place)} /> - )); - }; - - // TODO: auto center on select a document in the sidebar - private handleMapCenter = (map: google.maps.Map) => { - // console.log("print the selected views in selectionManager:") - // if (SelectionManager.Views().lastElement()) { - // console.log(SelectionManager.Views().lastElement()); - // } - }; - - panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth(); - panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); - scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); - transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.childFilters(), Utils.IsOpaqueFilter()]; - infoWidth = () => this.props.PanelWidth() / 5; - infoHeight = () => this.props.PanelHeight() / 5; - anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; - savedAnnotations = () => this._savedAnnotations; - - get MicrosoftMaps() { - return (window as any).Microsoft.Maps; - } - render() { - const renderAnnotations = (childFilters?: () => string[]) => null; - return ( - <div className="MapBox2" ref={this._ref}> - <div - className="MapBox2-wrapper" - onWheel={e => e.stopPropagation()} - onPointerDown={async e => { - e.button === 0 && !e.ctrlKey && e.stopPropagation(); - }} - style={{ width: `calc(100% - ${this.layout_sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> - <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> - {renderAnnotations(this.opaqueFilter)} - {SnappingManager.GetIsDragging() ? null : renderAnnotations()} - {this.annotationLayer} - - <div> - <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}> - <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}> - <input className="MapBox2-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" /> - </Autocomplete> - - {this.renderMarkers()} - {this.allMapMarkers - .filter(marker => marker.infoWindowOpen) - .map(marker => ( - <MapBoxInfoWindow - key={marker[Id]} - {...this.props} - setContentView={emptyFunction} - place={marker} - markerMap={this.markerMap} - PanelWidth={this.infoWidth} - PanelHeight={this.infoHeight} - moveDocument={this.moveDocument} - isAnyChildContentActive={this.isAnyChildContentActive} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - /> - ))} - {/* {this.handleMapCenter(this._map)} */} - </GoogleMap> - </div> - {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( - <MarqueeAnnotator - rootDoc={this.rootDoc} - anchorMenuClick={this.anchorMenuClick} - scrollTop={0} - down={this._marqueeing} - scaling={returnOne} - addDocument={this.addDocumentWrapper} - docView={this.props.docViewPath().lastElement()} - finishMarquee={this.finishMarquee} - savedAnnotations={this.savedAnnotations} - annotationLayer={this._annotationLayer.current} - selectionText={returnEmptyString} - mainCont={this._mainCont.current} - /> - )} - </div> - {/* </LoadScript > */} - <div className="MapBox2-sidebar" style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> - <SidebarAnnos - ref={this._sidebarRef} - {...this.props} - fieldKey={this.fieldKey} - rootDoc={this.rootDoc} - layoutDoc={this.layoutDoc} - dataDoc={this.dataDoc} - usePanelWidth={true} - showSidebar={this.SidebarShown} - nativeWidth={NumCast(this.layoutDoc._nativeWidth)} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - PanelWidth={this.sidebarWidth} - sidebarAddDocument={this.sidebarAddDocument} - moveDocument={this.moveDocument} - removeDocument={this.sidebarRemoveDocument} - /> - </div> - {this.sidebarHandle} - </div> - ); - } -} +// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +// import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-maps/api'; +// import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx'; +// import { observer } from 'mobx-react'; +// import * as React from 'react'; +// import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; +// import { Id } from '../../../../fields/FieldSymbols'; +// import { NumCast, StrCast } from '../../../../fields/Types'; +// import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; +// import { Docs } from '../../../documents/Documents'; +// import { DragManager } from '../../../util/DragManager'; +// import { SnappingManager } from '../../../util/SnappingManager'; +// import { UndoManager } from '../../../util/UndoManager'; +// import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm'; +// import { ViewBoxAnnotatableComponent } from '../../DocComponent'; +// import { Colors } from '../../global/globalEnums'; +// import { AnchorMenu } from '../../pdf/AnchorMenu'; +// import { Annotation } from '../../pdf/Annotation'; +// import { SidebarAnnos } from '../../SidebarAnnos'; +// import { FieldView, FieldViewProps } from '../FieldView'; +// import { PinProps } from '../trails'; +// import './MapBox2.scss'; +// import { MapBoxInfoWindow } from './MapBoxInfoWindow'; + +// /** +// * MapBox2 architecture: +// * Main component: MapBox2.tsx +// * Supporting Components: SidebarAnnos, CollectionStackingView +// * +// * MapBox2 is a node that extends the ViewBoxAnnotatableComponent. Similar to PDFBox and WebBox, it supports interaction between sidebar content and document content. +// * The main body of MapBox2 uses Google Maps API to allow location retrieval, adding map markers, pan and zoom, and open street view. +// * Dash Document architecture is integrated with Maps API: When drag and dropping documents with ExifData (gps Latitude and Longitude information) available, +// * sidebarAddDocument function checks if the document contains lat & lng information, if it does, then the document is added to both the sidebar and the infowindow (a pop up corresponding to a map marker--pin on map). +// * The lat and lng field of the document is filled when importing (spec see ConvertDMSToDD method and processFileUpload method in Documents.ts). +// * A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps +// */ + +// // const _global = (window /* browser */ || global /* node */) as any; + +// const mapContainerStyle = { +// height: '100%', +// }; + +// const defaultCenter = { +// lat: 42.360081, +// lng: -71.058884, +// }; + +// const mapOptions = { +// fullscreenControl: false, +// }; + +// const apiKey = process.env.GOOGLE_MAPS; + +// const script = document.createElement('script'); +// script.defer = true; +// script.async = true; +// script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,drawing`; +// console.log(script.src); +// document.head.appendChild(script); + +// /** +// * Consider integrating later: allows for drawing, circling, making shapes on map +// */ +// // const drawingManager = new window.google.maps.drawing.DrawingManager({ +// // drawingControl: true, +// // drawingControlOptions: { +// // position: google.maps.ControlPosition.TOP_RIGHT, +// // drawingModes: [ +// // google.maps.drawing.OverlayType.MARKER, +// // // currently we are not supporting the following drawing mode on map, a thought for future development +// // google.maps.drawing.OverlayType.CIRCLE, +// // google.maps.drawing.OverlayType.POLYLINE, +// // ], +// // }, +// // }); + +// // options for searchbox in Google Maps Places Autocomplete API +// const options = { +// fields: ['formatted_address', 'geometry', 'name'], // note: level of details is charged by item per retrieval, not recommended to return all fields +// strictBounds: false, +// types: ['establishment'], // type pf places, subject of change according to user need +// } as google.maps.places.AutocompleteOptions; + +// @observer +// export class MapBox2 extends ViewBoxAnnotatableComponent<FieldViewProps & Partial<GoogleMapProps>>() { +// private _dropDisposer?: DragManager.DragDropDisposer; +// private _disposers: { [name: string]: IReactionDisposer } = {}; +// private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); +// @observable private _overlayAnnoInfo: Opt<Doc> = undefined; +// showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno)); +// public static LayoutString(fieldKey: string) { +// return FieldView.LayoutString(MapBox2, fieldKey); +// } +// public get SidebarKey() { +// return this.fieldKey + '_sidebar'; +// } +// private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); +// @computed get inlineTextAnnotations() { +// return this.allMapMarkers.filter(a => a.text_inlineAnnotations); +// } + +// @observable private _map: google.maps.Map = null as unknown as google.maps.Map; +// @observable private selectedPlace: Doc | undefined = undefined; +// @observable private markerMap: { [id: string]: google.maps.Marker } = {}; +// @observable private center = navigator.geolocation ? navigator.geolocation.getCurrentPosition : defaultCenter; +// @observable private inputRef = React.createRef<HTMLInputElement>(); +// @observable private searchMarkers: google.maps.Marker[] = []; +// @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, options); +// @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); +// @computed get allSidebarDocs() { +// return DocListCast(this.dataDoc[this.SidebarKey]); +// } +// @computed get allMapMarkers() { +// return DocListCast(this.dataDoc[this.annotationKey]); +// } +// @observable private toggleAddMarker = false; + +// @observable _showSidebar = false; +// @computed get SidebarShown() { +// return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; +// } + +// static _canAnnotate = true; +// static _hadSelection: boolean = false; +// private _sidebarRef = React.createRef<SidebarAnnos>(); +// private _ref: React.RefObject<HTMLDivElement> = React.createRef(); + +// componentDidMount() { +// this._props.setContentView?.(this); +// } + +// @action +// private setSearchBox = (searchBox: any) => { +// this.searchBox = searchBox; +// }; + +// // iterate allMarkers to size, center, and zoom map to contain all markers +// private fitBounds = (map: google.maps.Map) => { +// const curBounds = map.getBounds ?? new window.google.maps.LatLngBounds(); +// const isFitting = this.allMapMarkers.reduce((fits, place) => fits && curBounds?.contains({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), true as boolean); +// !isFitting && map.fitBounds(this.allMapMarkers.reduce((bounds, place) => bounds.extend({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), new window.google.maps.LatLngBounds())); +// }; + +// /** +// * Custom control for add marker button +// * @param controlDiv +// * @param map +// */ +// private CenterControl = () => { +// const controlDiv = document.createElement('div'); +// controlDiv.className = 'MapBox2-addMarker'; +// // Set CSS for the control border. +// const controlUI = document.createElement('div'); +// controlUI.style.backgroundColor = '#fff'; +// controlUI.style.borderRadius = '3px'; +// controlUI.style.cursor = 'pointer'; +// controlUI.style.marginTop = '10px'; +// controlUI.style.borderRadius = '4px'; +// controlUI.style.marginBottom = '22px'; +// controlUI.style.textAlign = 'center'; +// controlUI.style.position = 'absolute'; +// controlUI.style.width = '32px'; +// controlUI.style.height = '32px'; +// controlUI.title = 'Click to toggle marker mode. In marker mode, click on map to place a marker.'; + +// const plIcon = document.createElement('img'); +// plIcon.src = 'https://cdn4.iconfinder.com/data/icons/wirecons-free-vector-icons/32/add-256.png'; +// plIcon.style.color = 'rgb(25,25,25)'; +// plIcon.style.fontFamily = 'Roboto,Arial,sans-serif'; +// plIcon.style.fontSize = '16px'; +// plIcon.style.lineHeight = '32px'; +// plIcon.style.left = '18'; +// plIcon.style.top = '15'; +// plIcon.style.position = 'absolute'; +// plIcon.width = 14; +// plIcon.height = 14; +// plIcon.innerHTML = 'Add'; +// controlUI.appendChild(plIcon); + +// // Set CSS for the control interior. +// const markerIcon = document.createElement('img'); +// markerIcon.src = 'https://cdn0.iconfinder.com/data/icons/small-n-flat/24/678111-map-marker-1024.png'; +// markerIcon.style.color = 'rgb(25,25,25)'; +// markerIcon.style.fontFamily = 'Roboto,Arial,sans-serif'; +// markerIcon.style.fontSize = '16px'; +// markerIcon.style.lineHeight = '32px'; +// markerIcon.style.left = '-2'; +// markerIcon.style.top = '1'; +// markerIcon.width = 30; +// markerIcon.height = 30; +// markerIcon.style.position = 'absolute'; +// markerIcon.innerHTML = 'Add'; +// controlUI.appendChild(markerIcon); + +// // Setup the click event listeners +// controlUI.addEventListener('click', () => { +// if (this.toggleAddMarker === true) { +// this.toggleAddMarker = false; +// console.log('add marker button status:' + this.toggleAddMarker); +// controlUI.style.backgroundColor = '#fff'; +// markerIcon.style.color = 'rgb(25,25,25)'; +// } else { +// this.toggleAddMarker = true; +// console.log('add marker button status:' + this.toggleAddMarker); +// controlUI.style.backgroundColor = '#4476f7'; +// markerIcon.style.color = 'rgb(255,255,255)'; +// } +// }); +// controlDiv.appendChild(controlUI); +// return controlDiv; +// }; + +// /** +// * Place the marker on google maps & store the empty marker as a MapMarker Document in allMarkers list +// * @param position - the LatLng position where the marker is placed +// * @param map +// */ +// @action +// private placeMarker = (position: google.maps.LatLng, map: google.maps.Map) => { +// const marker = new google.maps.Marker({ +// position: position, +// map: map, +// }); +// map.panTo(position); +// const mapMarker = Docs.Create.PushpinDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {}); +// this.addDocument(mapMarker, this.annotationKey); +// }; + +// _loadPending = true; +// /** +// * store a reference to google map instance +// * setup the drawing manager on the top right corner of map +// * fit map bounds to contain all markers +// * @param map +// */ +// @action +// private loadHandler = (map: google.maps.Map) => { +// this._map = map; +// this._loadPending = true; +// const centerControlDiv = this.CenterControl(); +// map.controls[google.maps.ControlPosition.TOP_RIGHT].push(centerControlDiv); +// //drawingManager.setMap(map); +// // if (navigator.geolocation) { +// // navigator.geolocation.getCurrentPosition( +// // (position: Position) => { +// // const pos = { +// // lat: position.coords.latitude, +// // lng: position.coords.longitude, +// // }; +// // this._map.setCenter(pos); +// // } +// // ); +// // } else { +// // alert("Your geolocation is not supported by browser.") +// // }; +// map.setZoom(NumCast(this.dataDoc.map_zoom, 2.5)); +// map.setCenter(new google.maps.LatLng(NumCast(this.dataDoc.mapLat), NumCast(this.dataDoc.mapLng))); +// setTimeout(() => { +// if (this._loadPending && this._map.getBounds) { +// this._loadPending = false; +// this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); +// } +// }, 250); +// // listener to addmarker event +// this._map.addListener('click', (e: MouseEvent) => { +// if (this.toggleAddMarker === true) { +// this.placeMarker((e as any).latLng, map); +// } +// }); +// }; + +// @action +// centered = () => { +// if (this._loadPending && this._map.getBounds) { +// this._loadPending = false; +// this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); +// } +// this.dataDoc.mapLat = this._map.getCenter()?.lat(); +// this.dataDoc.mapLng = this._map.getCenter()?.lng(); +// }; + +// @action +// zoomChanged = () => { +// if (this._loadPending && this._map.getBounds) { +// this._loadPending = false; +// this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); +// } +// this.dataDoc.map_zoom = this._map.getZoom(); +// }; + +// /** +// * Load and render all map markers +// * @param marker +// * @param place +// */ +// @action +// private markerLoadHandler = (marker: google.maps.Marker, place: Doc) => { +// place[Id] ? (this.markerMap[place[Id]] = marker) : null; +// }; + +// /** +// * on clicking the map marker, set the selected place to the marker document & set infowindowopen to be true +// * @param e +// * @param place +// */ +// @action +// private markerClickHandler = (e: google.maps.MapMouseEvent, place: Doc) => { +// // set which place was clicked +// this.selectedPlace = place; +// place.infoWindowOpen = true; +// }; + +// /** +// * Called when dragging documents into map sidebar or directly into infowindow; to create a map marker, ref to MapMarkerDocument in Documents.ts +// * @param doc +// * @param sidebarKey +// * @returns +// */ +// sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { +// console.log('print all sidebar Docs'); +// if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); +// const docs = doc instanceof Doc ? [doc] : doc; +// docs.forEach(doc => { +// if (doc.lat !== undefined && doc.lng !== undefined) { +// const existingMarker = this.allMapMarkers.find(marker => marker.lat === doc.lat && marker.lng === doc.lng); +// if (existingMarker) { +// Doc.AddDocToList(existingMarker, 'data', doc); +// } else { +// const marker = Docs.Create.PushpinDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {}); +// this.addDocument(marker, this.annotationKey); +// } +// } +// }); //add to annotation list + +// return this.addDocument(doc, sidebarKey); // add to sidebar list +// }; + +// /** +// * Removing documents from the sidebar +// * @param doc +// * @param sidebarKey +// * @returns +// */ +// sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => { +// if (this.layoutDoc._layout_showSidebar) this.toggleSidebar(); +// const docs = doc instanceof Doc ? [doc] : doc; +// return this.removeDocument(doc, sidebarKey); +// }; + +// /** +// * Toggle sidebar onclick the tiny comment button on the top right corner +// * @param e +// */ +// sidebarBtnDown = (e: React.PointerEvent) => { +// setupMoveUpEvents( +// this, +// e, +// (e, down, delta) => +// runInAction(() => { +// const localDelta = this._props +// .ScreenToLocalTransform() +// .scale(this._props.NativeDimScaling?.() || 1) +// .transformDirection(delta[0], delta[1]); +// const fullWidth = NumCast(this.layoutDoc._width); +// const mapWidth = fullWidth - this.sidebarWidth(); +// if (this.sidebarWidth() + localDelta[0] > 0) { +// this._showSidebar = true; +// this.layoutDoc._width = fullWidth + localDelta[0]; +// this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%'; +// } else { +// this._showSidebar = false; +// this.layoutDoc._width = mapWidth; +// this.layoutDoc._layout_sidebarWidthPercent = '0%'; +// } +// return false; +// }), +// emptyFunction, +// () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map') +// ); +// }; + +// sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); +// @computed get layout_sidebarWidthPercent() { +// return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); +// } +// @computed get sidebarColor() { +// return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4')); +// } + +// /** +// * function that reads the place inputed from searchbox, then zoom in on the location that's been autocompleted; +// * add a customized temporary marker on the map +// */ +// @action +// private handlePlaceChanged = () => { +// const place = this.searchBox.getPlace(); + +// if (!place.geometry || !place.geometry.location) { +// // user entered the name of a place that wasn't suggested & pressed the enter key, or place details request failed +// window.alert("No details available for input: '" + place.name + "'"); +// return; +// } + +// // zoom in on the location of the search result +// if (place.geometry.viewport) { +// this._map.fitBounds(place.geometry.viewport); +// } else { +// this._map.setCenter(place.geometry.location); +// this._map.setZoom(17); +// } + +// // customize icon => customized icon for the nature of the location selected +// const icon = { +// url: place.icon as string, +// size: new google.maps.Size(71, 71), +// origin: new google.maps.Point(0, 0), +// anchor: new google.maps.Point(17, 34), +// scaledSize: new google.maps.Size(25, 25), +// }; + +// // put temporary cutomized marker on searched location +// this.searchMarkers.forEach(marker => { +// marker.setMap(null); +// }); +// this.searchMarkers = []; +// this.searchMarkers.push( +// new window.google.maps.Marker({ +// map: this._map, +// icon, +// title: place.name, +// position: place.geometry.location, +// }) +// ); +// }; + +// /** +// * Handles toggle of sidebar on click the little comment button +// */ +// @computed get sidebarHandle() { +// return ( +// <div +// className="MapBox2-overlayButton-sidebar" +// key="sidebar" +// title="Toggle Sidebar" +// style={{ +// display: !this._props.isContentActive() ? 'none' : undefined, +// top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5, +// backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, +// }} +// onPointerDown={this.sidebarBtnDown}> +// <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> +// </div> +// ); +// } + +// // TODO: Adding highlight box layer to Maps +// @action +// toggleSidebar = () => { +// //1.2 * w * ? = .2 * w .2/1.2 +// const prevWidth = this.sidebarWidth(); +// this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%'; +// this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); +// }; + +// sidebarDown = (e: React.PointerEvent) => { +// setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true); +// }; +// sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { +// const bounds = this._ref.current!.getBoundingClientRect(); +// this.layoutDoc._layout_sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%'; +// this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%'; +// e.preventDefault(); +// return false; +// }; + +// setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func); + +// addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { +// return this.addDocument(doc, annotationKey); +// }; + +// pointerEvents = () => { +// return this._props.isContentActive() === false ? 'none' : this._props.isContentActive() && this._props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.IsDragging ? undefined : 'none'; +// }; +// @computed get annotationLayer() { +// return ( +// <div className="MapBox2-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> +// {this.inlineTextAnnotations +// .sort((a, b) => NumCast(a.y) - NumCast(b.y)) +// .map(anno => ( +// <Annotation key={`${anno[Id]}-annotation`} {...this._props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} /> +// ))} +// </div> +// ); +// } + +// getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.Document; + +// /** +// * render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker +// * @returns +// */ +// private renderMarkers = () => { +// return this.allMapMarkers.map(place => ( +// <Marker key={place[Id]} position={{ lat: NumCast(place.lat), lng: NumCast(place.lng) }} onLoad={marker => this.markerLoadHandler(marker, place)} onClick={(e: google.maps.MapMouseEvent) => this.markerClickHandler(e, place)} /> +// )); +// }; + +// // TODO: auto center on select a document in the sidebar +// private handleMapCenter = (map: google.maps.Map) => { +// // console.log("print the selected views in selectionManager:") +// // if (SelectionManager.Views.lastElement()) { +// // console.log(SelectionManager.Views.lastElement()); +// // } +// }; + +// panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1) - this.sidebarWidth(); +// panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); +// scrollXf = () => this.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); +// transparentFilter = () => [...this._props.childFilters(), Utils.TransparentBackgroundFilter]; +// opaqueFilter = () => [...this._props.childFilters(), Utils.OpaqueBackgroundFilter]; +// infoWidth = () => this._props.PanelWidth() / 5; +// infoHeight = () => this._props.PanelHeight() / 5; +// anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; +// savedAnnotations = () => this._savedAnnotations; + +// get MicrosoftMaps() { +// return (window as any).Microsoft.Maps; +// } +// render() { +// const renderAnnotations = (childFilters?: () => string[]) => null; +// return ( +// <div className="MapBox2" ref={this._ref}> +// <div +// className="MapBox2-wrapper" +// onWheel={e => e.stopPropagation()} +// onPointerDown={async e => { +// e.button === 0 && !e.ctrlKey && e.stopPropagation(); +// }} +// style={{ width: `calc(100% - ${this.layout_sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> +// <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> +// {renderAnnotations(this.opaqueFilter)} +// {SnappingManager.IsDragging ? null : renderAnnotations()} +// {this.annotationLayer} + +// <div> +// <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}> +// <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}> +// <input className="MapBox2-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" /> +// </Autocomplete> + +// {this.renderMarkers()} +// {this.allMapMarkers +// .filter(marker => marker.infoWindowOpen) +// .map(marker => ( +// <MapBoxInfoWindow +// key={marker[Id]} +// {...this._props} +// setContentView={emptyFunction} +// place={marker} +// markerMap={this.markerMap} +// PanelWidth={this.infoWidth} +// PanelHeight={this.infoHeight} +// moveDocument={this.moveDocument} +// isAnyChildContentActive={this.isAnyChildContentActive} +// whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} +// /> +// ))} +// {/* {this.handleMapCenter(this._map)} */} +// </GoogleMap> +// </div> +// </div> +// {/* </LoadScript > */} +// <div className="MapBox2-sidebar" style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> +// <SidebarAnnos +// ref={this._sidebarRef} +// {...this._props} +// fieldKey={this.fieldKey} +// Document={this.Document} +// layoutDoc={this.layoutDoc} +// dataDoc={this.dataDoc} +// usePanelWidth={true} +// showSidebar={this.SidebarShown} +// nativeWidth={NumCast(this.layoutDoc._nativeWidth)} +// whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} +// PanelWidth={this.sidebarWidth} +// sidebarAddDocument={this.sidebarAddDocument} +// moveDocument={this.moveDocument} +// removeDocument={this.sidebarRemoveDocument} +// /> +// </div> +// {this.sidebarHandle} +// </div> +// ); +// } +// } diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx index 66c47d131..6ccbbbe1c 100644 --- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx +++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx @@ -1,96 +1,94 @@ -import { InfoWindow } from '@react-google-maps/api'; -import { action } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { Doc } from '../../../../fields/Doc'; -import { Id } from '../../../../fields/FieldSymbols'; -import { emptyFunction, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils'; -import { Docs } from '../../../documents/Documents'; -import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; -import { CollectionNoteTakingView } from '../../collections/CollectionNoteTakingView'; -import { CollectionStackingView } from '../../collections/CollectionStackingView'; -import { ViewBoxAnnotatableProps } from '../../DocComponent'; -import { FieldViewProps } from '../FieldView'; -import { FormattedTextBox } from '../formattedText/FormattedTextBox'; -import './MapBox.scss'; +// import { InfoWindow } from '@react-google-maps/api'; +// import { action } from 'mobx'; +// import { observer } from 'mobx-react'; +// import * as React from 'react'; +// import { Doc } from '../../../../fields/Doc'; +// import { Id } from '../../../../fields/FieldSymbols'; +// import { emptyFunction, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils'; +// import { Docs } from '../../../documents/Documents'; +// import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; +// import { CollectionNoteTakingView } from '../../collections/CollectionNoteTakingView'; +// import { CollectionStackingView } from '../../collections/CollectionStackingView'; +// import { FieldViewProps } from '../FieldView'; +// import { FormattedTextBox } from '../formattedText/FormattedTextBox'; +// import './MapBox.scss'; -interface MapBoxInfoWindowProps { - place: Doc; - renderDepth: number; - markerMap: { [id: string]: google.maps.Marker }; - isAnyChildContentActive: () => boolean; -} -@observer -export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & ViewBoxAnnotatableProps & FieldViewProps> { - @action - private handleInfoWindowClose = () => { - if (this.props.place.infoWindowOpen) { - this.props.place.infoWindowOpen = false; - } - this.props.place.infoWindowOpen = false; - }; +// interface MapBoxInfoWindowProps extends FieldViewProps { +// place: Doc; +// renderDepth: number; +// markerMap: { [id: string]: google.maps.Marker }; +// isAnyChildContentActive: () => boolean; +// } +// @observer +// export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps> { +// @action +// private handleInfoWindowClose = () => { +// if (this.props.place.infoWindowOpen) { +// this.props.place.infoWindowOpen = false; +// } +// this.props.place.infoWindowOpen = false; +// }; - addNoteClick = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => { - const newBox = Docs.Create.TextDocument('Note', { _layout_autoHeight: true }); - FormattedTextBox.SelectOnLoad = newBox[Id]; // track the new text box so we can give it a prop that tells it to focus itself when it's displayed - Doc.AddDocToList(this.props.place, 'data', newBox); - this._stack?.scrollToBottom(); - e.stopPropagation(); - e.preventDefault(); - }); - }; +// addNoteClick = (e: React.PointerEvent) => { +// setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => { +// const newDoc = Docs.Create.TextDocument('Note', { _layout_autoHeight: true }); +// FormattedTextBox.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed +// Doc.AddDocToList(this.props.place, 'data', newDoc); +// this._stack?.scrollToBottom(); +// e.stopPropagation(); +// e.preventDefault(); +// }); +// }; - _stack: CollectionStackingView | CollectionNoteTakingView | null | undefined; - childLayoutFitWidth = (doc: Doc) => doc.type === DocumentType.RTF; - addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, 'data', d), true as boolean); - removeDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, 'data', d), true as boolean); - render() { - return ( - <InfoWindow - // anchor={this.props.markerMap[this.props.place[Id]]} - onCloseClick={this.handleInfoWindowClose}> - <div className="mapbox-infowindow"> - <div style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}> - <CollectionStackingView - ref={r => (this._stack = r)} - {...this.props} - setContentView={emptyFunction} - Document={this.props.place} - DataDoc={undefined} - fieldKey="data" - NativeWidth={returnZero} - NativeHeight={returnZero} - childFilters={returnEmptyFilter} - setHeight={emptyFunction} - isAnnotationOverlay={false} - select={emptyFunction} - NativeDimScaling={returnOne} - isContentActive={returnTrue} - chromeHidden={true} - rootSelected={returnFalse} - childHideResizeHandles={returnTrue} - childHideDecorationTitle={returnTrue} - childLayoutFitWidth={this.childLayoutFitWidth} - // childDocumentsActive={returnFalse} - removeDocument={this.removeDoc} - addDocument={this.addDoc} - renderDepth={this.props.renderDepth + 1} - type_collection={CollectionViewType.Stacking} - pointerEvents={returnAll} - /> - </div> - <hr /> - <div - onPointerDown={this.addNoteClick} - onClick={e => { - e.stopPropagation(); - e.preventDefault(); - }}> - Add Note - </div> - </div> - </InfoWindow> - ); - } -} +// _stack: CollectionStackingView | CollectionNoteTakingView | null | undefined; +// childLayoutFitWidth = (doc: Doc) => doc.type === DocumentType.RTF; +// addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, 'data', d), true as boolean); +// removeDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, 'data', d), true as boolean); +// render() { +// return ( +// <InfoWindow +// // anchor={this.props.markerMap[this.props.place[Id]]} +// onCloseClick={this.handleInfoWindowClose}> +// <div className="mapbox-infowindow"> +// <div style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}> +// <CollectionStackingView +// ref={r => (this._stack = r)} +// {...this.props} +// setContentView={emptyFunction} +// Document={this.props.place} +// TemplateDataDocument={undefined} +// fieldKey="data" +// NativeWidth={returnZero} +// NativeHeight={returnZero} +// childFilters={returnEmptyFilter} +// setHeight={emptyFunction} +// isAnnotationOverlay={false} +// select={emptyFunction} +// NativeDimScaling={returnOne} +// isContentActive={returnTrue} +// chromeHidden={true} +// childHideResizeHandles={true} +// childHideDecorationTitle={true} +// childLayoutFitWidth={this.childLayoutFitWidth} +// // childDocumentsActive={returnFalse} +// removeDocument={this.removeDoc} +// addDocument={this.addDoc} +// renderDepth={this.props.renderDepth + 1} +// type_collection={CollectionViewType.Stacking} +// pointerEvents={returnAll} +// /> +// </div> +// <hr /> +// <div +// onPointerDown={this.addNoteClick} +// onClick={e => { +// e.stopPropagation(); +// e.preventDefault(); +// }}> +// Add Note +// </div> +// </div> +// </InfoWindow> +// ); +// } +// } diff --git a/src/client/views/nodes/MapBox/MapPushpinBox.tsx b/src/client/views/nodes/MapBox/MapPushpinBox.tsx index 42bada0ef..fc5b4dd18 100644 --- a/src/client/views/nodes/MapBox/MapPushpinBox.tsx +++ b/src/client/views/nodes/MapBox/MapPushpinBox.tsx @@ -1,31 +1,27 @@ -import { observer } from 'mobx-react'; -// import { SettingsManager } from '../../../util/SettingsManager'; +import * as React from 'react'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { FieldView, FieldViewProps } from '../FieldView'; -import React = require('react'); -import { computed } from 'mobx'; import { MapBox } from './MapBox'; /** * Map Pushpin doc class */ -@observer export class MapPushpinBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapPushpinBox, fieldKey); } componentDidMount() { - this.mapBoxView.addPushpin(this.rootDoc); + this.mapBoxView.addPushpin(this.Document); } componentWillUnmount() { - this.mapBoxView.deletePushpin(this.rootDoc); + this.mapBoxView.deletePushpin(this.Document); } - @computed get mapBoxView() { - return this.props.DocumentView?.()?.props.docViewPath().lastElement()?.ComponentView as MapBox; + get mapBoxView() { + return this.DocumentView?.()?.containerViewPath?.().lastElement()?.ComponentView as MapBox; } - @computed get mapBox() { - return this.props.DocumentView?.().props.docViewPath().lastElement()?.rootDoc; + get mapBox() { + return this.DocumentView?.().containerViewPath?.().lastElement()?.Document; } render() { diff --git a/src/client/views/nodes/MapBox/MapboxApiUtility.ts b/src/client/views/nodes/MapBox/MapboxApiUtility.ts new file mode 100644 index 000000000..592330ac2 --- /dev/null +++ b/src/client/views/nodes/MapBox/MapboxApiUtility.ts @@ -0,0 +1,139 @@ + +const MAPBOX_FORWARD_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/'; +const MAPBOX_REVERSE_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/'; +const MAPBOX_DIRECTIONS_BASE_URL = 'https://api.mapbox.com/directions/v5/mapbox'; +const MAPBOX_ACCESS_TOKEN = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNscHgwNDd1MDA3MXIydm92ODdianp6cGYifQ.WFAqbhwxtMHOWSPtu0l2uQ'; + +export type TransportationType = 'driving' | 'cycling' | 'walking'; + +export class MapboxApiUtility { + + static forwardGeocodeForFeatures = async (searchText: string) => { + try { + const url = MAPBOX_FORWARD_GEOCODE_BASE_URL + encodeURI(searchText) +'.json' +`?access_token=${MAPBOX_ACCESS_TOKEN}`; + const response = await fetch(url); + const data = await response.json(); + return data.features; + } catch (error: any){ + // TODO: handle error in better way + return null; + } + } + + static reverseGeocodeForFeatures = async (longitude: number, latitude: number) => { + try { + const url = MAPBOX_REVERSE_GEOCODE_BASE_URL + encodeURI(longitude.toString() + "," + latitude.toString()) + '.json' + + `?access_token=${MAPBOX_ACCESS_TOKEN}`; + const response = await fetch(url); + const data = await response.json(); + return data.features; + } catch (error: any){ + return null; + } + } + + static getDirections = async (origin: number[], destination: number[]): Promise<Record<TransportationType, any> | undefined> => { + try { + + const directionsPromises: Promise<any>[] = []; + const transportationTypes: TransportationType[] = ['driving', 'cycling', 'walking']; + + transportationTypes.forEach((type) => { + directionsPromises.push( + fetch( + `${MAPBOX_DIRECTIONS_BASE_URL}/${type}/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}` + ).then((response) => response.json()) + ); + }); + + const results = await Promise.all(directionsPromises); + + const routeInfoMap: Record<TransportationType, any> = { + 'driving': {}, + 'cycling': {}, + 'walking': {}, + }; + + transportationTypes.forEach((type, index) => { + const routeData = results[index].routes[0]; + if (routeData) { + const geometry = routeData.geometry; + const coordinates = geometry.coordinates; + + routeInfoMap[type] = { + duration: this.secondsToMinutesHours(routeData.duration), + distance: this.metersToMiles(routeData.distance), + coordinates: coordinates, + }; + } + }); + + return routeInfoMap; + + // return current route info, and the temporary route + + } catch (error: any){ + return undefined; + console.log("Error: ", error); + } + } + + private static secondsToMinutesHours = (seconds: number) => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60).toFixed(2); + + if (hours === 0){ + return `${minutes} min` + } else { + return `${hours} hr ${minutes} min` + } + } + + private static metersToMiles = (meters: number) => { + return `${parseFloat((meters/1609.34).toFixed(2))} mi`; + } + +} + +// const drivingQuery = await fetch( +// `${MAPBOX_DIRECTIONS_BASE_URL}/driving/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`); + +// const cyclingQuery = await fetch( +// `${MAPBOX_DIRECTIONS_BASE_URL}/cycling/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`); + +// const walkingQuery = await fetch( +// `${MAPBOX_DIRECTIONS_BASE_URL}/walking/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`); + +// const drivingJson = await drivingQuery.json(); +// const cyclingJson = await cyclingQuery.json(); +// const walkingJson = await walkingQuery.json(); + +// console.log("Driving: ", drivingJson); +// console.log("Cycling: ", cyclingJson); +// console.log("Waling: ", walkingJson); + +// const routeMap = { +// 'driving': drivingJson.routes[0], +// 'cycling': cyclingJson.routes[0], +// 'walking': walkingJson.routes[0] +// } + +// const routeInfoMap: Record<TransportationType, any> = { +// 'driving': {}, +// 'cycling': {}, +// 'walking': {}, +// }; + +// Object.entries(routeMap).forEach(([key, routeData]) => { +// const transportationTypeKey = key as TransportationType; +// const geometry = routeData.geometry; +// const coordinates = geometry.coordinates; + +// console.log(coordinates); + +// routeInfoMap[transportationTypeKey] = { +// duration: this.secondsToMinutesHours(routeData.duration), +// distance: this.metersToMiles(routeData.distance), +// coordinates: coordinates +// } +// })
\ No newline at end of file diff --git a/src/client/views/nodes/MapBox/MarkerIcons.tsx b/src/client/views/nodes/MapBox/MarkerIcons.tsx new file mode 100644 index 000000000..a580fcaa0 --- /dev/null +++ b/src/client/views/nodes/MapBox/MarkerIcons.tsx @@ -0,0 +1,103 @@ +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { faShopify } from '@fortawesome/free-brands-svg-icons'; +import { + faBasketball, + faBicycle, + faBowlFood, + faBus, + faCameraRetro, + faCar, + faCartShopping, + faFilm, + faFootball, + faFutbol, + faHockeyPuck, + faHospital, + faHotel, + faHouse, + faLandmark, + faLocationDot, + faLocationPin, + faMapPin, + faMasksTheater, + faMugSaucer, + faPersonHiking, + faPlane, + faSchool, + faShirt, + faShop, + faSquareParking, + faStar, + faTrainSubway, + faTree, + faUtensils, + faVolleyball, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import * as React from 'react'; + +export class MarkerIcons { + // static getMapboxIcon = (color: string) => { + // return ( + // <svg xmlns="http://www.w3.org/2000/svg" id="marker" data-name="marker" width="20" height="48" viewBox="0 0 20 35"> + // <g id="mapbox-marker-icon"> + // <g id="icon"> + // <ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" /> + // <g id="mask" opacity="0.3"> + // <g id="group"> + // <path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fillRule="evenodd"/> + // </g> + // </g> + // <path id="color" fill={color} strokeWidth="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/> + // <path id="circle" fill="#fff" stroke='white' strokeWidth="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/> + // </g> + // </g> + // <rect width="20" height="48" fill="none"/> + // </svg> + // ) + // } + + static getFontAwesomeIcon(key: string, size: string, color?: string): JSX.Element { + const icon: IconProp = MarkerIcons.FAMarkerIconsMap[key]; + const iconProps: any = { icon }; + + if (color) { + iconProps.color = color; + } + + return <FontAwesomeIcon {...iconProps} size={size} />; + } + + static FAMarkerIconsMap: { [key: string]: IconProp } = { + MAP_PIN: faLocationDot, + RESTAURANT_ICON: faUtensils, + HOTEL_ICON: faHotel, + HOUSE_ICON: faHouse, + AIRPLANE_ICON: faPlane, + CAR_ICON: faCar, + BUS_ICON: faBus, + TRAIN_ICON: faTrainSubway, + BICYCLE_ICON: faBicycle, + PARKING_ICON: faSquareParking, + PHOTO_ICON: faCameraRetro, + CAFE_ICON: faMugSaucer, + STAR_ICON: faStar, + SHOPPING_CART_ICON: faCartShopping, + SHOPIFY_ICON: faShopify, + SHOP_ICON: faShop, + SHIRT_ICON: faShirt, + FOOD_ICON: faBowlFood, + LANDMARK_ICON: faLandmark, + HOSPITAL_ICON: faHospital, + NATURE_ICON: faTree, + HIKING_ICON: faPersonHiking, + SOCCER_ICON: faFutbol, + VOLLEYBALL_ICON: faVolleyball, + BASKETBALL_ICON: faBasketball, + HOCKEY_ICON: faHockeyPuck, + FOOTBALL_ICON: faFootball, + SCHOOL_ICON: faSchool, + THEATER_ICON: faMasksTheater, + FILM_ICON: faFilm, + }; +} diff --git a/src/client/views/nodes/MapBox/icon_images/mapbox-marker-icon-20px-blue.png b/src/client/views/nodes/MapBox/icon_images/mapbox-marker-icon-20px-blue.png Binary files differnew file mode 100644 index 000000000..8b686e2aa --- /dev/null +++ b/src/client/views/nodes/MapBox/icon_images/mapbox-marker-icon-20px-blue.png |
