Skip to content

Commit

Permalink
Split left map controls to its own component file
Browse files Browse the repository at this point in the history
For easier adding of FileInput
  • Loading branch information
jo-chemla committed Jul 26, 2024
1 parent 9971d98 commit d7a2840
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 19 deletions.
24 changes: 5 additions & 19 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { useState, useRef, useEffect, useCallback, useMemo } from "react";

import "./App.css";
import Map, { type MapRef, Source, Layer, ScaleControl, useControl } from "react-map-gl";
import GeocoderControl from "./geocoder-control";
import Map, { type MapRef, Source, Layer, ScaleControl } from "react-map-gl";
import ControlPanelDrawer, { type MapSplitMode } from "./control-panel";
import { set, subMonths } from "date-fns";
import Split from "react-split";
import { ToggleButton, ToggleButtonGroup } from "@mui/material";
import RulerControl from '@mapbox-controls/ruler';
import '@mapbox-controls/ruler/src/index.css';

import MapControls, {WrappedRulerControl} from './map-controls'

import {
planetBasemapUrl,
Expand Down Expand Up @@ -134,13 +132,6 @@ function App() {
// rightMapRef.current.resize();
}

function WrappedRulerControl(props) {
useControl(() => new RulerControl(props), {
position: props.position
});
return null;
}

// Update raster TMS source faster than react component remount on timelineDate state update
// useEffect(() => {
// leftMapRef.current
Expand Down Expand Up @@ -415,15 +406,11 @@ function App() {

// projection={"naturalEarth"} // globe mercator naturalEarth equalEarth // TODO: eventually make projection controllable
>
<GeocoderControl
mapboxAccessToken={MAPBOX_TOKEN}
position="top-left"
flyTo={false}
<MapControls
mapRef={leftMapRef}
mapboxAccessToken={MAPBOX_TOKEN}
/>
<WrappedRulerControl
position="top-left"
/>


{leftSelectedTms == BasemapsIds.Mapbox ? (
<></>
Expand Down Expand Up @@ -483,7 +470,6 @@ function App() {
</Source>
} */}
{/* beforeId={"GROUP_"} */}
<ScaleControl maxWidth={60} unit="metric" position={'top-left'}/>
</Map>
<div
style={RightMapStyle}
Expand Down
69 changes: 69 additions & 0 deletions src/custom-overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Forked from CustomOverlay https://github.com/visgl/react-map-gl/tree/7.0-release/examples/custom-overlay
// Minimal example here: https://visgl.github.io/react-map-gl/docs/get-started/adding-custom-data

import * as React from 'react'
import { useState, cloneElement } from 'react'
import { useControl } from 'react-map-gl'
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'

import type { MapboxMap, IControl } from 'react-map-gl'

class OverlayControl implements IControl {
_map: MapboxMap | null = null
_container: HTMLElement
_redraw: () => void

constructor(redraw: () => void) {
this._redraw = redraw
}

onAdd(map): HTMLElement {
this._map = map
map.on('move', this._redraw)
/* global document */
this._container = document.createElement('div')
this._redraw()
return this._container
}

onRemove(): void {
this._container.remove()
this._map?.off('move', this._redraw)
this._map = null
}

getMap(): MapboxMap | null {
return this._map
}

getElement(): HTMLElement {
return this._container
}
}

/**
* A custom control that rerenders arbitrary React content whenever the camera changes
*/
CustomOverlay.propTypes = {
position: PropTypes.string,
// children: React.ReactElement,
}
function CustomOverlay(props): React.ReactElement {
const [, setVersion] = useState(0)

const ctrl = useControl<OverlayControl>(
() => {
const forceUpdate = (): void => {
setVersion((v) => v + 1)
}
return new OverlayControl(forceUpdate)
},
{ position: props.position }
)

const map = ctrl.getMap()
return map && createPortal(cloneElement(props.children, { map }), ctrl.getElement())
}

export default React.memo(CustomOverlay)
154 changes: 154 additions & 0 deletions src/map-controls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import {ReactElement, memo, useCallback} from 'react'
import CustomOverlay from './custom-overlay';
import { ScaleControl, NavigationControl, useControl } from "react-map-gl";
import GeocoderControl from './geocoder-control'
import RulerControl from '@mapbox-controls/ruler';

import bbox from '@turf/bbox'
import { kml } from '@tmcw/togeojson'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUpload } from '@fortawesome/free-solid-svg-icons'


export function WrappedRulerControl(props: any) {
useControl(() => new RulerControl(props), {
position: props.position
});
return null;
}

function FileInput(props: any): ReactElement {
function handleKMLUpload(event): void {
event.preventDefault()
const kmlFile = event.target.files[0]
console.log('kmlFile info', kmlFile)
console.log('props in handleKMLUpload', props)
const reader = new FileReader()
reader.onload = async (e) => {
const kmlContent: string = e.target?.result as string
const xmlDoc = new DOMParser().parseFromString(kmlContent, 'text/xml')
const geojsonFeatures = kml(xmlDoc)
console.log('geojsonFeatures from kml', geojsonFeatures)

// Zoom on imported kml
const bounds = bbox(geojsonFeatures)
const [minLng, minLat, maxLng, maxLat] = bounds
console.log('mapRef', props)
props.mapRef.current.fitBounds(
[
[minLng, minLat],
[maxLng, maxLat],
],
{
padding: 150,
duration: 1000,
center: [0.5 * (minLng + maxLng), 0.5 * (minLat + maxLat)],
}
)

// Can be imported but not edited because not added to the draw features component
console.log('props in onload', props)
props.setDrawFeatures([{ ...geojsonFeatures.features[0], id: 'imported_kml_feature' }])
// Not useful
// props.map.fire("draw.create", {
// features: [{...geojsonFeatures.features[0], id: 'imported_kml_feature'}]
// });
}
reader.readAsText(event.target.files[0], 'UTF-8')
}

return (
<div className="mapboxgl-ctrl mapboxgl-ctrl-group">
<input
id="kmlUploadInput"
type="file"
onChange={(e) => {
handleKMLUpload(e)
}}
style={{ pointerEvents: 'auto', display: 'none' }}
/>
<button
type="button"
title="Upload KML AOI (will just zoom to it, not load it)"
onClick={() => {
document.getElementById('kmlUploadInput')?.click()
}}
>
<span className="mapboxgl-ctrl-icon" style={{ padding: '7px' }}>
<FontAwesomeIcon icon={faUpload} />{' '}
</span>
</button>
</div>
)
}


function FileUploadControl(props: any): ReactElement {
const onDrawCreate = useCallback((e) => {
props.setDrawFeatures((currFeatures) => {
const newFeatures = { ...currFeatures }
for (const f of e.features) {
newFeatures[f.id] = f
}
return newFeatures
})
}, [])
const onDrawUpdate = useCallback((e) => {
props.setDrawFeatures((currFeatures) => {
const newFeatures = { ...currFeatures }
for (const f of e.features) {
newFeatures[f.id] = f
}
return newFeatures
})
}, [])

const onDrawDelete = useCallback((e) => {
props.setDrawFeatures((currFeatures) => {
const newFeatures = { ...currFeatures }
for (const f of e.features) {
delete newFeatures[f.id]
}
return newFeatures
})
}, [])

return (

<CustomOverlay position="top-left" style={{ pointerEvents: 'all' }}>
<FileInput setDrawFeatures={props.setDrawFeatures} mapRef={props.mapRef} />
</CustomOverlay>
)
}

function MapControls(props: any): ReactElement {


return (
<>
<GeocoderControl
mapboxAccessToken={props.mapboxAccessToken}
position="top-left"
flyTo={false}
mapRef={props.mapRef}
/>
<WrappedRulerControl
position="top-left"
/>

{/* <FileUploadControl
/> */}
<NavigationControl showCompass={false} position="top-left" />

<ScaleControl
unit={'metric'}
position="top-left"
// style={{ clear: 'none' }}
maxWidth={60}
/>
</>
)
}

export default memo(MapControls)

0 comments on commit d7a2840

Please sign in to comment.