diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/views/nodes/MapBox/AnimationSpeedIcons.tsx | 35 | ||||
-rw-r--r-- | src/client/views/nodes/MapBox/AnimationUtility.ts | 429 | ||||
-rw-r--r-- | src/client/views/nodes/MapBox/MapAnchorMenu.tsx | 118 | ||||
-rw-r--r-- | src/client/views/nodes/MapBox/MapBox.scss | 50 | ||||
-rw-r--r-- | src/client/views/nodes/MapBox/MapBox.tsx | 776 | ||||
-rw-r--r-- | src/client/views/nodes/MapBox/MarkerIcons.tsx | 69 |
6 files changed, 1291 insertions, 186 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..256acbf13 --- /dev/null +++ b/src/client/views/nodes/MapBox/AnimationUtility.ts @@ -0,0 +1,429 @@ +import mapboxgl from "mapbox-gl"; +import { MercatorCoordinate } from "mapbox-gl"; +import { MapRef } from "react-map-gl"; +import * as React from 'react'; +import * as d3 from "d3"; +import * as turf from '@turf/turf'; +import { Position } from "@turf/turf"; +import { Feature, FeatureCollection, GeoJsonProperties, Geometry } from "geojson"; +import { observer } from "mobx-react"; +import { action, computed, observable } from "mobx"; + +export enum AnimationStatus { + START = 'start', + RESUME = 'resume', + RESTART = 'restart', +} + +export enum AnimationSpeed { + SLOW = '1x', + MEDIUM = '2x', + FAST = '3x', +} + +@observer +export class AnimationUtility { + private SMOOTH_FACTOR = 0.95 + private ROUTE_COORDINATES: Position[] = []; + private PATH: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties>; + private FLY_IN_START_PITCH = 40; + private FIRST_LNG_LAT: {lng: number, lat: number}; + private START_ALTITUDE = 3_000_000; + + @observable private isStreetViewAnimation: boolean; + @observable private animationSpeed: AnimationSpeed; + + + private previousLngLat: {lng: number, lat: number}; + + private currentStreetViewBearing: number = 0; + + @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 flyInStartBearing() { + return Math.max(0, Math.min(this.flyInEndBearing + 20, 360)); // between 0 and 360 + } + + @computed get flyInEndAltitude() { + return this.isStreetViewAnimation ? 70 : 10000; + } + + @computed get flyInEndPitch() { + return this.isStreetViewAnimation ? 80 : 50; + } + + @computed get flyToDuration() { + switch (this.animationSpeed) { + case AnimationSpeed.SLOW: + return 4000; + case AnimationSpeed.MEDIUM: + return 2500; + case AnimationSpeed.FAST: + return 1250; + default: + return 2500; + } + } + + @computed get animationDuration(): number { + return 20_000; + // compute path distance for a standard speed + // get animation speed + // get isStreetViewAnimation (should be slower if so) + } + + @action + public updateAnimationSpeed(speed: AnimationSpeed) { + // calculate new flyToDuration and animationDuration + this.animationSpeed = speed; + } + + @action + public updateIsStreetViewAnimation(isStreetViewAnimation: boolean) { + this.isStreetViewAnimation = isStreetViewAnimation; + } + + + constructor( + firstLngLat: {lng: number, lat: number}, + routeCoordinates: Position[], + isStreetViewAnimation: boolean, + animationSpeed: AnimationSpeed + ) { + this.FIRST_LNG_LAT = firstLngLat; + this.previousLngLat = firstLngLat; + this.ROUTE_COORDINATES = routeCoordinates; + this.PATH = turf.lineString(routeCoordinates); + + const bearing = this.calculateBearing( + { + lng: routeCoordinates[0][0], + lat: routeCoordinates[0][1] + }, + { + lng: routeCoordinates[1][0], + lat: routeCoordinates[1][1] + } + ); + this.currentStreetViewBearing = bearing; + // if (isStreetViewAnimation){ + // this.flyInEndBearing = bearing; + // } + this.isStreetViewAnimation = isStreetViewAnimation; + this.animationSpeed = animationSpeed; + // calculate animation duration based on speed + // this.animationDuration = animationDuration; + } + + 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) => { + const pathDistance = turf.lineDistance(this.PATH); + console.log("PATH DISTANCE: ", pathDistance); + 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; + } + + + // calculate the distance along the path based on the animationPhase + const alongPath = turf.along(this.PATH, pathDistance * 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.028 // Adjust the transition speed as needed + ); + 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; + } + + 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.flyInEndPitch, + bearing, + lngLat, + this.flyInEndAltitude, + true // smooth + ); + + // set the pitch and bearing of the camera + const camera = map.getFreeCameraOptions(); + camera.setPitchBearing(this.flyInEndPitch, bearing); + + console.log("Corrected pos: ", correctedPosition); + console.log("Start altitude: ", this.flyInEndAltitude); + // set the position and altitude of the camera + camera.position = MercatorCoordinate.fromLngLat( + correctedPosition, + this.flyInEndAltitude + ); + + + // apply the new camera options + map.setFreeCameraOptions(camera); + + // 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.320 * 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; + } + + +}
\ No newline at end of file diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx index 2c2879900..f4e24d9c1 100644 --- a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx @@ -32,11 +32,11 @@ import { Autocomplete, Checkbox, FormControlLabel, TextField } from '@mui/materi import { MapboxApiUtility, TransportationType } from './MapboxApiUtility'; import { MapBox } from './MapBox'; import { List } from '../../../../fields/List'; -import { MapboxColor, MarkerIcons } from './MarkerIcons'; -import { CirclePicker } from 'react-color'; +import { MarkerIcons } from './MarkerIcons'; +import { CirclePicker, ColorState } from 'react-color'; import { Position } from 'geojson'; -type MapAnchorMenuType = 'standard' | 'route' | 'calendar' | 'customize'; +type MapAnchorMenuType = 'standard' | 'routeCreation' | 'calendar' | 'customize' | 'route'; @observer export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -61,17 +61,28 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { public DisplayRoute: (routeInfoMap: Record<TransportationType, any> | undefined, type: TransportationType) => void = unimplementedFunction; public HideRoute: () => void = unimplementedFunction; - public AddNewRouteToMap: (coordinates: Position[], origin: string, destination: string) => 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 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 pinDoc: Doc | undefined = undefined; + + private routeDoc: Doc | undefined = undefined; private title: string | undefined = undefined; @@ -81,6 +92,11 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { this.title = StrCast(pinDoc.title ? pinDoc.title : `${NumCast(pinDoc.longitude)}, ${NumCast(pinDoc.latitude)}`) ; } + public setRouteDoc(routeDoc: Doc){ + this.routeDoc = routeDoc; + this.title = StrCast(routeDoc.title ?? 'Map route') + } + public setAllMapboxPins(pinDocs: Doc[]) { this.allMapPinDocs = pinDocs; pinDocs.forEach((p, idx) => { @@ -148,12 +164,11 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { // return this.top // } - @observable - menuType: MapAnchorMenuType = 'standard'; + @action DirectionsClick = () => { - this.menuType = 'route'; + this.menuType = 'routeCreation'; } @action @@ -175,6 +190,26 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { } } + @action + onMarkerColorChange = (color: ColorState) => { + 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[] = [] @@ -248,7 +283,8 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { if (this.currentRouteInfoMap && this.selectedTransportationType && this.selectedDestinationFeature){ const coordinates = this.currentRouteInfoMap[this.selectedTransportationType].coordinates; console.log(coordinates); - this.AddNewRouteToMap(coordinates, this.title ?? "", this.selectedDestinationFeature.place_name); + console.log(this.selectedDestinationFeature); + this.AddNewRouteToMap(coordinates, this.title ?? "", this.selectedDestinationFeature, this.createPinForDestination); this.HideRoute(); } } @@ -258,11 +294,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { const markerType = StrCast(this.pinDoc.markerType); const markerColor = StrCast(this.pinDoc.markerColor); - if (markerType.startsWith("MAPBOX")){ - return MarkerIcons.getMapboxIcon(markerColor as MapboxColor); - } else { // font awesome icon - return MarkerIcons.getFontAwesomeIcon(markerType, markerColor); - } + return MarkerIcons.getFontAwesomeIcon(markerType, '2x', markerColor); } return undefined; } @@ -313,7 +345,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { /> </> } - {this.menuType === 'route' && + {this.menuType === 'routeCreation' && <> <IconButton tooltip="Go back" // @@ -327,19 +359,39 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { icon={<FontAwesomeIcon icon={faAdd as IconLookup} />} color={SettingsManager.userColor} /> + </> + } + {this.menuType === 'route' && + <> + <IconButton + tooltip="Delete Route" // + onPointerDown={this.Delete} + icon={<FontAwesomeIcon icon="trash-alt" />} + color={SettingsManager.userColor} + /> <IconButton tooltip='Animate route' - onPointerDown={this.Delete} /**TODO: fix */ + onPointerDown={() => this.OpenAnimationPanel(this.routeDoc)} /**TODO: fix */ icon={<FontAwesomeIcon icon={faRoute as IconLookup}/>} 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='Add to calendar' onPointerDown={this.Delete} /**TODO: fix */ icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup}/>} color={SettingsManager.userColor} /> + </> + } {this.menuType === 'customize' && <> @@ -351,7 +403,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { /> <IconButton tooltip="Revert to original" // - onPointerDown={this.BackClick} + onPointerDown={() => this.revertToOriginalMarker()} icon={<FontAwesomeIcon icon={faArrowsRotate as IconLookup} />} color={SettingsManager.userColor} /> @@ -386,7 +438,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { {this.menuType === 'standard' && <div>{this.title}</div> } - {this.menuType === 'route' && + {this.menuType === 'routeCreation' && <div className='direction-inputs' style={{display: 'flex', flexDirection: 'column'}}> <TextField fullWidth @@ -493,28 +545,28 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { circleSize={15} circleSpacing={7} width='100%' - onChange={(color) => console.log(color.hex)} + onChange={(color) => this.onMarkerColorChange(color)} /> </div> <div className='all-markers-container'> - {Object.keys(MarkerIcons.FAMarkerIconsMap).map((iconKey) => { - const icon = MarkerIcons.getFontAwesomeIcon(iconKey); - if (icon){ - return ( - <div key={iconKey} className='marker-icon'> - <IconButton - onPointerDown={() => {}} - icon={MarkerIcons.getFontAwesomeIcon(iconKey, 'white')} - /> - </div> - ) - } - return null; - })} + {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> , true); diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss index bc2f90fbd..d3c6bb14e 100644 --- a/src/client/views/nodes/MapBox/MapBox.scss +++ b/src/client/views/nodes/MapBox/MapBox.scss @@ -82,6 +82,56 @@ } + .animation-panel { + z-index: 900; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + position: absolute; + background-color: rgb(187, 187, 187); + padding: 10px; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + width: 100%; + + #route-to-animate-title { + font-size: 1.25em; + font-weight: bold; + } + + .route-animation-options { + display: flex; + justify-content: flex-start; + align-items: center; + gap: 7px; + + .animation-suboptions{ + display: flex; + justify-content: flex-start; + align-items: center; + gap: 7px; + + label{ + margin-bottom: 0; + } + + .speed-label{ + margin-right: 5px; + } + + #last-divider{ + margin-left: 10px; + margin-right: 10px; + } + } + + + } + + } + + .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 ac926e1fb..cde68a2e6 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -2,7 +2,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import BingMapsReact from 'bingmaps-react'; // import 'mapbox-gl/dist/mapbox-gl.css'; -import { Button, EditableText, IconButton, Type } from 'browndash-components'; +import { Button, EditableText, IconButton, Size, Type } from 'browndash-components'; import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, flow, toJS} from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -51,14 +51,20 @@ import debounce from 'debounce'; import './MapBox.scss'; import { NumberLiteralType } from 'typescript'; // import { GeocoderControl } from './GeocoderControl'; -import mapboxgl, { LngLat, MapLayerMouseEvent } from 'mapbox-gl'; +import mapboxgl, { LngLat, LngLatBoundsLike, MapLayerMouseEvent, MercatorCoordinate } from 'mapbox-gl'; import { Feature, FeatureCollection, GeoJsonProperties, Geometry, LineString, MultiLineString, Position } from 'geojson'; import { MarkerEvent } from 'react-map-gl/dist/esm/types'; import { MapboxApiUtility, TransportationType} from './MapboxApiUtility'; import { Autocomplete, Checkbox, FormControlLabel, TextField } from '@mui/material'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; -import { IconLookup, faGear } from '@fortawesome/free-solid-svg-icons'; +import { IconLookup, faCircleXmark, faFileExport, faGear, faPause, faPlay, faRotate } from '@fortawesome/free-solid-svg-icons'; +import { MarkerIcons } from './MarkerIcons'; +import { SettingsManager } from '../../../util/SettingsManager'; +import * as turf from '@turf/turf'; +import * as d3 from "d3"; +import { AnimationSpeed, AnimationStatus, AnimationUtility } from './AnimationUtility'; +import { fastSpeedIcon, mediumSpeedIcon, slowSpeedIcon } from './AnimationSpeedIcons'; // amongus /** @@ -142,25 +148,93 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @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 + const startIndex = Math.floor(index); + const endIndex = Math.ceil(index); + + if (startIndex === endIndex) { + // AnimationPhase is at a whole number (no interpolation needed) + const coordinates = [originalCoordinates[startIndex]]; + const geometry: LineString = { + type: 'LineString', + coordinates, + }; + return { + 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; + + // Interpolate the coordinates + const interpolatedCoord = [ + startCoord[0] + fraction * (endCoord[0] - startCoord[0]), + startCoord[1] + fraction * (endCoord[1] - startCoord[1]), + ]; + + const coordinates = originalCoordinates.slice(0, startIndex + 1).concat([interpolatedCoord]); + + const geometry: LineString = { + type: 'LineString', + coordinates, + }; + return { + type: 'Feature', + properties: { + 'routeTitle': StrCast(this.routeToAnimate.title) + }, + geometry: geometry, + }; + } + } + 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(route => { - console.log("Route coords: ", route.coordinates); + 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(route.coordinates)) - } + coordinates: JSON.parse(StrCast(routeDoc.routeCoordinates)), + }; return { - type: 'Feature', - properties: {}, - geometry: geometry + type: 'Feature', + properties: { + 'routeTitle': routeDoc.title}, + geometry: geometry, }; - }); - - return { + }); + + return { type: 'FeatureCollection', - features: features - }; + features: features, + }; } + @computed get SidebarShown() { return this.layoutDoc._layout_showSidebar ? true : false; } @@ -184,7 +258,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps _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]?.()); } @@ -199,7 +273,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)); } @@ -300,10 +374,10 @@ 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; }); @@ -329,10 +403,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' @@ -410,11 +484,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; // The pin that is selected - @observable selectedPin: Doc | undefined; + @observable selectedPinOrRoute: Doc | undefined; + @action - deselectPin = () => { - if (this.selectedPin) { + deselectPinOrRoute = () => { + if (this.selectedPinOrRoute) { // // Removes filter // Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'remove'); // Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'remove'); @@ -435,6 +510,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } }; + getView = async (doc: Doc) => { if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(); return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); @@ -444,22 +520,22 @@ 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.rootDoc, 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 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.props.ScreenToLocalTransform().inverse().transformPoint(x, y); @@ -474,7 +550,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action mapOnClick = (e: { location: { latitude: any; longitude: any } }) => { this.props.select(false); - this.deselectPin(); + this.deselectPinOrRoute(); }; /* * Updates values of layout doc to match the current map @@ -520,14 +596,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps /// 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), + text: StrCast(this.selectedPinOrRoute?.map) || StrCast(this.rootDoc.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: StrCast((existingPin ?? this.selectedPinOrRoute)?.map) || StrCast(this.dataDoc.map), layout_unrendered: true, - mapPin: existingPin ?? this.selectedPin, + mapPin: existingPin ?? this.selectedPinOrRoute, annotationOn: this.rootDoc, }); if (anchor) { @@ -566,7 +642,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 @@ -576,23 +652,25 @@ 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(() => { + 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.rootDoc, 'latitude', this.selectedPinOrRoute.latitude, 'remove'); + Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPinOrRoute.longitude, 'remove'); + Doc.setDocFilter(this.rootDoc, 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); }, 'delete pin'); + + tryHideMapAnchorMenu = (e: PointerEvent) => { let target = document.elementFromPoint(e.x, e.y); while (target) { @@ -608,9 +686,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action centerOnSelectedPin = () => { - if (this.selectedPin) { + if (this.selectedPinOrRoute) { this._mapRef.current?.flyTo({ - center: [NumCast(this.selectedPin.longitude), NumCast(this.selectedPin.latitude)] + center: [NumCast(this.selectedPinOrRoute.longitude), NumCast(this.selectedPinOrRoute.latitude)] }) } // if (this.selectedPin) { @@ -776,12 +854,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps false, [], { - title: location ?? `lat=${latitude},lng=${longitude}`, + title: location ?? `lat=${NumCast(latitude)},lng=${NumCast(longitude)}`, map: location, description: "", wikiData: wikiData, - markerType: 'MAPBOX_MARKER', - markerColor: '#3FB1CE' + markerType: 'MAP_PIN', + markerColor: '#ff5722' }, // { title: map ?? `lat=${latitude},lng=${longitude}`, map: map }, // ,'pushpinIDamongus'+ this.incrementer++ @@ -794,14 +872,16 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }, 'createpin'); @action - createMapRoute = undoable((coordinates: Position[], origin: string, destination: string) => { - console.log(coordinates); + createMapRoute = undoable((coordinates: Position[], origin: string, destination: any, createPinForDestination: boolean) => { const mapRoute = Docs.Create.MapRouteDocument( false, [], - {title: `${origin} -> ${destination}`, routeCoordinates: JSON.stringify(coordinates)}, + {title: `${origin} --> ${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); + } return mapRoute; // mapMarker.infoWindowOpen = true; @@ -853,10 +933,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps */ handleSearchChange = async (searchText: string) => { const features = await MapboxApiUtility.forwardGeocodeForFeatures(searchText); - if (features){ + if (features && !this.isAnimating){ runInAction(() => { this.settingsOpen= false; this.featuresFromGeocodeResults = features; + this.routeToAnimate = undefined; }) } // try { @@ -874,12 +955,65 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // @action // debouncedCall = React.useCallback(debounce(this.debouncedOnSearchBarChange, 300), []); + + @action + handleMapClick = (e: MapLayerMouseEvent) => { + 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.rootDoc, 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.setRouteDoc(routeDoc); + + // TODO: Subject to change + MapAnchorMenu.Instance.setAllMapboxPins( + this.allAnnotations.filter(anno => !anno.layout_unrendered) + ) + + MapAnchorMenu.Instance.DisplayRoute = this.displayRoute; + MapAnchorMenu.Instance.HideRoute = this.hideRoute; + 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 */ - handleMapClick = async (e: MapLayerMouseEvent) => { + handleMapDblClick = async (e: MapLayerMouseEvent) => { e.preventDefault(); const lngLat: LngLat = e.lngLat; const longitude: number = lngLat.lng; @@ -914,18 +1048,18 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action handleMarkerClick = (e: MarkerEvent<mapboxgl.Marker, MouseEvent>, pinDoc: Doc) => { this.featuresFromGeocodeResults = []; - this.deselectPin(); // TODO: check this method - this.selectedPin = pinDoc; + this.deselectPinOrRoute(); // TODO: check this method + 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.rootDoc, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check'); - this.recolorPin(this.selectedPin, 'green'); // TODO: check this method + this.recolorPin(this.selectedPinOrRoute, 'green'); // TODO: check this method - 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; @@ -941,11 +1075,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps MapAnchorMenu.Instance.AddNewRouteToMap = this.createMapRoute; MapAnchorMenu.Instance.CreatePin = this.addMarkerForFeature; - // const longitude = NumCast(pinDoc.longitude); - // const latitude = NumCast(pinDoc.longitude); - // const x = longitude + (this.props.PanelWidth() - this.sidebarWidth()) / 2; - // const y = latitude + this.props.PanelHeight() / 2 + 20; - // const cpt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y); + MapAnchorMenu.Instance.setMenuType('standard'); + MapAnchorMenu.Instance.jumpTo(e.originalEvent.clientX, e.originalEvent.clientY, true); document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true); @@ -979,6 +1110,351 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } } + @observable + isAnimating: boolean = false; + + @observable + isPaused: 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; + } + + @action + openAnimationPanel = (routeDoc: Doc | undefined) => { + if (routeDoc){ + MapAnchorMenu.Instance.fadeOut(true); + document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); + this.routeToAnimate = routeDoc; + } + } + + @observable + animationDuration = 40000; + + @observable + animationAltitude = 12800; + + @observable + pathDistance = 0; + + @observable + isStreetViewAnimation: boolean = false; + + @observable + animationSpeed: AnimationSpeed = AnimationSpeed.MEDIUM; + + @action + updateAnimationSpeed = () => { + switch (this.animationSpeed){ + case AnimationSpeed.SLOW: + this.animationSpeed = AnimationSpeed.MEDIUM; + break; + case AnimationSpeed.MEDIUM: + this.animationSpeed = AnimationSpeed.FAST; + break; + case AnimationSpeed.FAST: + this.animationSpeed = AnimationSpeed.SLOW; + break; + default: + this.animationSpeed = AnimationSpeed.MEDIUM; + break; + } + } + @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 = () => { + this.isStreetViewAnimation = !this.isStreetViewAnimation; + } + + @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; + } + + if (this.isAnimating){ + 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.pathDistance = turf.lineDistance(path); + this.isAnimating = true; + runInAction(() => { + return new Promise<void>(async (resolve) => { + let animationUtil; + try { + const targetLngLat = { + lng: this.selectedRouteCoordinates[0][0], + lat: this.selectedRouteCoordinates[0][1], + }; + + animationUtil = new AnimationUtility( + targetLngLat, + this.selectedRouteCoordinates, + this.isStreetViewAnimation, + this.animationSpeed + ); + + + 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(() => { + resolve(); + }, 10000); + } catch (error: any){ + console.log(error); + console.log('animation util: ', animationUtil); + }}); + + }) + + + } + + + @action + pauseAnimation = () => { + if (this.frameId && this.animationPhase > 0){ + window.cancelAnimationFrame(this.frameId); + this.frameId = null; + this.isAnimating = false; + } + } + + @action + stopAndCloseAnimation = () => { + if (this.frameId){ + window.cancelAnimationFrame(this.frameId); + this.frameId = null; + this.finishedFlyTo = false; + this.isAnimating = false; + this.animationPhase = 0; + this.routeToAnimate = undefined; + // this.selectedRouteCoordinates = []; + } + // reset bearing and pitch to original, zoom out + } + + @action + exportAnimationToVideo = () => { + + } + + 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.playAnimation(AnimationStatus.RESTART)} + icon={<FontAwesomeIcon icon={faRotate as IconLookup}/>} + color='black' + size={Size.MEDIUM} + /> + + } + <IconButton + tooltip='Stop and close animation' + onPointerDown={this.stopAndCloseAnimation} + icon={<FontAwesomeIcon icon={faCircleXmark as IconLookup}/>} + color='black' + size={Size.MEDIUM} + /> + <IconButton + style={{marginRight: '10px'}} + tooltip='Export to video' + onPointerDown={this.exportAnimationToVideo} + icon={<FontAwesomeIcon icon={faFileExport as IconLookup}/>} + color='black' + size={Size.MEDIUM} + /> + {!this.isAnimating && + <> + <div className='animation-suboptions'> + <div>|</div> + <FormControlLabel + label='Street view animation' + labelPlacement='start' + control={ + <Checkbox + color='success' + checked={this.isStreetViewAnimation} + onChange={this.toggleIsStreetViewAnimation} + /> + } + /> + <div id='last-divider'>|</div> + <IconButton + tooltip={this.animationSpeedTooltipText} + onPointerDown={this.updateAnimationSpeed} + icon={this.animationSpeedIcon} + size={Size.MEDIUM} + /> + </div> + </> + } + </> + ) + } + @action hideRoute = () => { this.temporaryRouteSource = { @@ -1015,8 +1491,10 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action toggleSettings = () => { - this.featuresFromGeocodeResults = []; - this.settingsOpen = !this.settingsOpen; + if (!this.isAnimating && this.animationPhase == 0) { + this.featuresFromGeocodeResults = []; + this.settingsOpen = !this.settingsOpen; + } } @action @@ -1056,6 +1534,16 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } } + getMarkerIcon = (pinDoc: Doc): JSX.Element | null => { + const markerType = StrCast(pinDoc.markerType); + const markerColor = StrCast(pinDoc.markerColor); + + return MarkerIcons.getFontAwesomeIcon(markerType, '2x', markerColor) ?? null; + + } + + + @@ -1092,21 +1580,22 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> {renderAnnotations(this.opaqueFilter)} {SnappingManager.GetIsDragging() ? null : renderAnnotations()} + {!this.routeToAnimate && + <div className="mapBox-searchbar"> + <TextField + fullWidth + placeholder='Enter a location' + onChange={(e) => this.handleSearchChange(e.target.value)} + /> + <IconButton + icon={<FontAwesomeIcon icon={faGear as IconLookup} size='1x'/>} + type={Type.TERT} + onClick={(e) => this.toggleSettings()} - <div className="mapBox-searchbar"> - <TextField - fullWidth - placeholder='Enter a location' - onChange={(e) => this.handleSearchChange(e.target.value)} - /> - <IconButton - icon={<FontAwesomeIcon icon={faGear as IconLookup} size='1x'/>} - type={Type.TERT} - onClick={(e) => this.toggleSettings()} - - /> - </div> - {this.settingsOpen && + /> + </div> + } + {this.settingsOpen && !this.routeToAnimate && <div className='mapbox-settings-panel' style={{right: `${0+ this.sidebarWidth()}px`}}> <div className='mapbox-style-select'> <div> @@ -1151,45 +1640,52 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps </div> </div> } - - <div className='mapbox-geocoding-search-results'> - {this.featuresFromGeocodeResults.length > 0 && ( + {this.routeToAnimate && + <div className='animation-panel'> + <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} + <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> - </div> ))} </React.Fragment> - )} - </div> + + </div> + )} <MapProvider> <MapboxMap ref={this._mapRef} - initialViewState={{ - longitude: -100, - latitude: 40, - zoom: 3.5 - }} mapboxAccessToken={MAPBOX_ACCESS_TOKEN} id="mapbox-map" mapStyle={this.mapStyle} style={{height: '100%', width: '100%'}} - {...this.mapboxMapViewState} + initialViewState={this.isAnimating ? undefined : this.mapboxMapViewState} + // {...this.mapboxMapViewState} onMove={this.onMapMove} - onDblClick={this.handleMapClick} + onClick={this.handleMapClick} + onDblClick={this.handleMapDblClick} terrain={this.showTerrain ? { source: 'mapbox-dem', exaggeration: 2.0 } : undefined} @@ -1210,6 +1706,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps 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' @@ -1217,9 +1714,60 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps 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': 'yellow', + 'line-width': 4, + }} + /> + </> + } + <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.allPushpins + {!this.isAnimating && this.animationPhase == 0 && this.allPushpins // .filter(anno => !anno.layout_unrendered) .map((pushpin, idx) => ( <Marker @@ -1228,7 +1776,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps latitude={NumCast(pushpin.latitude)} anchor='bottom' onClick={(e: MarkerEvent<mapboxgl.Marker, MouseEvent>) => this.handleMarkerClick(e, pushpin)} - /> + > + {this.getMarkerIcon(pushpin)} + </Marker> ))} </> diff --git a/src/client/views/nodes/MapBox/MarkerIcons.tsx b/src/client/views/nodes/MapBox/MarkerIcons.tsx index cf50109ac..146f296c1 100644 --- a/src/client/views/nodes/MapBox/MarkerIcons.tsx +++ b/src/client/views/nodes/MapBox/MarkerIcons.tsx @@ -1,57 +1,46 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { faShopify } from '@fortawesome/free-brands-svg-icons'; -import { IconLookup, faBasketball, faBicycle, faBowlFood, faBus, faCameraRetro, faCar, faCartShopping, faFilm, faFootball, faFutbol, faHockeyPuck, faHospital, faHotel, faHouse, faLandmark, faMasksTheater, faMugSaucer, faPersonHiking, faPlane, faSchool, faShirt, faShop, faSquareParking, faStar, faTrainSubway, faTree, faUtensils, faVolleyball } from '@fortawesome/free-solid-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 React = require('react'); -export type MapboxColor = 'yellow' | 'red' | 'orange' | 'purple' | 'pink' | 'blue' | 'green'; -type ColorProperties = { - fill: string, - stroke: string -} -type ColorsMap = { - [key in MapboxColor]: ColorProperties; -} - 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 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, color?: string): JSX.Element | undefined { + static getFontAwesomeIcon(key: string, size: string, color?: string): JSX.Element { const icon: IconProp = MarkerIcons.FAMarkerIconsMap[key]; + const iconProps: any = { icon }; - if (icon) { - const iconProps: any = { icon }; - - if (color) { - iconProps.color = color; - } - - return (<FontAwesomeIcon {...iconProps} size='1x' />); - } + if (color) { + iconProps.color = color; + } + + return (<FontAwesomeIcon {...iconProps} size={size} />); + - return undefined; } static FAMarkerIconsMap: {[key: string]: IconProp} = { + 'MAP_PIN': faLocationDot, 'RESTAURANT_ICON': faUtensils, 'HOTEL_ICON': faHotel, 'HOUSE_ICON': faHouse, |