Skip to content

Commit

Permalink
M1190 add IC map labels for patches
Browse files Browse the repository at this point in the history
  • Loading branch information
mmnoo committed Jan 24, 2025
1 parent 9a7354c commit d2f6235
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 42 deletions.
Binary file added public/label-background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ const ImageAnnotationModal = ({

const { imageScale } = useImageScale({ hasMapLoaded, dataToReview })

const { getPointsGeojson } = usePointsGeoJson({ dataToReview, imageScale, map })
const { getPointsGeojson, getPointsLabelAnchorsGeoJson } = usePointsGeoJson({
dataToReview,
imageScale,
map,
})

const { zoomToPointsByAttributeId } = useZoomToPointsByAttributeId({
getPointsGeojson,
Expand Down Expand Up @@ -157,6 +161,7 @@ const ImageAnnotationModal = ({
databaseSwitchboardInstance={databaseSwitchboardInstance}
setIsDataUpdatedSinceLastSave={setIsDataUpdatedSinceLastSave}
getPointsGeojson={getPointsGeojson}
getPointsLabelAnchorsGeoJson={getPointsLabelAnchorsGeoJson}
hasMapLoaded={hasMapLoaded}
imageScale={imageScale}
map={map}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,21 @@ const ImageAnnotationModalMap = ({
databaseSwitchboardInstance,
setIsDataUpdatedSinceLastSave,
getPointsGeojson,
getPointsLabelAnchorsGeoJson,
hasMapLoaded,
imageScale,
map,
setHasMapLoaded,
setIsTableShowing,
isTableShowing,
}) => {
const [areLabelsShowing, setAreLabelsShowing] = useState(false)
const [hoveredPointId, setHoveredPointId] = useState(null)
const [selectedPoint, setSelectedPoint] = useState({
id: null,
popupAnchorLngLat: null,
popupAnchorPosition: null,
})
const [areLablesShowing, setAreLabelsShowing] = useState(false)
const mapContainer = useRef(null)
const popupRef = useRef()

Expand All @@ -98,7 +99,34 @@ const ImageAnnotationModalMap = ({
}

const toggleLabels = () => {
setAreLabelsShowing(!areLablesShowing)
const newAreLabelsShowing = !areLabelsShowing
setAreLabelsShowing(newAreLabelsShowing)

if (!map.current) {
return
}

if (newAreLabelsShowing) {
map.current.addLayer({
id: 'patches-label-layer',
type: 'symbol',
source: 'patches-labels',
layout: {
'text-field': ['get', 'ba_gr_label'],
'text-radial-offset': 1.2,
'text-anchor': 'bottom',
'icon-text-fit': 'both',
'icon-image': 'label-background',
'text-size': 11,
},
})

return
}

if (map.current.getLayer('patches-label-layer')) {
map.current.removeLayer('patches-label-layer')
}
}

const updatePointsOnMap = useCallback(() => {
Expand All @@ -111,9 +139,10 @@ const ImageAnnotationModalMap = ({

map.current?.getSource('patches')?.setData(patchesGeoJson)
map.current?.getSource('patches-center')?.setData(patchesCenters)
map.current?.getSource('patches-labels')?.setData(getPointsLabelAnchorsGeoJson())

hackResetMapToCurrentPosition(map, currentZoom, currentCenter)
}, [getPointsGeojson, map])
}, [getPointsGeojson, getPointsLabelAnchorsGeoJson, map])

const updateImageSizeOnMap = () => {
const bounds = map.current.getBounds()
Expand Down Expand Up @@ -145,6 +174,7 @@ const ImageAnnotationModalMap = ({
minZoom: DEFAULT_ZOOM,
renderWorldCopies: false, // prevents the image from repeating
dragRotate: false,
accessToken: process.env.REACT_APP_MAPBOX_ACCESS_TOKEN,
})

map.current.addControl(zoomControl, 'top-left')
Expand All @@ -156,6 +186,7 @@ const ImageAnnotationModalMap = ({
map.current.setStyle({
version: 8,
name: 'image',
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
sources: {
benthicQuadratImage: {
type: 'image',
Expand All @@ -176,6 +207,7 @@ const ImageAnnotationModalMap = ({
type: 'geojson',
data: patchesCenters,
},
'patches-labels': { type: 'geojson', data: getPointsLabelAnchorsGeoJson() },
},
layers: [
{
Expand Down Expand Up @@ -229,66 +261,99 @@ const ImageAnnotationModalMap = ({

const handleMapLoad = () => {
setHasMapLoaded(true)

map.current.loadImage('/cross-hair.png', (error, image) => {
if (error) {
return
}
map.current.addImage('crosshair', image)
map.current.addImage('cross-hair', image)
map.current.addLayer({
id: 'patches-center-layer',
type: 'symbol',
source: 'patches-center',
layout: {
'icon-image': 'crosshair',
'icon-image': 'cross-hair',
},
})
})
}
const displayPointFeatureLabel = ({ features }) => {
const [{ properties }] = features
map.current.getCanvas().style.cursor = 'pointer'
const label = properties.isUnclassified ? 'Unclassified' : properties.ba_gr_label
const confirmedStatus = properties.isConfirmed ? 'confirmed' : 'unconfirmed'
const pointStatus = properties.isUnclassified ? 'unclassified' : confirmedStatus
const popupContent = (
<LabelPopup>
<IconCircle style={{ color: IMAGE_CLASSIFICATION_COLORS[pointStatus] }} /> {label}
</LabelPopup>
)
const popupContentHack = document.createElement('div')
ReactDOM.createRoot(popupContentHack).render(popupContent)

pointLabelPopup
.setLngLat(JSON.parse(properties.labelAnchor))
.setDOMContent(popupContentHack)
.addTo(map.current)

pointLabelPopup.once('open', () => {
const popupElement = document.querySelector('.mapboxgl-popup-content')

if (popupElement) {
popupElement.style.padding = '0'
map.current.loadImage('/label-background.png', (error, image) => {
if (error) {
return
}

map.current.addImage('label-background', image, {
// this configuration allows the image to stretch around the label text
stretchX: [[5, 135]],
stretchY: [[5, 135]],
content: [5, 5, 135, 135],
pixelRatio: 2,
})
})
}
const hidePointFeatureLabel = () => {
map.current.getCanvas().style.cursor = ''
pointLabelPopup.remove()
}

map.current.on('load', handleMapLoad)
map.current.on('mouseenter', 'patches-fill-layer', displayPointFeatureLabel)
map.current.on('mouseleave', 'patches-fill-layer', hidePointFeatureLabel)

const currentMap = map.current
return () => {
currentMap.off('load', handleMapLoad)
currentMap.off('mouseenter', 'patches-fill-layer', displayPointFeatureLabel)
currentMap.off('mouseleave', 'patches-fill-layer', hidePointFeatureLabel)
currentMap.remove()
}
// eslint-disable-next-line
}, [])

useEffect(
function configurePatchesPopup() {
if (!map.current) {
return
}
const displayPointFeatureLabel = ({ features }) => {
if (areLabelsShowing) {
return
}
const [{ properties }] = features
map.current.getCanvas().style.cursor = 'pointer'
const label = properties.isUnclassified ? 'Unclassified' : properties.ba_gr_label
const confirmedStatus = properties.isConfirmed ? 'confirmed' : 'unconfirmed'
const pointStatus = properties.isUnclassified ? 'unclassified' : confirmedStatus
const popupContent = (
<LabelPopup>
<IconCircle style={{ color: IMAGE_CLASSIFICATION_COLORS[pointStatus] }} /> {label}
</LabelPopup>
)
const popupContentHack = document.createElement('div')
ReactDOM.createRoot(popupContentHack).render(popupContent)

pointLabelPopup
.setLngLat(JSON.parse(properties.labelAnchor))
.setDOMContent(popupContentHack)
.addTo(map.current)

pointLabelPopup.once('open', () => {
// eslint-disable-next-line testing-library/no-node-access
const popupElementForStylingHack = document.querySelector('.mapboxgl-popup-content')

if (popupElementForStylingHack) {
popupElementForStylingHack.style.padding = '0'
}
})
}
const hidePointFeatureLabel = () => {
map.current.getCanvas().style.cursor = ''
pointLabelPopup.remove()
}

map.current.on('mouseenter', 'patches-fill-layer', displayPointFeatureLabel)
map.current.on('mouseleave', 'patches-fill-layer', hidePointFeatureLabel)

const currentMap = map.current
return () => {
currentMap.off('mouseenter', 'patches-fill-layer', displayPointFeatureLabel)
currentMap.off('mouseleave', 'patches-fill-layer', hidePointFeatureLabel)
}
},
[areLabelsShowing, map],
)

const zoomToSelectedPoint = useCallback(() => {
if (!selectedPoint.bounds || !map.current) {
return
Expand Down Expand Up @@ -493,7 +558,7 @@ const ImageAnnotationModalMap = ({
<ToggleTableButton type="button" onClick={toggleTable} $isSelected={isTableShowing}>
<IconTable />
</ToggleTableButton>
<ToggleLabelsButton type="button" onClick={toggleLabels} $isSelected={areLablesShowing}>
<ToggleLabelsButton type="button" onClick={toggleLabels} $isSelected={areLabelsShowing}>
<IconLabel />
</ToggleLabelsButton>

Expand Down Expand Up @@ -528,6 +593,7 @@ ImageAnnotationModalMap.propTypes = {
databaseSwitchboardInstance: PropTypes.object.isRequired,
setIsDataUpdatedSinceLastSave: PropTypes.func.isRequired,
getPointsGeojson: PropTypes.func.isRequired,
getPointsLabelAnchorsGeoJson: PropTypes.func.isRequired,
hasMapLoaded: PropTypes.bool.isRequired,
imageScale: PropTypes.number.isRequired,
map: PropTypes.object.isRequired,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const usePointsGeoJson = ({ dataToReview, map, imageScale }) => {
properties: {
id: point.id,
ba_gr: point.annotations[0]?.ba_gr,
ba_gr_label: point.annotations[0]?.ba_gr_label,
ba_gr_label: point.annotations[0]?.ba_gr_label ?? 'Unclassified',
isUnclassified: !point.annotations.length,
isConfirmed: !!point.annotations[0]?.is_confirmed,
isPointInLeftHalfOfImage,
Expand Down Expand Up @@ -70,5 +70,30 @@ export const usePointsGeoJson = ({ dataToReview, map, imageScale }) => {
],
)

return { getPointsGeojson }
const getPointsLabelAnchorsGeoJson = useCallback(
() => ({
type: 'FeatureCollection',
features: dataToReview.points.flatMap((point) => {
const labelPosition = map.current.unproject([
point.column * imageScale,
(point.row - halfPatchSize) * imageScale,
])

return {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [labelPosition.lng, labelPosition.lat],
},
properties: {
id: point.id,
ba_gr_label: point.annotations[0]?.ba_gr_label ?? 'Unclassified',
},
}
}),
}),
[dataToReview, map, imageScale, halfPatchSize],
)

return { getPointsGeojson, getPointsLabelAnchorsGeoJson }
}

0 comments on commit d2f6235

Please sign in to comment.