The map was one picture of one floor. A second floor was added, the blueprint changed size, and machines moved, so a position now records WHICH DRAWING its coordinates belong to. Buildings and levels (ADR-017). Each level owns its blueprint per theme and its own native pixel size; assets.mapx/mapy are pixels of assets.levelid, not of the site. A position whose level is unknown renders "level unknown" and is never drawn on the default level, because a marker on the wrong floor plan looks entirely correct while pointing at the wrong place. Repositioning in bulk: filter by unplaced, needs-review or level, search, place, confirm. Landmark recalibration solves the transform PER AXIS from landmark pairs and never from image dimensions - the canvas grew taller without rescaling, so a dimension-derived scale would stretch Y by 1.57 and be wrong everywhere. It defaults to a dry run, reports what would land off the drawing, snapshots before applying, and clears mapverifiedat because a transform is a guess awaiting review. Snapshots restore, including the level and the review state, and a restore snapshots first so an undo is undoable. Search: gaugelabreference was matched only for measuring tools and maintenancereference was matched nowhere at all, for any asset type, while Settings happily offers both identifiers on machines and PCs. A tag an operator is told to record has to be findable or it is a write-only field. USB devices and printed items were unreachable from search entirely - neither is an asset, so the generic asset search could not see them and no searcher existed; they now match on serial, asset tag, label, bin code and gage-lab tag, honouring isactive, with Settings toggles and result labels to match. The retired-application rule was half a rule: GET /api/knowledgebase hid articles whose topic application is retired while global search still returned them and printed the retired application as the subject. A filter is only real if every path that reaches the row applies it. Contract to 0.20.0 (additive): Asset gained levelid and mapverifiedat, Location gained levelid, and resolve_asset_position returns the levelid belonging to whichever source supplied the coordinates. The five plugins that write a map position are re-pinned. The install-list text format gained levelid as a NINTH field, appended, because the shipped Pascal installer reads fields 0-7 by index. That installer still compiles in one drawing's dimensions and bundles one blueprint, so its map is accurate for the default level only; /api/maplevels is deliberately unauthenticated so it can read both at runtime once rebuilt. Recorded in PRINTER-INSTALLER.md section 6 along with the other known gaps. Migration 7d33 converts an existing single-map site into one building and one default level carrying the old map_* settings, then assigns every placed asset and location to it. Nothing moves on screen. Old settings rows are kept so a rollback still finds them. Verified end to end on MySQL 5.6 from a production-shaped database.
156 lines
5.1 KiB
JavaScript
156 lines
5.1 KiB
JavaScript
// Which drawing renders a marker, and at what native size (ADR-017).
|
|
//
|
|
// This used to hold ONE blueprint and ONE pixel size, read from four settings,
|
|
// because a site had one floor map. It now holds every level of every building,
|
|
// because `assets.mapx`/`mapy` are pixels in a specific level's space and the
|
|
// same coordinates mean different places on different drawings.
|
|
//
|
|
// THE RULE THIS FILE ENFORCES: a position without a level is not rendered on the
|
|
// default level. `blueprintUrlFor(theme, levelid)` returns null for an unknown
|
|
// level, and every caller must show "level unknown" rather than draw something.
|
|
// Falling back would put one building's ground floor behind a marker positioned
|
|
// for another building's mezzanine - it renders perfectly and points at the
|
|
// wrong place, which is worse than rendering nothing.
|
|
import { reactive } from 'vue'
|
|
|
|
import { mapLevelsApi } from '../api'
|
|
import { withBase } from '../utils/basePath'
|
|
|
|
// Used until the levels load, and on a fresh install with none configured, so a
|
|
// map still draws something rather than breaking.
|
|
const PLACEHOLDER = '/static/images/floorplan-placeholder.svg'
|
|
const FALLBACK_WIDTH = 3300
|
|
const FALLBACK_HEIGHT = 2550
|
|
|
|
export const state = reactive({
|
|
buildings: [],
|
|
// Flat index by levelid, because every hover preview resolves an arbitrary
|
|
// asset's level and has no idea which building it is in.
|
|
levels: {},
|
|
defaultlevelid: null,
|
|
currentlevelid: null,
|
|
loaded: false,
|
|
})
|
|
|
|
let inflight = null
|
|
|
|
function levelFor(levelid) {
|
|
if (levelid === null || levelid === undefined) return null
|
|
return state.levels[levelid] || null
|
|
}
|
|
|
|
function fetchLevels() {
|
|
inflight = mapLevelsApi.list()
|
|
.then(({ data }) => {
|
|
const payload = data.data || {}
|
|
state.buildings = payload.buildings || []
|
|
state.levels = {}
|
|
state.buildings.forEach(building => {
|
|
;(building.levels || []).forEach(level => {
|
|
state.levels[level.levelid] = { ...level, buildingname: building.buildingname }
|
|
})
|
|
})
|
|
state.defaultlevelid = payload.defaultlevelid || null
|
|
if (!state.currentlevelid || !state.levels[state.currentlevelid]) {
|
|
state.currentlevelid = state.defaultlevelid
|
|
}
|
|
state.loaded = true
|
|
})
|
|
.catch(() => { state.loaded = true })
|
|
.finally(() => { inflight = null })
|
|
return inflight
|
|
}
|
|
|
|
// Fetch once, shared across every map component. Await it before initialising a
|
|
// Leaflet map, which needs the dimensions to set its bounds.
|
|
export function loadMapConfig() {
|
|
if (state.loaded) return Promise.resolve()
|
|
if (inflight) return inflight
|
|
return fetchLevels()
|
|
}
|
|
|
|
// Re-read after the levels admin changes something.
|
|
export function reloadMapConfig() {
|
|
return fetchLevels()
|
|
}
|
|
|
|
export function setCurrentLevel(levelid) {
|
|
if (state.levels[levelid]) state.currentlevelid = levelid
|
|
}
|
|
|
|
/**
|
|
* Blueprint URL for one level in one theme, or null when the level is unknown.
|
|
*
|
|
* Falls back to the OTHER theme's image before giving up, because a site that
|
|
* uploaded only a light blueprint should still render in dark mode - a
|
|
* hard-to-read floor plan beats no floor plan.
|
|
*/
|
|
export function blueprintUrlFor(theme, levelid) {
|
|
const level = levelFor(levelid === undefined ? state.currentlevelid : levelid)
|
|
if (!level) return null
|
|
const wanted = theme === 'light' ? level.blueprintlight : level.blueprintdark
|
|
const other = theme === 'light' ? level.blueprintdark : level.blueprintlight
|
|
const chosen = wanted || other
|
|
return withBase(chosen || PLACEHOLDER)
|
|
}
|
|
|
|
/**
|
|
* Native pixel size of a level, which is what its marker coordinates mean.
|
|
*
|
|
* Returns the fallback for an unknown level so arithmetic does not divide by
|
|
* undefined, but callers deciding WHETHER to draw must ask `hasLevel` - these
|
|
* numbers are a safe default, not evidence the level exists.
|
|
*/
|
|
export function dimensionsFor(levelid) {
|
|
const level = levelFor(levelid === undefined ? state.currentlevelid : levelid)
|
|
return {
|
|
width: level?.mapwidth || FALLBACK_WIDTH,
|
|
height: level?.mapheight || FALLBACK_HEIGHT,
|
|
}
|
|
}
|
|
|
|
export function hasLevel(levelid) {
|
|
return !!levelFor(levelid)
|
|
}
|
|
|
|
export function levelName(levelid) {
|
|
const level = levelFor(levelid)
|
|
if (!level) return null
|
|
// Qualified by building only when there is more than one, so a single-building
|
|
// site is not made to read "Main / Ground floor" everywhere.
|
|
return state.buildings.length > 1
|
|
? `${level.buildingname} / ${level.levelname}`
|
|
: level.levelname
|
|
}
|
|
|
|
// Every level flat, in building then level order, for a selector.
|
|
export function levelOptions() {
|
|
const options = []
|
|
state.buildings.forEach(building => {
|
|
;(building.levels || []).forEach(level => {
|
|
options.push({
|
|
levelid: level.levelid,
|
|
levelname: level.levelname,
|
|
buildingname: building.buildingname,
|
|
label: state.buildings.length > 1
|
|
? `${building.buildingname} / ${level.levelname}`
|
|
: level.levelname,
|
|
})
|
|
})
|
|
})
|
|
return options
|
|
}
|
|
|
|
export function useMapConfig() {
|
|
loadMapConfig()
|
|
return {
|
|
state,
|
|
blueprintUrlFor,
|
|
dimensionsFor,
|
|
hasLevel,
|
|
levelName,
|
|
levelOptions,
|
|
setCurrentLevel,
|
|
}
|
|
}
|