Skip to content
14 changes: 9 additions & 5 deletions src/backends/adapters/MapLibreAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class MapLibreAdapter implements IMapBackend {

// Add navigation controls
this.map.addControl(new maplibregl.NavigationControl(), 'top-right')
this.map.addControl(new maplibregl.GlobeControl(), 'top-right')
this.map.addControl(new maplibregl.ScaleControl({ unit: 'metric' }), 'bottom-left')

// Wait for map to load
Expand Down Expand Up @@ -207,17 +208,20 @@ export class MapLibreAdapter implements IMapBackend {

const sourceId = `source-${id}`

// Remove all associated layers
const layerIds = [`${id}-fill`, `${id}-line`, `${id}-point`, id]
// Remove all associated layers (including label, highlight, heatmap)
const layerIds = [`${id}-fill`, `${id}-line`, `${id}-point`, `${id}-label`, `${id}-highlight-line`, `${id}-heatmap`, id]
for (const layerId of layerIds) {
if (this.map.getLayer(layerId)) {
this.map.removeLayer(layerId)
}
}

// Remove source
if (this.map.getSource(sourceId)) {
this.map.removeSource(sourceId)
// Remove sources (including highlight and heatmap sources)
const sourceIds = [sourceId, `${id}-highlight-source`, `${id}-heatmap-source`]
for (const sid of sourceIds) {
if (this.map.getSource(sid)) {
this.map.removeSource(sid)
}
}

this.layers.delete(id)
Expand Down
87 changes: 84 additions & 3 deletions src/components/landing/LandingPage.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { FolderOpen } from 'lucide-react'
import { useState, useEffect } from 'react'
import { FolderOpen, Clock, X } from 'lucide-react'
import { BackendSelector } from './BackendSelector'
import { useProjectStore } from '../../stores/projectStore'
import { useUIStore } from '../../stores/uiStore'
import type { BackendType } from '../../backends/types'
import type { RecentProject } from '../../types/project'

export function LandingPage() {
const { setBackend } = useProjectStore()
const { setView } = useUIStore()
const [recentProjects, setRecentProjects] = useState<RecentProject[]>([])

useEffect(() => {
setRecentProjects(useProjectStore.getState().getRecentProjects())
}, [])

const handleSelectBackend = (backend: BackendType) => {
setBackend(backend)
Expand All @@ -29,9 +36,48 @@ export function LandingPage() {
}
}

const handleOpenRecent = async (path: string) => {
const api = window.electronAPI
if (!api) return

try {
const result = await api.readFile(path)
if (result?.content) {
const project = JSON.parse(result.content)
useProjectStore.getState().loadProject(project, path)
setView('map')
}
} catch (e) {
console.error('Failed to open recent project:', e)
// Remove from recent if it fails
try {
const stored = localStorage.getItem('anymap-recent-projects')
const recent: RecentProject[] = stored ? JSON.parse(stored) : []
localStorage.setItem('anymap-recent-projects', JSON.stringify(recent.filter(r => r.path !== path)))
setRecentProjects(prev => prev.filter(r => r.path !== path))
} catch { /* ignore */ }
}
}

const handleRemoveRecent = (path: string, e: React.MouseEvent) => {
e.stopPropagation()
try {
const stored = localStorage.getItem('anymap-recent-projects')
const recent: RecentProject[] = stored ? JSON.parse(stored) : []
localStorage.setItem('anymap-recent-projects', JSON.stringify(recent.filter(r => r.path !== path)))
setRecentProjects(prev => prev.filter(r => r.path !== path))
} catch { /* ignore */ }
}

const formatDate = (iso: string) => {
try {
return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
} catch { return '' }
}

return (
<div className="flex h-full flex-col items-center justify-center bg-background p-8">
<div className="mb-12 text-center">
<div className="mb-10 text-center">
<h1 className="mb-2 text-4xl font-bold text-foreground">AnyMap Studio</h1>
<p className="text-lg text-muted-foreground">
Modern GIS desktop application with multi-backend mapping
Expand All @@ -41,13 +87,48 @@ export function LandingPage() {
<div className="mb-8 flex gap-4">
<button
onClick={handleOpenProject}
className="flex items-center gap-2 rounded-lg border border-border bg-card px-6 py-3 text-foreground hover:bg-accent"
className="flex items-center gap-2 rounded-lg border border-border bg-card px-6 py-3 text-foreground hover:bg-accent transition-colors"
>
<FolderOpen className="h-5 w-5" />
Open Project
</button>
</div>

{/* Recent Projects */}
{recentProjects.length > 0 && (
<div className="mb-8 w-full max-w-md">
<h3 className="mb-3 flex items-center gap-2 text-sm font-medium text-muted-foreground">
<Clock className="h-4 w-4" />
Recent Projects
</h3>
<div className="rounded-xl border border-border bg-card overflow-hidden">
{recentProjects.map((project, i) => (
<button
key={project.path}
onClick={() => handleOpenRecent(project.path)}
className={`flex w-full items-center gap-3 px-4 py-3 text-left hover:bg-accent transition-colors group ${
i > 0 ? 'border-t border-border' : ''
}`}
>
<FolderOpen className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-foreground truncate">{project.name}</div>
<div className="text-xs text-muted-foreground truncate">{project.path}</div>
</div>
<span className="text-xs text-muted-foreground whitespace-nowrap">{formatDate(project.lastOpened)}</span>
<button
onClick={(e) => handleRemoveRecent(project.path, e)}
className="rounded p-1 opacity-0 group-hover:opacity-100 hover:bg-slate-600 transition-all"
title="Remove from recent"
>
<X className="h-3 w-3 text-muted-foreground" />
</button>
</button>
))}
</div>
</div>
)}

<div className="mb-6 text-center">
<h2 className="text-xl font-semibold text-foreground">Start New Project</h2>
<p className="text-sm text-muted-foreground">Choose a mapping backend</p>
Expand Down
7 changes: 7 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,10 @@ body {
color: #e2e8f0;
background-color: transparent;
}

/* Hover popup - smaller, no close button */
.hover-popup .maplibregl-popup-content {
padding: 6px 10px;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
66 changes: 61 additions & 5 deletions src/stores/projectStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { create } from 'zustand'
import type { BackendType } from '../backends/types'
import type { AnyMapProject, UnifiedLayerConfig, ViewOptions } from '../types/project'
import type { AnyMapProject, UnifiedLayerConfig, ViewOptions, RecentProject } from '../types/project'

export interface Bookmark {
id: string
name: string
view: ViewOptions
}

interface ProjectState {
name: string
Expand All @@ -13,6 +19,7 @@ interface ProjectState {
selectedLayerId: string | null
created: string
modified: string
bookmarks: Bookmark[]

// Actions
setName: (name: string) => void
Expand All @@ -31,6 +38,15 @@ interface ProjectState {
toggleLayerVisibility: (id: string) => void
setLayerOpacity: (id: string, opacity: number) => void

// Bookmark operations
addBookmark: (name: string) => void
removeBookmark: (id: string) => void
goToBookmark: (id: string) => Bookmark | undefined

// Recent projects (persisted via localStorage)
getRecentProjects: () => RecentProject[]
addRecentProject: (path: string, name: string) => void

// Project operations
loadProject: (project: AnyMapProject, filePath?: string) => void
exportProject: () => AnyMapProject
Expand All @@ -55,6 +71,7 @@ export const useProjectStore = create<ProjectState>((set, get) => ({
selectedLayerId: null,
created: new Date().toISOString(),
modified: new Date().toISOString(),
bookmarks: [],

setName: (name) => set({ name, isDirty: true, modified: new Date().toISOString() }),
setDescription: (description) => set({ description, isDirty: true, modified: new Date().toISOString() }),
Expand Down Expand Up @@ -109,7 +126,42 @@ export const useProjectStore = create<ProjectState>((set, get) => ({
modified: new Date().toISOString()
})),

loadProject: (project, filePath) =>
addBookmark: (name) =>
set((state) => ({
bookmarks: [...state.bookmarks, { id: `bm-${Date.now()}`, name, view: { ...state.view } }],
isDirty: true
})),

removeBookmark: (id) =>
set((state) => ({
bookmarks: state.bookmarks.filter(b => b.id !== id),
isDirty: true
})),

goToBookmark: (id) => {
const bm = get().bookmarks.find(b => b.id === id)
if (bm) set({ view: { ...bm.view } })
return bm
},

getRecentProjects: () => {
try {
const stored = localStorage.getItem('anymap-recent-projects')
return stored ? JSON.parse(stored) : []
} catch { return [] }
},

addRecentProject: (path, name) => {
try {
const stored = localStorage.getItem('anymap-recent-projects')
const recent: RecentProject[] = stored ? JSON.parse(stored) : []
const filtered = recent.filter(r => r.path !== path)
filtered.unshift({ path, name, lastOpened: new Date().toISOString() })
localStorage.setItem('anymap-recent-projects', JSON.stringify(filtered.slice(0, 10)))
} catch { /* ignore */ }
},

loadProject: (project, filePath) => {
set({
name: project.metadata.name,
description: project.metadata.description || '',
Expand All @@ -120,8 +172,11 @@ export const useProjectStore = create<ProjectState>((set, get) => ({
layers: project.layers,
selectedLayerId: null,
created: project.metadata.created,
modified: project.metadata.modified
}),
modified: project.metadata.modified,
bookmarks: []
})
if (filePath) get().addRecentProject(filePath, project.metadata.name)
},

exportProject: () => {
const state = get()
Expand Down Expand Up @@ -150,6 +205,7 @@ export const useProjectStore = create<ProjectState>((set, get) => ({
layers: [],
selectedLayerId: null,
created: new Date().toISOString(),
modified: new Date().toISOString()
modified: new Date().toISOString(),
bookmarks: []
})
}))
10 changes: 10 additions & 0 deletions src/types/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ export interface ViewOptions {
bearing?: number
}

export interface LabelConfig {
enabled: boolean
field: string
fontSize?: number
color?: string
haloColor?: string
haloWidth?: number
}

export interface LayerStyle {
fillColor?: string
fillOpacity?: number
Expand All @@ -15,6 +24,7 @@ export interface LayerStyle {
strokeOpacity?: number
pointRadius?: number
pointColor?: string
label?: LabelConfig
}

export interface UnifiedLayerConfig {
Expand Down