1
Fork 0

Make mapbox map more reusable + add coordinates to exif

This commit is contained in:
viktorstrate 2021-10-03 15:36:10 +02:00
parent f3223005b1
commit b5be84bbe4
No known key found for this signature in database
GPG Key ID: 3F855605109C1E8A
12 changed files with 481 additions and 218 deletions

View File

@ -75,6 +75,11 @@ type ComplexityRoot struct {
Token func(childComplexity int) int
}
Coordinates struct {
Latitude func(childComplexity int) int
Longitude func(childComplexity int) int
}
FaceGroup struct {
ID func(childComplexity int) int
ImageFaceCount func(childComplexity int) int
@ -122,6 +127,7 @@ type ComplexityRoot struct {
MediaExif struct {
Aperture func(childComplexity int) int
Camera func(childComplexity int) int
Coordinates func(childComplexity int) int
DateShot func(childComplexity int) int
Exposure func(childComplexity int) int
ExposureProgram func(childComplexity int) int
@ -473,6 +479,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.AuthorizeResult.Token(childComplexity), true
case "Coordinates.latitude":
if e.complexity.Coordinates.Latitude == nil {
break
}
return e.complexity.Coordinates.Latitude(childComplexity), true
case "Coordinates.longitude":
if e.complexity.Coordinates.Longitude == nil {
break
}
return e.complexity.Coordinates.Longitude(childComplexity), true
case "FaceGroup.id":
if e.complexity.FaceGroup.ID == nil {
break
@ -695,6 +715,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.MediaExif.Camera(childComplexity), true
case "MediaEXIF.coordinates":
if e.complexity.MediaExif.Coordinates == nil {
break
}
return e.complexity.MediaExif.Coordinates(childComplexity), true
case "MediaEXIF.dateShot":
if e.complexity.MediaExif.DateShot == nil {
break
@ -1745,7 +1772,7 @@ type Query {
"Get media owned by the logged in user, returned in GeoJson format"
myMediaGeoJson: Any! @isAuthorized
"Get the mapbox api token, returns null if mapbox is not enabled"
mapboxToken: String @isAuthorized
mapboxToken: String
shareToken(credentials: ShareTokenCredentials!): ShareToken!
shareTokenValidatePassword(credentials: ShareTokenCredentials!): Boolean!
@ -1955,8 +1982,6 @@ type Album {
path: [Album!]!
shares: [ShareToken!]!
#coverID: Int
}
type MediaURL {
@ -2029,6 +2054,14 @@ type MediaEXIF {
flash: Int
"An index describing the mode for adjusting the exposure of the image"
exposureProgram: Int
coordinates: Coordinates
}
type Coordinates {
"GPS latitude in degrees"
latitude: Float!
"GPS longitude in degrees"
longitude: Float!
}
type VideoMetadata {
@ -3459,6 +3492,76 @@ func (ec *executionContext) _AuthorizeResult_token(ctx context.Context, field gr
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) _Coordinates_latitude(ctx context.Context, field graphql.CollectedField, obj *models.Coordinates) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Coordinates",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Latitude, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(float64)
fc.Result = res
return ec.marshalNFloat2float64(ctx, field.Selections, res)
}
func (ec *executionContext) _Coordinates_longitude(ctx context.Context, field graphql.CollectedField, obj *models.Coordinates) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Coordinates",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Longitude, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(float64)
fc.Result = res
return ec.marshalNFloat2float64(ctx, field.Selections, res)
}
func (ec *executionContext) _FaceGroup_id(ctx context.Context, field graphql.CollectedField, obj *models.FaceGroup) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -4853,6 +4956,38 @@ func (ec *executionContext) _MediaEXIF_exposureProgram(ctx context.Context, fiel
return ec.marshalOInt2ᚖint64(ctx, field.Selections, res)
}
func (ec *executionContext) _MediaEXIF_coordinates(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "MediaEXIF",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Coordinates(), nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*models.Coordinates)
fc.Result = res
return ec.marshalOCoordinates2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐCoordinates(ctx, field.Selections, res)
}
func (ec *executionContext) _MediaURL_url(ctx context.Context, field graphql.CollectedField, obj *models.MediaURL) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -7283,28 +7418,8 @@ func (ec *executionContext) _Query_mapboxToken(ctx context.Context, field graphq
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
directive0 := func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().MapboxToken(rctx)
}
directive1 := func(ctx context.Context) (interface{}, error) {
if ec.directives.IsAuthorized == nil {
return nil, errors.New("directive isAuthorized is not implemented")
}
return ec.directives.IsAuthorized(ctx, nil, directive0)
}
tmp, err := directive1(rctx)
if err != nil {
return nil, graphql.ErrorOnPath(ctx, err)
}
if tmp == nil {
return nil, nil
}
if data, ok := tmp.(*string); ok {
return data, nil
}
return nil, fmt.Errorf(`unexpected type %T from directive, should be *string`, tmp)
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().MapboxToken(rctx)
})
if err != nil {
ec.Error(ctx, err)
@ -10429,6 +10544,38 @@ func (ec *executionContext) _AuthorizeResult(ctx context.Context, sel ast.Select
return out
}
var coordinatesImplementors = []string{"Coordinates"}
func (ec *executionContext) _Coordinates(ctx context.Context, sel ast.SelectionSet, obj *models.Coordinates) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, coordinatesImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("Coordinates")
case "latitude":
out.Values[i] = ec._Coordinates_latitude(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "longitude":
out.Values[i] = ec._Coordinates_longitude(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var faceGroupImplementors = []string{"FaceGroup"}
func (ec *executionContext) _FaceGroup(ctx context.Context, sel ast.SelectionSet, obj *models.FaceGroup) graphql.Marshaler {
@ -10824,6 +10971,8 @@ func (ec *executionContext) _MediaEXIF(ctx context.Context, sel ast.SelectionSet
out.Values[i] = ec._MediaEXIF_flash(ctx, field, obj)
case "exposureProgram":
out.Values[i] = ec._MediaEXIF_exposureProgram(ctx, field, obj)
case "coordinates":
out.Values[i] = ec._MediaEXIF_coordinates(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@ -12875,6 +13024,13 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast
return graphql.MarshalBoolean(*v)
}
func (ec *executionContext) marshalOCoordinates2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐCoordinates(ctx context.Context, sel ast.SelectionSet, v *models.Coordinates) graphql.Marshaler {
if v == nil {
return graphql.Null
}
return ec._Coordinates(ctx, sel, v)
}
func (ec *executionContext) unmarshalOFloat2ᚖfloat64(ctx context.Context, v interface{}) (*float64, error) {
if v == nil {
return nil, nil

View File

@ -15,6 +15,13 @@ type AuthorizeResult struct {
Token *string `json:"token"`
}
type Coordinates struct {
// GPS latitude in degrees
Latitude float64 `json:"latitude"`
// GPS longitude in degrees
Longitude float64 `json:"longitude"`
}
type MediaDownload struct {
Title string `json:"title"`
MediaURL *MediaURL `json:"mediaUrl"`

View File

@ -28,3 +28,14 @@ func (MediaEXIF) TableName() string {
func (exif *MediaEXIF) Media() *Media {
panic("not implemented")
}
func (exif *MediaEXIF) Coordinates() *Coordinates {
if exif.GPSLatitude == nil || exif.GPSLongitude == nil {
return nil
}
return &Coordinates{
Latitude: *exif.GPSLatitude,
Longitude: *exif.GPSLongitude,
}
}

View File

@ -76,7 +76,7 @@ type Query {
"Get media owned by the logged in user, returned in GeoJson format"
myMediaGeoJson: Any! @isAuthorized
"Get the mapbox api token, returns null if mapbox is not enabled"
mapboxToken: String @isAuthorized
mapboxToken: String
shareToken(credentials: ShareTokenCredentials!): ShareToken!
shareTokenValidatePassword(credentials: ShareTokenCredentials!): Boolean!
@ -358,6 +358,15 @@ type MediaEXIF {
flash: Int
"An index describing the mode for adjusting the exposure of the image"
exposureProgram: Int
"GPS coordinates of where the image was taken"
coordinates: Coordinates
}
type Coordinates {
"GPS latitude in degrees"
latitude: Float!
"GPS longitude in degrees"
longitude: Float!
}
type VideoMetadata {

View File

@ -52,14 +52,6 @@ const MapClusterMarker = ({
const thumbnail = JSON.parse(marker.thumbnail)
const presentMedia = () => {
// presentMarkerClicked({
// dispatchMedia: dispatchMarkerMedia,
// mediaState: markerMediaState,
// marker: {
// cluster: !!marker.cluster,
// id: marker.cluster ? marker.cluster_id : marker.media_id,
// },
// })
dispatchMarkerMedia({
type: 'replacePresentMarker',
marker: {
@ -67,10 +59,6 @@ const MapClusterMarker = ({
id: marker.cluster ? marker.cluster_id : marker.media_id,
},
})
// setPresentMarker({
// cluster: !!marker.cluster,
// id: marker.cluster ? marker.cluster_id : marker.media_id,
// })
}
return (

View File

@ -80,6 +80,9 @@ type MapPresetMarkerProps = {
dispatchMarkerMedia: React.Dispatch<PlacesAction>
}
/**
* Full-screen present-view that works with PlacesState
*/
const MapPresentMarker = ({
map,
markerMediaState,

View File

@ -1,30 +1,24 @@
import { gql, useQuery } from '@apollo/client'
import type mapboxgl from 'mapbox-gl'
import React, { useEffect, useReducer, useRef, useState } from 'react'
import React, { useReducer } from 'react'
import { Helmet } from 'react-helmet'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import Layout from '../../components/layout/Layout'
import { makeUpdateMarkers } from './mapboxHelperFunctions'
import MapPresentMarker from './MapPresentMarker'
import { registerMediaMarkers } from '../../components/mapbox/mapboxHelperFunctions'
import useMapboxMap from '../../components/mapbox/MapboxMap'
import { urlPresentModeSetupHook } from '../../components/photoGallery/photoGalleryReducer'
import { placesReducer } from './placesReducer'
import 'mapbox-gl/dist/mapbox-gl.css'
import MapPresentMarker from './MapPresentMarker'
import { PlacesAction, placesReducer } from './placesReducer'
import { mediaGeoJson } from './__generated__/mediaGeoJson'
const MapWrapper = styled.div`
width: 100%;
height: calc(100vh - 120px);
`
const MapContainer = styled.div`
width: 100%;
height: 100%;
`
const MAPBOX_DATA_QUERY = gql`
query placePageMapboxToken {
mapboxToken
query mediaGeoJson {
myMediaGeoJson
}
`
@ -37,10 +31,9 @@ export type PresentMarker = {
const MapPage = () => {
const { t } = useTranslation()
const [mapboxLibrary, setMapboxLibrary] = useState<typeof mapboxgl | null>()
const mapContainer = useRef<HTMLDivElement | null>(null)
const map = useRef<mapboxgl.Map | null>(null)
// const [presentMarker, setPresentMarker] = useState<PresentMarker | null>(null)
const { data: mapboxData } = useQuery<mediaGeoJson>(MAPBOX_DATA_QUERY, {
fetchPolicy: 'cache-first',
})
const [markerMediaState, dispatchMarkerMedia] = useReducer(placesReducer, {
presenting: false,
@ -48,76 +41,21 @@ const MapPage = () => {
media: [],
})
const { data: mapboxData } = useQuery(MAPBOX_DATA_QUERY)
const { mapContainer, mapboxMap, mapboxToken } = useMapboxMap({
configureMapbox: configureMapbox({ mapboxData, dispatchMarkerMedia }),
})
useEffect(() => {
async function loadMapboxLibrary() {
const mapbox = (await import('mapbox-gl')).default
setMapboxLibrary(mapbox)
}
loadMapboxLibrary()
}, [])
useEffect(() => {
if (
mapboxLibrary == null ||
mapContainer.current == null ||
mapboxData == null ||
map.current != null
) {
return
}
mapboxLibrary.accessToken = mapboxData.mapboxToken
map.current = new mapboxLibrary.Map({
container: mapContainer.current,
style: 'mapbox://styles/mapbox/streets-v11',
zoom: 1,
})
// Add map navigation control
map.current.addControl(new mapboxLibrary.NavigationControl())
map.current.on('load', () => {
if (map.current == null) {
console.error('ERROR: map is null')
return
}
map.current.addSource('media', {
type: 'geojson',
data: mapboxData.myMediaGeoJson,
cluster: true,
clusterRadius: 50,
clusterProperties: {
thumbnail: ['coalesce', ['get', 'thumbnail'], false],
},
urlPresentModeSetupHook({
dispatchMedia: dispatchMarkerMedia,
openPresentMode: event => {
dispatchMarkerMedia({
type: 'openPresentMode',
activeIndex: event.state.activeIndex,
})
},
})
// Add dummy layer for features to be queryable
map.current.addLayer({
id: 'media-points',
type: 'circle',
source: 'media',
filter: ['!', true],
})
const updateMarkers = makeUpdateMarkers({
map: map.current,
mapboxLibrary,
dispatchMarkerMedia,
})
map.current.on('move', updateMarkers)
map.current.on('moveend', updateMarkers)
map.current.on('sourcedata', updateMarkers)
updateMarkers()
})
}, [mapContainer, mapboxLibrary, mapboxData])
if (mapboxData && mapboxData.mapboxToken == null) {
if (mapboxData && mapboxToken == null) {
return (
<Layout title={t('places_page.title', 'Places')}>
<h1>Mapbox token is not set</h1>
@ -134,34 +72,64 @@ const MapPage = () => {
)
}
urlPresentModeSetupHook({
dispatchMedia: dispatchMarkerMedia,
openPresentMode: event => {
dispatchMarkerMedia({
type: 'openPresentMode',
activeIndex: event.state.activeIndex,
})
},
})
return (
<Layout title="Places">
<Helmet>
{/* <link rel="stylesheet" href="/mapbox-gl.css" /> */}
{/* <style type="text/css">{mapboxStyles}</style> */}
</Helmet>
<MapWrapper>
<MapContainer ref={mapContainer}></MapContainer>
</MapWrapper>
<MapWrapper>{mapContainer}</MapWrapper>
<MapPresentMarker
map={map.current}
map={mapboxMap}
markerMediaState={markerMediaState}
dispatchMarkerMedia={dispatchMarkerMedia}
// presentMarker={presentMarker}
// setPresentMarker={setPresentMarker}
/>
</Layout>
)
}
const configureMapbox =
({
mapboxData,
dispatchMarkerMedia,
}: {
mapboxData?: mediaGeoJson
dispatchMarkerMedia: React.Dispatch<PlacesAction>
}) =>
(map: mapboxgl.Map, mapboxLibrary: typeof mapboxgl) => {
// Add map navigation control
map.addControl(new mapboxLibrary.NavigationControl())
map.on('load', () => {
if (map == null) {
console.error('ERROR: map is null')
return
}
map.addSource('media', {
type: 'geojson',
data: mapboxData?.myMediaGeoJson,
cluster: true,
clusterRadius: 50,
clusterProperties: {
thumbnail: ['coalesce', ['get', 'thumbnail'], false],
},
})
// Add dummy layer for features to be queryable
map.addLayer({
id: 'media-points',
type: 'circle',
source: 'media',
filter: ['!', true],
})
registerMediaMarkers({
map: map,
mapboxLibrary,
dispatchMarkerMedia,
})
})
}
export default MapPage

View File

@ -0,0 +1,15 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: mediaGeoJson
// ====================================================
export interface mediaGeoJson {
/**
* Get media owned by the logged in user, returned in GeoJson format
*/
myMediaGeoJson: any
}

View File

@ -1,80 +0,0 @@
import type mapboxgl from 'mapbox-gl'
import type geojson from 'geojson'
import React from 'react'
import ReactDOM from 'react-dom'
import MapClusterMarker from './MapClusterMarker'
import { MediaMarker } from './MapPresentMarker'
import { PlacesAction } from './placesReducer'
const markers: { [key: string]: mapboxgl.Marker } = {}
let markersOnScreen: typeof markers = {}
type makeUpdateMarkersArgs = {
map: mapboxgl.Map
mapboxLibrary: typeof mapboxgl
dispatchMarkerMedia: React.Dispatch<PlacesAction>
// setPresentMarker: React.Dispatch<React.SetStateAction<PresentMarker | null>>
}
export const makeUpdateMarkers = ({
map,
mapboxLibrary,
dispatchMarkerMedia,
}: makeUpdateMarkersArgs) => () => {
const newMarkers: typeof markers = {}
const features = map.querySourceFeatures('media')
// for every media on the screen, create an HTML marker for it (if we didn't yet),
// and add it to the map if it's not there already
for (const feature of features) {
const point = feature.geometry as geojson.Point
const coords = point.coordinates as [number, number]
const props = feature.properties as MediaMarker
if (props == null) {
console.warn('WARN: geojson feature had no properties', feature)
continue
}
const id = props.cluster
? `cluster_${props.cluster_id}`
: `media_${props.media_id}`
let marker = markers[id]
if (!marker) {
const el = createClusterPopupElement(props, {
dispatchMarkerMedia,
})
marker = markers[id] = new mapboxLibrary.Marker({
element: el,
}).setLngLat(coords)
}
newMarkers[id] = marker
if (!markersOnScreen[id]) marker.addTo(map)
}
// for every marker we've added previously, remove those that are no longer visible
for (const id in markersOnScreen) {
if (!newMarkers[id]) markersOnScreen[id].remove()
}
markersOnScreen = newMarkers
}
function createClusterPopupElement(
geojsonProps: MediaMarker,
{
dispatchMarkerMedia,
}: {
dispatchMarkerMedia: React.Dispatch<PlacesAction>
}
) {
// setPresentMarker: React.Dispatch<React.SetStateAction<PresentMarker | null>>
const el = document.createElement('div')
ReactDOM.render(
<MapClusterMarker
marker={geojsonProps}
dispatchMarkerMedia={dispatchMarkerMedia}
/>,
el
)
return el
}

View File

@ -0,0 +1,74 @@
import React, { useState, useRef, useEffect } from 'react'
import { gql, useQuery } from '@apollo/client'
import type mapboxgl from 'mapbox-gl'
import styled from 'styled-components'
import 'mapbox-gl/dist/mapbox-gl.css'
import { mapboxToken } from './__generated__/mapboxToken'
const MAPBOX_TOKEN_QUERY = gql`
query mapboxToken {
mapboxToken
myMediaGeoJson
}
`
const MapContainer = styled.div`
width: 100%;
height: 100%;
`
type MapboxMapProps = {
configureMapbox(map: mapboxgl.Map, mapboxLibrary: typeof mapboxgl): void
readonly initialZoom?: number
}
const useMapboxMap = ({ configureMapbox, initialZoom = 1 }: MapboxMapProps) => {
const [mapboxLibrary, setMapboxLibrary] = useState<typeof mapboxgl>()
const mapContainer = useRef<HTMLDivElement | null>(null)
const map = useRef<mapboxgl.Map | null>(null)
const { data: mapboxData } = useQuery<mapboxToken>(MAPBOX_TOKEN_QUERY, {
fetchPolicy: 'cache-first',
})
useEffect(() => {
async function loadMapboxLibrary() {
const mapbox = (await import('mapbox-gl')).default
setMapboxLibrary(mapbox)
}
loadMapboxLibrary()
}, [])
useEffect(() => {
if (
mapboxLibrary == null ||
mapContainer.current == null ||
mapboxData == null ||
map.current != null
) {
return
}
if (mapboxData.mapboxToken)
mapboxLibrary.accessToken = mapboxData.mapboxToken
map.current = new mapboxLibrary.Map({
container: mapContainer.current,
style: 'mapbox://styles/mapbox/streets-v11',
zoom: initialZoom,
})
configureMapbox(map.current, mapboxLibrary)
}, [mapContainer, mapboxLibrary, mapboxData])
return {
mapContainer: <MapContainer ref={mapContainer}></MapContainer>,
mapboxMap: map.current,
mapboxLibrary,
mapboxToken: mapboxData?.mapboxToken || null,
}
}
export default useMapboxMap

View File

@ -0,0 +1,19 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: mapboxToken
// ====================================================
export interface mapboxToken {
/**
* Get the mapbox api token, returns null if mapbox is not enabled
*/
mapboxToken: string | null
/**
* Get media owned by the logged in user, returned in GeoJson format
*/
myMediaGeoJson: any
}

View File

@ -0,0 +1,93 @@
import type mapboxgl from 'mapbox-gl'
import type geojson from 'geojson'
import React from 'react'
import ReactDOM from 'react-dom'
import MapClusterMarker from '../../Pages/PlacesPage/MapClusterMarker'
import { MediaMarker } from '../../Pages/PlacesPage/MapPresentMarker'
import { PlacesAction } from '../../Pages/PlacesPage/placesReducer'
const markers: { [key: string]: mapboxgl.Marker } = {}
let markersOnScreen: typeof markers = {}
type registerMediaMarkersArgs = {
map: mapboxgl.Map
mapboxLibrary: typeof mapboxgl
dispatchMarkerMedia: React.Dispatch<PlacesAction>
}
/**
* Add appropriate event handlers to the map, to render and update media markers
* Expects the provided mapbox map to contain geojson source of media
*/
export const registerMediaMarkers = (args: registerMediaMarkersArgs) => {
const updateMarkers = makeUpdateMarkers(args)
args.map.on('move', updateMarkers)
args.map.on('moveend', updateMarkers)
args.map.on('sourcedata', updateMarkers)
updateMarkers()
}
/**
* Make a function that can be passed to Mapbox to tell it how to render and update the image markers
*/
const makeUpdateMarkers =
({ map, mapboxLibrary, dispatchMarkerMedia }: registerMediaMarkersArgs) =>
() => {
const newMarkers: typeof markers = {}
const features = map.querySourceFeatures('media')
// for every media on the screen, create an HTML marker for it (if we didn't yet),
// and add it to the map if it's not there already
for (const feature of features) {
const point = feature.geometry as geojson.Point
const coords = point.coordinates as [number, number]
const props = feature.properties as MediaMarker
if (props == null) {
console.warn('WARN: geojson feature had no properties', feature)
continue
}
const id = props.cluster
? `cluster_${props.cluster_id}`
: `media_${props.media_id}`
let marker = markers[id]
if (!marker) {
const el = createClusterPopupElement(props, {
dispatchMarkerMedia,
})
marker = markers[id] = new mapboxLibrary.Marker({
element: el,
}).setLngLat(coords)
}
newMarkers[id] = marker
if (!markersOnScreen[id]) marker.addTo(map)
}
// for every marker we've added previously, remove those that are no longer visible
for (const id in markersOnScreen) {
if (!newMarkers[id]) markersOnScreen[id].remove()
}
markersOnScreen = newMarkers
}
function createClusterPopupElement(
geojsonProps: MediaMarker,
{
dispatchMarkerMedia,
}: {
dispatchMarkerMedia: React.Dispatch<PlacesAction>
}
) {
// setPresentMarker: React.Dispatch<React.SetStateAction<PresentMarker | null>>
const el = document.createElement('div')
ReactDOM.render(
<MapClusterMarker
marker={geojsonProps}
dispatchMarkerMedia={dispatchMarkerMedia}
/>,
el
)
return el
}