Add initial project structure with backend and frontend setup
- Created backend structure with Go, including main application logic and API endpoints. - Added Docker support for both development and production environments. - Introduced frontend using Nuxt 3 with Tailwind CSS for styling. - Included configuration files for Docker and environment variables. - Established basic documentation for contributing, development, and deployment processes. - Set up .gitignore and .dockerignore files to manage ignored files in the repository.
This commit is contained in:
61
frontend-nuxt/lib/Character.js
Normal file
61
frontend-nuxt/lib/Character.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { HnHMaxZoom } from '~/lib/LeafletCustomTypes'
|
||||
import * as L from 'leaflet'
|
||||
|
||||
export class Character {
|
||||
constructor(characterData) {
|
||||
this.name = characterData.name
|
||||
this.position = characterData.position
|
||||
this.type = characterData.type
|
||||
this.id = characterData.id
|
||||
this.map = characterData.map
|
||||
this.marker = null
|
||||
this.text = this.name
|
||||
this.value = this.id
|
||||
this.onClick = null
|
||||
}
|
||||
|
||||
getId() {
|
||||
return `${this.name}`
|
||||
}
|
||||
|
||||
remove(mapview) {
|
||||
if (this.marker) {
|
||||
const layer = mapview.markerLayer ?? mapview.map
|
||||
layer.removeLayer(this.marker)
|
||||
this.marker = null
|
||||
}
|
||||
}
|
||||
|
||||
add(mapview) {
|
||||
if (this.map === mapview.mapid) {
|
||||
const position = mapview.map.unproject([this.position.x, this.position.y], HnHMaxZoom)
|
||||
this.marker = L.marker(position, { title: this.name })
|
||||
this.marker.on('click', this.callCallback.bind(this))
|
||||
const targetLayer = mapview.markerLayer ?? mapview.map
|
||||
this.marker.addTo(targetLayer)
|
||||
}
|
||||
}
|
||||
|
||||
update(mapview, updated) {
|
||||
if (this.map !== updated.map) {
|
||||
this.remove(mapview)
|
||||
}
|
||||
this.map = updated.map
|
||||
this.position = updated.position
|
||||
if (!this.marker && this.map === mapview.mapid) {
|
||||
this.add(mapview)
|
||||
}
|
||||
if (this.marker) {
|
||||
const position = mapview.map.unproject([updated.position.x, updated.position.y], HnHMaxZoom)
|
||||
this.marker.setLatLng(position)
|
||||
}
|
||||
}
|
||||
|
||||
setClickCallback(callback) {
|
||||
this.onClick = callback
|
||||
}
|
||||
|
||||
callCallback(e) {
|
||||
if (this.onClick != null) this.onClick(e)
|
||||
}
|
||||
}
|
||||
88
frontend-nuxt/lib/LeafletCustomTypes.js
Normal file
88
frontend-nuxt/lib/LeafletCustomTypes.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import L, { Bounds, LatLng, Point } from 'leaflet'
|
||||
|
||||
export const TileSize = 100
|
||||
export const HnHMaxZoom = 6
|
||||
export const HnHMinZoom = 1
|
||||
export const HnHDefaultZoom = 6
|
||||
|
||||
/** When scaleFactor exceeds this, render one label per tile instead of a full grid (avoids 100k+ DOM nodes at zoom 1). */
|
||||
const GRID_COORD_SCALE_FACTOR_THRESHOLD = 8
|
||||
|
||||
export const GridCoordLayer = L.GridLayer.extend({
|
||||
options: {
|
||||
visible: true,
|
||||
},
|
||||
createTile(coords) {
|
||||
if (!this.options.visible) {
|
||||
const element = document.createElement('div')
|
||||
element.style.width = TileSize + 'px'
|
||||
element.style.height = TileSize + 'px'
|
||||
element.classList.add('map-tile')
|
||||
return element
|
||||
}
|
||||
const element = document.createElement('div')
|
||||
element.style.width = TileSize + 'px'
|
||||
element.style.height = TileSize + 'px'
|
||||
element.style.position = 'relative'
|
||||
element.classList.add('map-tile')
|
||||
|
||||
const scaleFactor = Math.pow(2, HnHMaxZoom - coords.z)
|
||||
const topLeft = { x: coords.x * scaleFactor, y: coords.y * scaleFactor }
|
||||
const bottomRight = { x: topLeft.x + scaleFactor - 1, y: topLeft.y + scaleFactor - 1 }
|
||||
|
||||
if (scaleFactor > GRID_COORD_SCALE_FACTOR_THRESHOLD) {
|
||||
// Low zoom: one label per tile to avoid hundreds of thousands of DOM nodes (Reset view freeze fix)
|
||||
const textElement = document.createElement('div')
|
||||
textElement.classList.add('map-tile-text')
|
||||
textElement.textContent = `(${topLeft.x}, ${topLeft.y})`
|
||||
textElement.style.position = 'absolute'
|
||||
textElement.style.left = '2px'
|
||||
textElement.style.top = '2px'
|
||||
textElement.style.fontSize = Math.max(8, 12 - Math.log2(scaleFactor) * 2) + 'px'
|
||||
element.appendChild(textElement)
|
||||
return element
|
||||
}
|
||||
|
||||
for (let gx = topLeft.x; gx <= bottomRight.x; gx++) {
|
||||
for (let gy = topLeft.y; gy <= bottomRight.y; gy++) {
|
||||
const leftPx = ((gx - topLeft.x) / scaleFactor) * TileSize
|
||||
const topPx = ((gy - topLeft.y) / scaleFactor) * TileSize
|
||||
const textElement = document.createElement('div')
|
||||
textElement.classList.add('map-tile-text')
|
||||
textElement.textContent = `(${gx}, ${gy})`
|
||||
textElement.style.position = 'absolute'
|
||||
textElement.style.left = leftPx + 2 + 'px'
|
||||
textElement.style.top = topPx + 2 + 'px'
|
||||
if (scaleFactor > 1) {
|
||||
textElement.style.fontSize = Math.max(8, 12 - Math.log2(scaleFactor) * 2) + 'px'
|
||||
}
|
||||
element.appendChild(textElement)
|
||||
}
|
||||
}
|
||||
return element
|
||||
},
|
||||
})
|
||||
|
||||
export const ImageIcon = L.Icon.extend({
|
||||
options: {
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 16],
|
||||
},
|
||||
})
|
||||
|
||||
const latNormalization = (90.0 * TileSize) / 2500000.0
|
||||
const lngNormalization = (180.0 * TileSize) / 2500000.0
|
||||
|
||||
const HnHProjection = {
|
||||
project(latlng) {
|
||||
return new Point(latlng.lat / latNormalization, latlng.lng / lngNormalization)
|
||||
},
|
||||
unproject(point) {
|
||||
return new LatLng(point.x * latNormalization, point.y * lngNormalization)
|
||||
},
|
||||
bounds: (() => new Bounds([-latNormalization, -lngNormalization], [latNormalization, lngNormalization]))(),
|
||||
}
|
||||
|
||||
export const HnHCRS = L.extend({}, L.CRS.Simple, {
|
||||
projection: HnHProjection,
|
||||
})
|
||||
89
frontend-nuxt/lib/Marker.js
Normal file
89
frontend-nuxt/lib/Marker.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import { HnHMaxZoom, ImageIcon } from '~/lib/LeafletCustomTypes'
|
||||
import * as L from 'leaflet'
|
||||
|
||||
function detectType(name) {
|
||||
if (name === 'gfx/invobjs/small/bush' || name === 'gfx/invobjs/small/bumling') return 'quest'
|
||||
if (name === 'custom') return 'custom'
|
||||
return name.substring('gfx/terobjs/mm/'.length)
|
||||
}
|
||||
|
||||
export class Marker {
|
||||
constructor(markerData) {
|
||||
this.id = markerData.id
|
||||
this.position = markerData.position
|
||||
this.name = markerData.name
|
||||
this.image = markerData.image
|
||||
this.type = detectType(this.image)
|
||||
this.marker = null
|
||||
this.text = this.name
|
||||
this.value = this.id
|
||||
this.hidden = markerData.hidden
|
||||
this.map = markerData.map
|
||||
this.onClick = null
|
||||
this.onContext = null
|
||||
}
|
||||
|
||||
remove(mapview) {
|
||||
if (this.marker) {
|
||||
this.marker.remove()
|
||||
this.marker = null
|
||||
}
|
||||
}
|
||||
|
||||
add(mapview) {
|
||||
if (!this.hidden) {
|
||||
let icon
|
||||
if (this.image === 'gfx/terobjs/mm/custom') {
|
||||
icon = new ImageIcon({
|
||||
iconUrl: 'gfx/terobjs/mm/custom.png',
|
||||
iconSize: [21, 23],
|
||||
iconAnchor: [11, 21],
|
||||
popupAnchor: [1, 3],
|
||||
tooltipAnchor: [1, 3],
|
||||
})
|
||||
} else {
|
||||
icon = new ImageIcon({ iconUrl: `${this.image}.png`, iconSize: [32, 32] })
|
||||
}
|
||||
|
||||
const position = mapview.map.unproject([this.position.x, this.position.y], HnHMaxZoom)
|
||||
this.marker = L.marker(position, { icon, title: this.name })
|
||||
this.marker.addTo(mapview.markerLayer)
|
||||
this.marker.on('click', this.callClickCallback.bind(this))
|
||||
this.marker.on('contextmenu', this.callContextCallback.bind(this))
|
||||
}
|
||||
}
|
||||
|
||||
update(mapview, updated) {
|
||||
this.position = updated.position
|
||||
this.name = updated.name
|
||||
this.hidden = updated.hidden
|
||||
this.map = updated.map
|
||||
if (this.marker) {
|
||||
const position = mapview.map.unproject([updated.position.x, updated.position.y], HnHMaxZoom)
|
||||
this.marker.setLatLng(position)
|
||||
}
|
||||
}
|
||||
|
||||
jumpTo(map) {
|
||||
if (this.marker) {
|
||||
const position = map.unproject([this.position.x, this.position.y], HnHMaxZoom)
|
||||
this.marker.setLatLng(position)
|
||||
}
|
||||
}
|
||||
|
||||
setClickCallback(callback) {
|
||||
this.onClick = callback
|
||||
}
|
||||
|
||||
callClickCallback(e) {
|
||||
if (this.onClick != null) this.onClick(e)
|
||||
}
|
||||
|
||||
setContextMenu(callback) {
|
||||
this.onContext = callback
|
||||
}
|
||||
|
||||
callContextCallback(e) {
|
||||
if (this.onContext != null) this.onContext(e)
|
||||
}
|
||||
}
|
||||
73
frontend-nuxt/lib/SmartTileLayer.js
Normal file
73
frontend-nuxt/lib/SmartTileLayer.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import L, { Util, Browser } from 'leaflet'
|
||||
|
||||
export const SmartTileLayer = L.TileLayer.extend({
|
||||
cache: {},
|
||||
invalidTile: '',
|
||||
map: 0,
|
||||
|
||||
getTileUrl(coords) {
|
||||
if (!this._map) return this.invalidTile
|
||||
let zoom
|
||||
try {
|
||||
zoom = this._getZoomForUrl()
|
||||
} catch {
|
||||
return this.invalidTile
|
||||
}
|
||||
return this.getTrueTileUrl(coords, zoom)
|
||||
},
|
||||
|
||||
getTrueTileUrl(coords, zoom) {
|
||||
const data = {
|
||||
r: Browser.retina ? '@2x' : '',
|
||||
s: this._getSubdomain(coords),
|
||||
x: coords.x,
|
||||
y: coords.y,
|
||||
map: this.map,
|
||||
z: zoom,
|
||||
}
|
||||
if (this._map && !this._map.options.crs.infinite) {
|
||||
const invertedY = this._globalTileRange.max.y - coords.y
|
||||
if (this.options.tms) {
|
||||
data.y = invertedY
|
||||
}
|
||||
data['-y'] = invertedY
|
||||
}
|
||||
|
||||
const cacheKey = `${data.map}:${data.x}:${data.y}:${data.z}`
|
||||
data.cache = this.cache[cacheKey]
|
||||
|
||||
// Don't request tiles for invalid/unknown map (avoids 404 spam in console)
|
||||
if (data.map === undefined || data.map === null || data.map < 1) {
|
||||
return this.invalidTile
|
||||
}
|
||||
// Only use placeholder when server explicitly marks tile as invalid (-1)
|
||||
if (data.cache === -1) {
|
||||
return this.invalidTile
|
||||
}
|
||||
// Allow tile request when map is valid even if SSE snapshot hasn't arrived yet
|
||||
// (avoids empty map when proxy/SSE delays or drops first message)
|
||||
if (data.cache === undefined || data.cache === null) {
|
||||
data.cache = 0
|
||||
}
|
||||
|
||||
return Util.template(this._url, Util.extend(data, this.options))
|
||||
},
|
||||
|
||||
refresh(x, y, z) {
|
||||
let zoom = z
|
||||
const maxZoom = this.options.maxZoom
|
||||
const zoomReverse = this.options.zoomReverse
|
||||
const zoomOffset = this.options.zoomOffset
|
||||
|
||||
if (zoomReverse) {
|
||||
zoom = maxZoom - zoom
|
||||
}
|
||||
zoom += zoomOffset
|
||||
|
||||
const key = `${x}:${y}:${zoom}`
|
||||
const tile = this._tiles[key]
|
||||
if (tile) {
|
||||
tile.el.src = this.getTrueTileUrl({ x, y }, z)
|
||||
}
|
||||
},
|
||||
})
|
||||
39
frontend-nuxt/lib/UniqueList.js
Normal file
39
frontend-nuxt/lib/UniqueList.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Elements should have unique field "id"
|
||||
*/
|
||||
export class UniqueList {
|
||||
constructor() {
|
||||
this.elements = {}
|
||||
}
|
||||
|
||||
update(dataList, addCallback, removeCallback, updateCallback) {
|
||||
const elementsToAdd = dataList.filter((it) => this.elements[it.id] === undefined)
|
||||
const elementsToRemove = Object.keys(this.elements)
|
||||
.filter((it) => dataList.find((up) => String(up.id) === it) === undefined)
|
||||
.map((id) => this.elements[id])
|
||||
if (removeCallback) {
|
||||
elementsToRemove.forEach((it) => removeCallback(it))
|
||||
}
|
||||
if (updateCallback) {
|
||||
dataList.forEach((newElement) => {
|
||||
const oldElement = this.elements[newElement.id]
|
||||
if (oldElement) {
|
||||
updateCallback(oldElement, newElement)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (addCallback) {
|
||||
elementsToAdd.forEach((it) => addCallback(it))
|
||||
}
|
||||
elementsToRemove.forEach((it) => delete this.elements[it.id])
|
||||
elementsToAdd.forEach((it) => (this.elements[it.id] = it))
|
||||
}
|
||||
|
||||
getElements() {
|
||||
return Object.values(this.elements)
|
||||
}
|
||||
|
||||
byId(id) {
|
||||
return this.elements[id]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user