make kestrel a tak server, so that it can send and receive pois as cots data
Some checks failed
ci/woodpecker/pr/pr Pipeline failed
Some checks failed
ci/woodpecker/pr/pr Pipeline failed
This commit is contained in:
@@ -66,6 +66,10 @@ const props = defineProps({
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
cotEntities: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
canEditPois: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -81,6 +85,7 @@ const mapContext = ref(null)
|
||||
const markersRef = ref([])
|
||||
const poiMarkersRef = ref({})
|
||||
const liveMarkersRef = ref({})
|
||||
const cotMarkersRef = ref({})
|
||||
const contextMenu = ref({ ...CONTEXT_MENU_EMPTY })
|
||||
|
||||
const showPoiModal = ref(false)
|
||||
@@ -89,6 +94,7 @@ const addPoiLatlng = ref(null)
|
||||
const editPoi = ref(null)
|
||||
const deletePoi = ref(null)
|
||||
const poiForm = ref({ label: '', iconType: 'pin' })
|
||||
const resizeObserver = ref(null)
|
||||
|
||||
const TILE_URL = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png'
|
||||
const TILE_SUBDOMAINS = 'abcd'
|
||||
@@ -135,6 +141,17 @@ function getLiveSessionIcon(L) {
|
||||
})
|
||||
}
|
||||
|
||||
const COT_ICON_COLOR = '#f59e0b' /* amber – ATAK/CoT devices */
|
||||
function getCotEntityIcon(L) {
|
||||
const html = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${COT_ICON_COLOR}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="8" r="2.5" fill="${COT_ICON_COLOR}"/></svg>`
|
||||
return L.divIcon({
|
||||
className: 'poi-div-icon cot-entity-icon',
|
||||
html: `<span class="poi-icon-svg">${html}</span>`,
|
||||
iconSize: [ICON_SIZE, ICON_SIZE],
|
||||
iconAnchor: [ICON_SIZE / 2, ICON_SIZE],
|
||||
})
|
||||
}
|
||||
|
||||
function createMap(initialCenter) {
|
||||
const { L, offlineApi } = leafletRef.value || {}
|
||||
if (typeof document === 'undefined' || !mapRef.value || !L?.map) return
|
||||
@@ -201,6 +218,7 @@ function createMap(initialCenter) {
|
||||
updateMarkers()
|
||||
updatePoiMarkers()
|
||||
updateLiveMarkers()
|
||||
updateCotMarkers()
|
||||
nextTick(() => map.invalidateSize())
|
||||
}
|
||||
|
||||
@@ -291,6 +309,39 @@ function updateLiveMarkers() {
|
||||
liveMarkersRef.value = next
|
||||
}
|
||||
|
||||
function updateCotMarkers() {
|
||||
const ctx = mapContext.value
|
||||
const { L } = leafletRef.value || {}
|
||||
if (!ctx?.map || !L) return
|
||||
|
||||
const entities = (props.cotEntities || []).filter(
|
||||
e => typeof e?.lat === 'number' && typeof e?.lng === 'number' && e?.id,
|
||||
)
|
||||
const byId = Object.fromEntries(entities.map(e => [e.id, e]))
|
||||
const prev = cotMarkersRef.value
|
||||
const icon = getCotEntityIcon(L)
|
||||
|
||||
Object.keys(prev).forEach((id) => {
|
||||
if (!byId[id]) prev[id]?.remove()
|
||||
})
|
||||
|
||||
const next = entities.reduce((acc, entity) => {
|
||||
const content = `<div class="kestrel-live-popup"><strong>${escapeHtml(entity.label || entity.id)}</strong> <span class="text-kestrel-muted">ATAK</span></div>`
|
||||
const existing = prev[entity.id]
|
||||
if (existing) {
|
||||
existing.setLatLng([entity.lat, entity.lng])
|
||||
existing.setIcon(icon)
|
||||
existing.getPopup()?.setContent(content)
|
||||
return { ...acc, [entity.id]: existing }
|
||||
}
|
||||
const marker = L.marker([entity.lat, entity.lng], { icon })
|
||||
.addTo(ctx.map)
|
||||
.bindPopup(content, { className: 'kestrel-live-popup-wrap', maxWidth: 360 })
|
||||
return { ...acc, [entity.id]: marker }
|
||||
}, {})
|
||||
cotMarkersRef.value = next
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div')
|
||||
div.textContent = text
|
||||
@@ -376,6 +427,8 @@ function destroyMap() {
|
||||
poiMarkersRef.value = {}
|
||||
Object.values(liveMarkersRef.value).forEach(m => m?.remove())
|
||||
liveMarkersRef.value = {}
|
||||
Object.values(cotMarkersRef.value).forEach(m => m?.remove())
|
||||
cotMarkersRef.value = {}
|
||||
|
||||
const ctx = mapContext.value
|
||||
if (ctx) {
|
||||
@@ -404,8 +457,6 @@ function initMapWithLocation() {
|
||||
)
|
||||
}
|
||||
|
||||
let resizeObserver = null
|
||||
|
||||
onMounted(async () => {
|
||||
if (!import.meta.client || typeof document === 'undefined') return
|
||||
const [leaflet, offline] = await Promise.all([
|
||||
@@ -428,10 +479,10 @@ onMounted(async () => {
|
||||
|
||||
nextTick(() => {
|
||||
if (mapRef.value) {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
resizeObserver.value = new ResizeObserver(() => {
|
||||
mapContext.value?.map?.invalidateSize()
|
||||
})
|
||||
resizeObserver.observe(mapRef.value)
|
||||
resizeObserver.value.observe(mapRef.value)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -442,9 +493,9 @@ function onDocumentClick(e) {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', onDocumentClick)
|
||||
if (resizeObserver && mapRef.value) {
|
||||
resizeObserver.disconnect()
|
||||
resizeObserver = null
|
||||
if (resizeObserver.value && mapRef.value) {
|
||||
resizeObserver.value.disconnect()
|
||||
resizeObserver.value = null
|
||||
}
|
||||
destroyMap()
|
||||
})
|
||||
@@ -452,4 +503,5 @@ onBeforeUnmount(() => {
|
||||
watch(() => props.devices, () => updateMarkers(), { deep: true })
|
||||
watch([() => props.pois, () => props.canEditPois], () => updatePoiMarkers(), { deep: true })
|
||||
watch(() => props.liveSessions, () => updateLiveMarkers(), { deep: true })
|
||||
watch(() => props.cotEntities, () => updateCotMarkers(), { deep: true })
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user