diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000000..b8598415c2 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,97 @@ +# Chronicle Map + +A first-class chronicle system inside Wiki.js for pinning historical (or fictional) events onto era-specific maps, with page embeds and a master overview. + +## Language + +**Chronicle**: +A first-class aggregate that owns era maps, events, and its own tags. Wiki pages embed or link to it; they do not own its data. +_Avoid_: Map collection, project, atlas (as the top-level container) + +**Event**: +A pin-able record inside a Chronicle — title, summary, when/where it occurred, and tags. May optionally link to one or more Wiki Pages for long-form detail; the pin itself is not a Page. +_Avoid_: Page (for the pin), marker-only stub with no domain identity + +**Era Map**: +A dated basemap (2D imagery or 3D scene) owned by a Chronicle, covering a time range. Multiple Era Maps coexist; each can be aligned to a shared spatial reference so the same Event can appear across eras despite scale and position drift. +_Avoid_: Layer (as the map entity), basemap-only asset with no era identity + +**Canonical Position**: +The Event's single authoritative location in the Chronicle's shared spatial reference (typically modern lat/lng, or a custom CRS for fictional worlds). Default source for projecting pins onto every Era Map. +_Avoid_: Per-map coordinates (as the primary store), "roughly around here" without a stored point + +**Alignment**: +A transform on an Era Map (control points / warp) that maps Canonical Positions onto that basemap's pixel or local space, correcting scale and position drift between eras. +_Avoid_: Manual re-pinning every Event per map, ad-hoc UI pan/zoom offsets + +**Pin Override**: +An optional per–Event-per–Era-Map position that replaces the Alignment projection when the default transform is wrong for that pin. Escape hatch only — Canonical Position remains the default. +_Avoid_: Making per-map pins the normal path for every Event + +**Occurrence**: +When an Event happened — a point or interval on the Chronicle timeline (may be approximate). Used to decide default Era Map visibility by intersection with each Era Map's time range. +_Avoid_: "Date" alone when an interval or fuzzy span is meant; publication date of the wiki page + +**Visibility Override**: +An optional per–Event-per–Era-Map force include or force exclude that replaces the default Occurrence∩Era-Map time-range rule. +_Avoid_: Manually listing every Era Map for every Event as the normal path + +**Chronicle Tag**: +A Chronicle-scoped label for filtering and grouping Events on maps (e.g. haunt, traffic death, urban, outlying islands). Independent of Wiki.js Page Tags. +_Avoid_: Page Tag (for map filters), global taxonomy shared across unrelated Chronicles + +**Page Tag**: +The existing Wiki.js flat tag on a Wiki Page. Used only for wiki content; not the primary map filter. An Event's linked Pages keep their own Page Tags. +_Avoid_: Reusing Page Tags as the Chronicle's event taxonomy + +**Chronicle Overlay**: +An optional map view mode that shows Events (and optionally Era Maps) from more than one Chronicle on the same canvas, with each Chronicle toggleable. Chronicles remain separate aggregates; overlay is a view concern, not a merge of data. +_Avoid_: World Layer inside a single Chronicle; permanently merging taxonomies or alignments across Chronicles + +**Map View**: +The single interactive map experience for exploring Chronicles — Era Maps, Event pins, filters, overlays, and (for editors) alignment tools. Reused everywhere; not duplicated per surface. +_Avoid_: Separate “simple embed map” and “full map” products with different data rules + +**Chronicle Embed**: +A Map View instance placed inside a Wiki Page, usually with a locked or narrowed scope (one Chronicle, Era Map, Tag set, or Event focus). Same underlying Map View as the master route. +_Avoid_: Static thumbnail that is not a Map View; treating the embed as a second data store + +**Event Draft**: +A proposed Event (from Ingest API or AI Analysis) that is not yet a live pin. Becomes an Event only after human confirmation. Same fields as Event, different lifecycle. +_Avoid_: Writing AI/API output straight to live Events; a separate parallel data model unrelated to Event + +**Ingest API**: +An HTTP/GraphQL API for submitting structured Event or Event Draft payloads from external systems. Does not bypass confirmation when the payload is marked as draft/AI-sourced. +_Avoid_: Ad-hoc DB inserts; scraping hooks that publish live pins with no review path + +**AI Analysis**: +A process that turns unstructured input (text, URL, bulk notes) into one or more Event Drafts — suggested title, Occurrence, Canonical Position, Chronicle Tags — for human review. +_Avoid_: AI as the source of truth; AI that only fills a form with no draft record + +**Map Mode**: +How an Era Map is presented in Map View: `2d`, `3d`, or `both` (user-switchable). Events still use a 2D Canonical Position by default; 3D is primarily a presentation concern, with optional elevation later. +_Avoid_: Separate 2D-only and 3D-only Era Map entity types; treating 3D as a different aggregate + +**Basemap Source**: +Where an Era Map's imagery or tiles come from. One of: Public Basemap, Uploaded Basemap, or Derived Basemap. Source type lives on the Era Map; it is not a separate aggregate. +_Avoid_: Iframe-only external maps outside the Chronicle model; one-off assets with no source metadata + +**Public Basemap**: +A Basemap Source that connects to an open or configured tile/map provider (e.g. OSM, satellite, government open layers) by provider id or URL. Preferred for real-world geographies already in a known CRS. +_Avoid_: Re-uploading public tiles as static images when a provider works + +**Uploaded Basemap**: +A Basemap Source from manually uploaded imagery or tile packs — fantasy maps, scanned historical maps, custom art. Usually requires Alignment. +_Avoid_: Treating uploads as already georeferenced without Alignment + +**Derived Basemap**: +A Basemap Source produced by AI/API conversion from an image or other map (e.g. suggested Alignment control points, cleaned tile asset). Stored as a draft asset until a human confirms and attaches it to an Era Map. +_Avoid_: AI silently publishing live basemaps; calling this a third aggregate beside Era Map + +**Footprint**: +Optional line or polygon geometry on an Event that shows extent (building, road stretch, district). Canonical Position remains required; Footprint does not replace it for alignment, overrides, or default AI geocoding. +_Avoid_: Polygon-only Events with no point; treating Footprint as the primary pin + +**Chronicle Permission**: +Access rules owned by the Chronicle domain — read map data, write Events, approve Event Drafts / Derived Basemaps, and manage Era Maps / Alignment. Independent of Wiki Page permissions; may be global or per-Chronicle via groups. +_Avoid_: Inferring Chronicle edit rights from “can edit this wiki page”; admin-only as the only write model diff --git a/client/client-app.js b/client/client-app.js index f3dc05a8b6..88f40853f3 100644 --- a/client/client-app.js +++ b/client/client-app.js @@ -170,6 +170,7 @@ Vue.component('Register', () => import(/* webpackChunkName: "register" */ './com Vue.component('SearchResults', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/search-results.vue')) Vue.component('SocialSharing', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/social-sharing.vue')) Vue.component('Tags', () => import(/* webpackChunkName: "tags" */ './components/tags.vue')) +Vue.component('ChronicleMap', () => import(/* webpackChunkName: "chronicle" */ './components/chronicle-map.vue')) Vue.component('Unauthorized', () => import(/* webpackChunkName: "unauthorized" */ './components/unauthorized.vue')) Vue.component('VCardChin', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/v-card-chin.vue')) Vue.component('VCardInfo', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/v-card-info.vue')) diff --git a/client/components/admin.vue b/client/components/admin.vue index aa6ad23c01..6721398309 100644 --- a/client/components/admin.vue +++ b/client/components/admin.vue @@ -39,6 +39,9 @@ :dark='pendingReviewsCount > 0' ) .caption(:class='pendingReviewsCount > 0 ? `` : `grey--text`') {{ pendingReviewsCount }} + v-list-item(to='/chronicles', color='primary', v-if='hasPermission([`manage:system`, `manage:chronicles`])') + v-list-item-avatar(size='24', tile): v-icon mdi-map-clock-outline + v-list-item-title Chronicles v-list-item(to='/tags', v-if='hasPermission([`manage:system`])') v-list-item-avatar(size='24', tile): v-icon mdi-tag-multiple v-list-item-title {{ $t('admin:tags.title') }} @@ -163,6 +166,7 @@ const router = new VueRouter({ { path: '/pages/:id(\\d+)', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-pages-edit.vue') }, { path: '/pages/visualize', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-pages-visualize.vue') }, { path: '/page-reviews', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-page-reviews.vue') }, + { path: '/chronicles', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-chronicles.vue') }, { path: '/tags', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-tags.vue') }, { path: '/theme', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-theme.vue') }, { path: '/groups', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-groups.vue') }, diff --git a/client/components/admin/admin-chronicles.vue b/client/components/admin/admin-chronicles.vue new file mode 100644 index 0000000000..a61f50d356 --- /dev/null +++ b/client/components/admin/admin-chronicles.vue @@ -0,0 +1,574 @@ + + + + + diff --git a/client/components/admin/admin-groups-edit-permissions.vue b/client/components/admin/admin-groups-edit-permissions.vue index 0e955c7053..5f5d9cabd9 100644 --- a/client/components/admin/admin-groups-edit-permissions.vue +++ b/client/components/admin/admin-groups-edit-permissions.vue @@ -155,6 +155,34 @@ export default { warning: false, restrictedForSystem: true, disabled: false + }, + { + permission: 'read:chronicles', + hint: 'Can view Chronicle maps and event pins', + warning: false, + restrictedForSystem: false, + disabled: false + }, + { + permission: 'write:chronicles', + hint: 'Can create and edit Chronicle events (and submit drafts)', + warning: false, + restrictedForSystem: true, + disabled: false + }, + { + permission: 'approve:chronicles', + hint: 'Can approve Event Drafts and Derived Basemaps', + warning: true, + restrictedForSystem: true, + disabled: false + }, + { + permission: 'manage:chronicles', + hint: 'Can create Chronicles, Era Maps, Alignment, and manage all chronicle data', + warning: true, + restrictedForSystem: true, + disabled: false } ] }, diff --git a/client/components/chronicle-map.vue b/client/components/chronicle-map.vue new file mode 100644 index 0000000000..0935c552b3 --- /dev/null +++ b/client/components/chronicle-map.vue @@ -0,0 +1,346 @@ + + + + + diff --git a/client/themes/default/components/chronicle-embed.vue b/client/themes/default/components/chronicle-embed.vue new file mode 100644 index 0000000000..4547b53cb4 --- /dev/null +++ b/client/themes/default/components/chronicle-embed.vue @@ -0,0 +1,31 @@ + + + diff --git a/client/themes/default/components/page.vue b/client/themes/default/components/page.vue index 73f314f671..d3836bb314 100644 --- a/client/themes/default/components/page.vue +++ b/client/themes/default/components/page.vue @@ -393,6 +393,7 @@