import React from 'react'
import PropTypes from 'prop-types'
import en from 'locales/en'
import { ErrorBoundary } from 'ErrorBoundary'
// context is only created when <MultiMapManager> is implemented (see constructor)
export let MultiMapContext = null
/**
* A higher order component that manages MultiMapContext for connectToContext wrapped children
* @component
* @category MultiMap
* @since 1.7.0
*/
class MultiMapManager extends React.Component {
constructor (props) {
super(props)
// state becomes an object of persistedStateKeys (or component names) with their persisted states'
this.state = {
initialized: false,
maps: [],
syncedState: [],
visibleState: [],
visibleMapCount: 0
}
this.syncedView = null
this.promisesResolvers = []
// create context when <MultiMapManager> is included in component tree
MultiMapContext = React.createContext()
}
componentDidMount () {
window.addEventListener('resize', this.refreshMaps)
}
refreshMaps = () => {
this.forceUpdate()
// this refresh needs to fire after the component updates the map views
setTimeout(() => this.state.maps.map(map => map.updateSize()), 0)
}
syncableMapListener = (map, e) => {
const { synced, type } = e
if (type === 'synced' && !synced) {
// reset view of newly desynced map
const view = this.state[map.getTargetElement().id].view
map.setView(view)
} else {
this.setSyncedView(map)
}
this.refreshMaps()
}
setSyncedView = map => {
if (!this.syncedView) {
// this is the first map, set the view in state
this.syncedView = map.getView()
} else {
map.setView(this.syncedView)
}
}
addToContext = (config, addToContextProp = () => {}) => {
const { map } = config
const mapId = map.getTargetElement().id
const synced = map.getSyncedState()
const visible = map.getVisibleState()
const mapConfig = {
...config,
synced,
visible,
view: map.getView()
}
const newState = { ...this.state, [mapId]: mapConfig }
// call original prop
addToContextProp(config)
this.setState({ ...newState })
// attach listener
const listener = e => this.syncableMapListener(map, e)
map.on(['synced', 'visible'], listener)
if (synced) this.setSyncedView(map)
}
onMapInitOverride = async (map, onMapInitProp = () => {}) => {
const maps = [...this.state.maps, map]
this.setState({ maps })
const promise = new Promise((resolve) => {
// call original prop
onMapInitProp(map)
this.promisesResolvers.push(resolve)
})
// check for that last time this is called & initialize
if (maps.length === this.props.mapsConfig.length) this.initialize()
return promise
}
initialize = async () => {
const { maps } = this.state
const { contextProps } = await this.props.onMapsInit(maps)
// resolve all onMapInit promises now
this.promisesResolvers.map(resolve => resolve())
this.refreshMaps()
this.setState({ initialized: true, ...contextProps })
}
getContextValue = () => {
const { contextProps, translations } = this.props
const { maps } = this.state
const map = maps[0]
return {
...this.state,
map,
onMapAdded: this.onMapAdded,
onMapRemoved: this.onMapRemoved,
syncedState: maps.map(m => m.getSyncedState()),
translations,
visibleState: maps.map(m => m.getVisibleState()),
visibleMapCount: maps.map(m => m.getVisibleState()).filter(e => e).length,
...contextProps
}
}
childModifier = rawChildren => {
const { initialized } = this.state
const children = !Array.isArray(rawChildren) ? [rawChildren] : rawChildren
const adoptedChildren = children.map((child, i) => {
// only render FlexMap & FullScreenFlex until initialized
const allow = initialized || child.props.disableAsyncRender
if (child.props.isMultiMap) {
// we caught a map
const addToContextOverride = config => this.addToContext(config, child.props.addMapToContext)
const onMapInitOverride = map => this.onMapInitOverride(map, child.props.onMapInit)
const propsOverride = {
...child.props,
addMapToContext: addToContextOverride,
onMapInit: onMapInitOverride,
_ol_kit_context_id: child.props.id,
isMultiMap: true,
key: i,
map: null // important so <Map> creates a SyncableMap
}
const adoptedChild = React.cloneElement(child, propsOverride)
return adoptedChild
} else if (Array.isArray(child)) {
// child is an array of children
return this.childModifier(child)
} else if (child?.props?.children) {
// loop through children of children
return allow && React.cloneElement(child, { ...child.props }, [this.childModifier(child.props.children)])
} else {
// this allows the Maps to render and initialize first before all other comps
return allow ? child : null
}
})
console.log('children rendered by MultiMapManager: ', adoptedChildren)
return adoptedChildren
}
render () {
const adoptedChildren = this.childModifier(this.props.children)
return (
<ErrorBoundary floating={true}>
<MultiMapContext.Provider value={this.getContextValue()}>
{adoptedChildren}
</MultiMapContext.Provider>
</ErrorBoundary>
)
}
}
MultiMapManager.defaultProps = {
contextProps: {},
groups: [],
onMapsInit: () => {},
translations: en
}
MultiMapManager.propTypes = {
/** Pass components as children of Provider component */
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
/** Add any custom props to context and they will be passed to all components wrapped by connectToContext */
contextProps: PropTypes.object,
/** Nested arrays of ids grouped together to syncronize events across multiple maps */
groups: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
/** Array of map config objects */
mapsConfig: PropTypes.array.isRequired,
/** callback called with array of map objects after all multimaps have been created */
onMapsInit: PropTypes.func,
/** Object with key/value pairs for component translation strings */
translations: PropTypes.object
}
export default MultiMapManager