Make mapbox map more reusable + add coordinates to exif
This commit is contained in:
parent
f3223005b1
commit
b5be84bbe4
|
@ -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)
|
||||
})
|
||||
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
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -80,6 +80,9 @@ type MapPresetMarkerProps = {
|
|||
dispatchMarkerMedia: React.Dispatch<PlacesAction>
|
||||
}
|
||||
|
||||
/**
|
||||
* Full-screen present-view that works with PlacesState
|
||||
*/
|
||||
const MapPresentMarker = ({
|
||||
map,
|
||||
markerMediaState,
|
||||
|
|
|
@ -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)
|
||||
|
||||
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,
|
||||
const { mapContainer, mapboxMap, mapboxToken } = useMapboxMap({
|
||||
configureMapbox: configureMapbox({ mapboxData, dispatchMarkerMedia }),
|
||||
})
|
||||
|
||||
// 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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue