import OlMap from 'ol/Map.js'
import { defaults as defaultControls } from 'ol/control'
import FullScreen from 'ol/control/FullScreen'
import { defaults as defaultInteractions } from 'ol/interaction'
import Collection from 'ol/Collection'
import { image2world } from '@dataforsyningen/saul'
import svgSprites from '@dataforsyningen/designsystem/assets/icons.svg'
import { SkraaFotoExposureTool } from '../tools/map-tool-exposure.js'
import { PlacementPinTool } from '../tools/map-tool-pin.js'
import { MeasureWidthTool } from '../tools/map-tool-measure-width.js'
import { MeasureHeightTool } from '../tools/map-tool-measure-height.js'
import { updateViewportPointer, generatePointerLayer } from '../../custom-plugins/plugin-pointer'
import { footprintHandler } from '../../custom-plugins/plugin-footprint.js'
import { configuration } from '../../modules/configuration.js'
import {
updateViewport,
updateMapView,
updateMapImage,
updateMapCenterIcon,
updateTextContent,
updatePlugins,
updateCenter
} from './viewport-mixin.js'
import { state, reaction, when, autorun } from '../../state/index.js'
customElements.define('skraafoto-exposure-tool', SkraaFotoExposureTool)
customElements.define('skraafoto-pin-tool', PlacementPinTool)
customElements.define('skraafoto-measure-width-tool', MeasureWidthTool)
customElements.define('skraafoto-measure-height-tool', MeasureHeightTool)
// Imports and definitions based on configuration
if (configuration.ENABLE_PRINT) {
import('../tools/map-tool-print.js').then(({ SkraaFotoPrintTool }) => {
customElements.define('skraafoto-print-tool', SkraaFotoPrintTool)
})
}
if (configuration.ENABLE_DOWNLOAD) {
import('../tools/map-tool-download.js').then(({ SkraaFotoDownloadTool }) => {
customElements.define('skraafoto-download-tool', SkraaFotoDownloadTool)
})
}
/**
* HTML web component that displays an image using the OpenLayers library.
* This is the main component of the Skraafoto application.
* It provides methods, and UI tools for handling interactions with the image.
*/
export class SkraaFotoViewport extends HTMLElement {
// properties
coord_image
map
compass_element
update_pointer_function
update_view_function
tool_measure_width
tool_measure_height
template = /*html*/`
<p class="basic-image-info"></p>
<nav class="ds-nav-tools sf-viewport-tools" data-theme="light">
<div class="ds-button-group">
<skraafoto-year-selector data-itemkey="${ this.dataset.itemkey }" data-viewport-id="${this.id}"></skraafoto-year-selector>
<hr>
<skraafoto-pin-tool></skraafoto-pin-tool>
<skraafoto-measure-width-tool></skraafoto-measure-width-tool>
<skraafoto-measure-height-tool></skraafoto-measure-height-tool>
<skraafoto-info-box id="info-btn"></skraafoto-info-box>
${ configuration.ENABLE_DOWNLOAD ? '<skraafoto-download-tool></skraafoto-download-tool>' : '' }
${ configuration.ENABLE_PRINT ? '<skraafoto-print-tool></skraafoto-print-tool>' : '' }
</div>
</nav>
<skraafoto-date-selector data-itemkey="${ this.dataset.itemkey }"></skraafoto-date-selector>
<div class="viewport-map"></div>
${
configuration.ENABLE_COMPASSARROWS ?
`<skraafoto-compass-arrows direction="north" data-itemkey="${ this.dataset.itemkey }"></skraafoto-compass-arrows>`:
`<skraafoto-compass direction="north"></skraafoto-compass>`
}
${ configuration.ENABLE_GEOLOCATION ? `<skraafoto-geolocation></skraafoto-geolocation>`: '' }
`
constructor() {
super()
}
// Methods
createDOM() {
this.innerHTML = this.template
this.compass_element = configuration.ENABLE_COMPASSARROWS ? this.querySelector('skraafoto-compass-arrows') : this.querySelector('skraafoto-compass')
}
/** Creates an OpenLayers map object and adds interactions, image data, etc. to it */
async createMap(item) {
// Initialize a map
this.map = new OlMap({
target: this.querySelector('.viewport-map'),
controls: defaultControls({rotate: false, attribution: false, zoom: true}),
interactions: new Collection()
})
updateMapImage(this.map, item)
await updateMapView({
map: this.map,
item: item,
zoom: state.view.zoom,
center: this.coord_image
})
updateMapCenterIcon(this.map, this.coord_image)
// add interactions
const interactions = defaultInteractions({ pinchRotate: false })
interactions.forEach(interaction => {
this.map.addInteraction(interaction)
})
// Add controls
this.querySelector('.ol-zoom-out').innerHTML = `<svg><use href="${ svgSprites }#minus" /></svg>`
this.querySelector('.ol-zoom-in').innerHTML = `<svg><use href="${ svgSprites }#plus" /></svg>`
if (configuration.ENABLE_FULLSCREEN) {
this.map.addControl(new FullScreen({
className: 'sf-fullscreen-btn',
label: '',
tipLabel: 'Skift fuldskærmsvisning'
}))
// Add our custom fullscreen icon to fullscreen button
this.querySelector('.sf-fullscreen-btn button').innerHTML = `
<svg class="fullscreen-false"><use href="${ svgSprites }#fullscreen" /></svg>
<svg class="fullscreen-true"><use href="${ svgSprites }#close" /></svg>
`
}
}
/** Initializes the image map */
initializeMap(item) {
if (!item) {
return
}
this.toggleSpinner(true)
const center = state.view.position
updateCenter(center, item).then((newCenters) => {
this.coord_image = newCenters.imageCoord
this.createMap(item)
this.setupTools()
this.updateNonMap(item)
this.setupListeners()
})
}
setupTools() {
this.tool_measure_width = new MeasureWidthTool(this)
this.tool_measure_height = new MeasureHeightTool(this)
// Add button to adjust brightness to the DOM
const button_group = this.querySelector('.ds-button-group')
const info_button = this.querySelector('#info-btn')
button_group.insertBefore(document.createElement('skraafoto-exposure-tool'), info_button)
}
/** Updates various items not directly related to the image map */
updateNonMap(item) {
this.compass_element.setAttribute('direction', item.properties.direction)
this.querySelector('.basic-image-info').innerText = updateTextContent(item)
updatePlugins(this, item)
this.querySelector('skraafoto-info-box').setItem = item
this.querySelector('skraafoto-exposure-tool').setContextTarget = this
this.querySelector('skraafoto-pin-tool').setContextTarget = this
if (configuration.ENABLE_PRINT) {
this.querySelector('skraafoto-print-tool').setContextTarget = this
}
if (configuration.ENABLE_DOWNLOAD) {
this.querySelector('skraafoto-download-tool').setContextTarget = this
}
}
/** Toggles the visibility of the loading spinner. */
toggleSpinner(bool) {
const canvasElement = this.querySelector('.ol-viewport canvas')
if (bool) {
if (canvasElement) {
canvasElement.style.cursor = 'progress'
}
// Attach a loading animation element while updating
const spinner_element = document.createElement('ds-spinner')
spinner_element.className = 'viewport-spinner'
this.append(spinner_element)
} else {
if (canvasElement) {
canvasElement.style.cursor = 'inherit'
}
// Removes loading animation elements
setTimeout(() => {
this.querySelectorAll('.viewport-spinner').forEach(function(spinner) {
spinner.remove()
})
}, 200)
}
}
/**
* Triggers view sync in all viewports by updating the `view` state.
*/
syncHandler() {
const view = this.map.getView()
if (!view) {
return
}
const center = view.getCenter()
const world_zoom = view.getZoom()
const world_center = image2world(state.items[this.dataset.itemkey], center[0], center[1], state.view.kote)
state.setView({
zoom: world_zoom,
position: world_center.slice(0,2),
kote: world_center[2]
})
}
// Maintains zoom level at new marker
toImageZoom(zoom) {
return zoom
}
clearDrawings() {
// Clears tooltips from layer
const overlays = this.map.getOverlays()
overlays.forEach((overlay) => {
if (!overlay) {
return
}
const className = overlay.getElement().className
if (className === 'ol-tooltip ol-tooltip-measure' || className === 'ol-tooltip ol-tooltip-static') {
this.map.removeOverlay(overlay)
}
})
}
setupListeners() {
// When map has finished loading, remove spinner, etc.
this.map.on('rendercomplete', () => {
this.toggleSpinner(false)
})
// When state changes, update viewport
this.reactionDisposer = reaction(
() => {
return {
item: state.items[this.dataset.itemkey],
view: {
position: state.view.position,
kote: state.view.kote,
zoom: state.view.zoom
},
marker: {
position: state.marker.position,
kote: state.marker.kote
},
mode: state.toolMode
}
},
(newData, oldData) => {
// If there is no new image or the user just fiddled with some tools (`mode`),
// refrain from updating the viewport since that will abort the Draw action.
if (!newData || newData.mode && newData.mode !== 'center') {
return
}
if (!oldData || newData.item.id !== oldData.item.id) {
this.clearDrawings()
}
// Update viewport
updateViewport(newData, oldData, this.map).then(() => {
this.updateNonMap(newData.item)
})
}
)
if (configuration.ENABLE_POINTER) {
this.map.addLayer(generatePointerLayer())
this.pointerDisposer = autorun(() => {
updateViewportPointer(this, state.pointerPosition, this.dataset.itemkey)
})
}
this.map.on('pointermove', (event) => {
// When user moves the pointer over this viewport, update all other viewports
if (configuration.ENABLE_POINTER) {
const coord = image2world(state.items[this.dataset.itemkey], event.coordinate[0], event.coordinate[1], state.view.kote)
state.setPointerPosition = {point: coord, itemkey: this.dataset.itemkey}
}
// When user changes viewport orientation, display image footprint on the map
footprintHandler(event, state.items[this.dataset.itemkey])
})
// Viewport sync trigger
this.map.on('moveend', this.syncHandler.bind(this))
}
// Lifecycle callbacks
connectedCallback() {
this.createDOM()
// Initialize image map when image item is available
this.whenDisposer = when(
() => state.items[this.dataset.itemkey],
() => { this.initializeMap(state.items[this.dataset.itemkey]) }
)
}
disconnectedCallback() {
this.pointerDisposer()
this.reactionDisposer()
this.whenDisposer()
}
}