fix: enable open-mapping module with TypeScript fixes
- Fix unused parameter errors (prefix with underscore)
- Fix TrustCircleManager API: add getTrustLevel/setTrustLevel methods
- Fix MyceliumNetwork method calls: addNode→createNode, addHypha→createHypha
- Fix createCommitment signature to use CommitmentParams object
- Fix GeohashCommitment type with proper geohash field
- Fix PRECISION_CELL_SIZE usage (returns {lat,lng} object)
- Add type assertions for fetch response data
- Fix MapCanvas attributionControl type
- Fix GPSCollaborationLayer markerStyle merge with defaults
- Update MapShapeUtil with better event handling:
- Raise z-index to 10000 for all map buttons
- Add pointerEvents: auto for button clickability
- Add handleWheel with preventDefault to enable map zoom
- Add capturePointerEvents for proper interaction
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d491d3ea72
commit
637f05b715
|
|
@ -11,11 +11,11 @@
|
|||
"dev:client": "vite --host 0.0.0.0 --port 5173",
|
||||
"dev:worker": "wrangler dev --config wrangler.dev.toml --remote --port 5172",
|
||||
"dev:worker:local": "wrangler dev --config wrangler.dev.toml --port 5172 --ip 0.0.0.0",
|
||||
"build": "tsc && vite build",
|
||||
"build": "NODE_OPTIONS=\"--max-old-space-size=8192\" tsc && NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
|
||||
"build:worker": "wrangler build --config wrangler.dev.toml",
|
||||
"preview": "vite preview",
|
||||
"deploy": "tsc && vite build && wrangler deploy",
|
||||
"deploy:pages": "tsc && vite build",
|
||||
"deploy": "NODE_OPTIONS=\"--max-old-space-size=8192\" tsc && NODE_OPTIONS=\"--max-old-space-size=8192\" vite build && wrangler deploy",
|
||||
"deploy:pages": "NODE_OPTIONS=\"--max-old-space-size=8192\" tsc && NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
|
||||
"deploy:worker": "wrangler deploy",
|
||||
"deploy:worker:dev": "wrangler deploy --config wrangler.dev.toml",
|
||||
"types": "tsc --noEmit",
|
||||
|
|
|
|||
|
|
@ -24,11 +24,11 @@ interface LayerPanelProps {
|
|||
export function LayerPanel({
|
||||
layers,
|
||||
onLayerToggle,
|
||||
onLayerOpacity,
|
||||
onLayerReorder,
|
||||
onLayerAdd,
|
||||
onLayerRemove,
|
||||
onLayerEdit,
|
||||
onLayerOpacity: _onLayerOpacity,
|
||||
onLayerReorder: _onLayerReorder,
|
||||
onLayerAdd: _onLayerAdd,
|
||||
onLayerRemove: _onLayerRemove,
|
||||
onLayerEdit: _onLayerEdit,
|
||||
}: LayerPanelProps) {
|
||||
// TODO: Implement layer panel UI
|
||||
// This will be implemented in Phase 2
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export function MapCanvas({
|
|||
center: viewport.center as [number, number],
|
||||
zoom: viewport.zoom,
|
||||
interactive,
|
||||
attributionControl: true,
|
||||
attributionControl: {},
|
||||
});
|
||||
|
||||
// Add navigation controls
|
||||
|
|
@ -142,8 +142,8 @@ export function MapCanvas({
|
|||
// Handle clicks
|
||||
map.on('click', (e) => {
|
||||
onMapClick?.({
|
||||
latitude: e.lngLat.lat,
|
||||
longitude: e.lngLat.lng,
|
||||
lat: e.lngLat.lat,
|
||||
lng: e.lngLat.lng,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -34,15 +34,15 @@ const DEFAULT_PROFILE_COLORS: Record<RoutingProfile, string> = {
|
|||
};
|
||||
|
||||
export function RouteLayer({
|
||||
routes,
|
||||
selectedRouteId,
|
||||
showAlternatives = true,
|
||||
showElevation = false,
|
||||
onRouteSelect,
|
||||
onRouteEdit,
|
||||
routes: _routes,
|
||||
selectedRouteId: _selectedRouteId,
|
||||
showAlternatives: _showAlternatives = true,
|
||||
showElevation: _showElevation = false,
|
||||
onRouteSelect: _onRouteSelect,
|
||||
onRouteEdit: _onRouteEdit,
|
||||
profileColors = {},
|
||||
}: RouteLayerProps) {
|
||||
const colors = { ...DEFAULT_PROFILE_COLORS, ...profileColors };
|
||||
const _colors = { ...DEFAULT_PROFILE_COLORS, ...profileColors };
|
||||
|
||||
// TODO: Implement route rendering with MapLibre GL JS
|
||||
// This will be implemented in Phase 2
|
||||
|
|
|
|||
|
|
@ -24,16 +24,16 @@ interface WaypointMarkerProps {
|
|||
}
|
||||
|
||||
export function WaypointMarker({
|
||||
waypoint,
|
||||
index,
|
||||
isSelected = false,
|
||||
isDraggable = true,
|
||||
showLabel = true,
|
||||
showTime = false,
|
||||
showBudget = false,
|
||||
onSelect,
|
||||
onDragEnd,
|
||||
onDelete,
|
||||
waypoint: _waypoint,
|
||||
index: _index,
|
||||
isSelected: _isSelected = false,
|
||||
isDraggable: _isDraggable = true,
|
||||
showLabel: _showLabel = true,
|
||||
showTime: _showTime = false,
|
||||
showBudget: _showBudget = false,
|
||||
onSelect: _onSelect,
|
||||
onDragEnd: _onDragEnd,
|
||||
onDelete: _onDelete,
|
||||
}: WaypointMarkerProps) {
|
||||
// TODO: Implement marker rendering with MapLibre GL JS
|
||||
// This will be implemented in Phase 1
|
||||
|
|
|
|||
|
|
@ -609,6 +609,73 @@ export function sampleConeSurface(
|
|||
return points;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Aliases for backwards compatibility
|
||||
// =============================================================================
|
||||
|
||||
/** Alias for addVectors */
|
||||
export const vectorAdd = addVectors;
|
||||
|
||||
/** Alias for subtractVectors */
|
||||
export const vectorSubtract = subtractVectors;
|
||||
|
||||
/** Alias for scaleVector */
|
||||
export const vectorScale = scaleVector;
|
||||
|
||||
/** Alias for dotProduct */
|
||||
export const vectorDot = dotProduct;
|
||||
|
||||
/** Alias for magnitude */
|
||||
export const vectorNorm = magnitude;
|
||||
|
||||
/** Alias for normalize */
|
||||
export const vectorNormalize = normalize;
|
||||
|
||||
/** Alias for crossProduct (3D) */
|
||||
export const vectorCross3D = crossProduct;
|
||||
|
||||
/**
|
||||
* Calculate angle from a reference axis
|
||||
*/
|
||||
export function angleFromAxis(v: SpaceVector, axis: SpaceVector): number {
|
||||
const normV = normalize(v);
|
||||
const normAxis = normalize(axis);
|
||||
const dot = dotProduct(normV, normAxis);
|
||||
return Math.acos(Math.max(-1, Math.min(1, dot)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine two cones by intersection
|
||||
*/
|
||||
export function combineCones(
|
||||
cone1: PossibilityCone,
|
||||
cone2: PossibilityCone
|
||||
): PossibilityCone {
|
||||
// Simplified: use the more restrictive aperture and average axes
|
||||
const avgAxis = normalize(addVectors(cone1.axis, cone2.axis));
|
||||
return createCone({
|
||||
apex: cone1.apex, // Use first cone's apex
|
||||
axis: avgAxis,
|
||||
aperture: Math.min(cone1.aperture, cone2.aperture),
|
||||
direction: cone1.direction,
|
||||
extent: cone1.extent !== null && cone2.extent !== null
|
||||
? Math.min(cone1.extent, cone2.extent)
|
||||
: cone1.extent ?? cone2.extent,
|
||||
constraints: [...cone1.constraints, ...cone2.constraints],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Slice a cone with a plane and return the conic section
|
||||
*/
|
||||
export function sliceConeWithPlane(
|
||||
cone: PossibilityCone,
|
||||
planeNormal: SpaceVector,
|
||||
planeOffset: number
|
||||
): ConicSection {
|
||||
return createConicSection(cone, planeNormal, planeOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a vector orthogonal to the given vector
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -171,8 +171,6 @@ export {
|
|||
export {
|
||||
PathOptimizer,
|
||||
createPathOptimizer,
|
||||
DEFAULT_OPTIMIZER_CONFIG,
|
||||
type OptimizerConfig,
|
||||
} from './optimization';
|
||||
|
||||
// Visualization
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ export class PathOptimizer {
|
|||
// Helper Methods
|
||||
// ===========================================================================
|
||||
|
||||
private createGrid(resolution: number): void {
|
||||
private createGrid(_resolution: number): void {
|
||||
// Grid is implicit - we just use resolution to map points to cells
|
||||
}
|
||||
|
||||
|
|
@ -381,7 +381,7 @@ export class PathOptimizer {
|
|||
if (index === dim) {
|
||||
if (!current.every((v, i) => v === cell[i])) {
|
||||
// Check bounds
|
||||
if (current.every((v, i) => v >= 0 && v < resolution)) {
|
||||
if (current.every((v, _i) => v >= 0 && v < resolution)) {
|
||||
neighbors.push([...current]);
|
||||
}
|
||||
}
|
||||
|
|
@ -401,7 +401,7 @@ export class PathOptimizer {
|
|||
private heuristic(
|
||||
cell: number[],
|
||||
goalCell: number[],
|
||||
resolution: number
|
||||
_resolution: number
|
||||
): number {
|
||||
// Euclidean distance in cell space
|
||||
let sum = 0;
|
||||
|
|
@ -561,7 +561,7 @@ export class PathOptimizer {
|
|||
cameFrom: Map<string, number[]>,
|
||||
goalCell: number[],
|
||||
start: SpacePoint,
|
||||
goal: SpacePoint,
|
||||
_goal: SpacePoint,
|
||||
resolution: number
|
||||
): PossibilityPath {
|
||||
const waypoints: PathWaypoint[] = [];
|
||||
|
|
|
|||
|
|
@ -21,12 +21,15 @@ import type {
|
|||
GameEventListener,
|
||||
} from './types';
|
||||
import { TEMPERATURE_THRESHOLDS } from './types';
|
||||
import type { GeohashCommitment, ProximityProof } from '../privacy/types';
|
||||
import type { GeohashCommitment, ProximityProof, GeohashPrecision } from '../privacy/types';
|
||||
import {
|
||||
createCommitment,
|
||||
verifyCommitment,
|
||||
generateProximityProof,
|
||||
verifyProximityProof,
|
||||
generateSalt,
|
||||
encodeGeohash,
|
||||
decodeBounds,
|
||||
PRECISION_CELL_SIZE,
|
||||
} from '../privacy';
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -101,13 +104,23 @@ export class AnchorManager {
|
|||
const id = `anchor-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
// Create zkGPS commitment for the location
|
||||
const locationCommitment = await createCommitment(
|
||||
params.latitude,
|
||||
params.longitude,
|
||||
12, // Full precision internally
|
||||
params.creatorPubKey,
|
||||
params.creatorPrivKey
|
||||
);
|
||||
const salt = generateSalt();
|
||||
const locationCommitmentBase = await createCommitment({
|
||||
coordinate: { lat: params.latitude, lng: params.longitude },
|
||||
precision: 12 as GeohashPrecision, // Full precision internally
|
||||
salt,
|
||||
});
|
||||
|
||||
// Create GeohashCommitment from LocationCommitment
|
||||
const fullGeohash = encodeGeohash(params.latitude, params.longitude, 12);
|
||||
const locationCommitment: GeohashCommitment = {
|
||||
commitment: locationCommitmentBase.commitment,
|
||||
geohash: fullGeohash,
|
||||
precision: locationCommitmentBase.precision,
|
||||
timestamp: locationCommitmentBase.timestamp,
|
||||
expiresAt: locationCommitmentBase.expiresAt,
|
||||
salt,
|
||||
};
|
||||
|
||||
const anchor: DiscoveryAnchor = {
|
||||
id,
|
||||
|
|
@ -227,22 +240,28 @@ export class AnchorManager {
|
|||
}
|
||||
}
|
||||
|
||||
// Get the anchor target location from geohash
|
||||
const anchorBounds = decodeBounds(anchor.locationCommitment.geohash);
|
||||
const targetPoint = {
|
||||
lat: (anchorBounds.minLat + anchorBounds.maxLat) / 2,
|
||||
lng: (anchorBounds.minLng + anchorBounds.maxLng) / 2,
|
||||
};
|
||||
|
||||
// Convert precision to approximate max distance in meters
|
||||
const cellSize = PRECISION_CELL_SIZE[anchor.requiredPrecision as keyof typeof PRECISION_CELL_SIZE];
|
||||
const maxDistance = cellSize ? Math.max(cellSize.lat, cellSize.lng) : 100;
|
||||
|
||||
// Generate proximity proof
|
||||
const proximityProof = await generateProximityProof(
|
||||
params.playerLatitude,
|
||||
params.playerLongitude,
|
||||
anchor.locationCommitment,
|
||||
anchor.requiredPrecision,
|
||||
params.playerPubKey,
|
||||
params.playerPrivKey
|
||||
{ lat: params.playerLatitude, lng: params.playerLongitude },
|
||||
targetPoint,
|
||||
maxDistance,
|
||||
params.playerPrivKey,
|
||||
params.playerPubKey
|
||||
);
|
||||
|
||||
// Verify proximity
|
||||
const proofValid = await verifyProximityProof(
|
||||
proximityProof,
|
||||
anchor.locationCommitment,
|
||||
params.playerPubKey
|
||||
);
|
||||
const proofValid = await verifyProximityProof(proximityProof);
|
||||
|
||||
if (!proofValid) {
|
||||
return { success: false, error: 'Not close enough to anchor' };
|
||||
|
|
@ -404,7 +423,7 @@ export class AnchorManager {
|
|||
anchorId: string,
|
||||
playerLatitude: number,
|
||||
playerLongitude: number,
|
||||
playerPrecision: number = 7
|
||||
_playerPrecision: number = 7
|
||||
): Promise<NavigationHint | null> {
|
||||
const anchor = this.anchors.get(anchorId);
|
||||
if (!anchor) return null;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import type {
|
|||
GameEvent,
|
||||
GameEventListener,
|
||||
} from './types';
|
||||
import type { GeohashCommitment } from '../privacy/types';
|
||||
import type { GeohashCommitment, GeohashPrecision } from '../privacy/types';
|
||||
import type { MyceliumNode, Hypha, Signal, NodeType, HyphaType } from '../mycelium/types';
|
||||
import { MyceliumNetwork, createMyceliumNetwork } from '../mycelium';
|
||||
|
||||
|
|
@ -225,14 +225,16 @@ export class SporeManager {
|
|||
}
|
||||
|
||||
// Create mycelium node at location
|
||||
const node = this.network.addNode({
|
||||
const position = this.geohashToPosition(params.locationCommitment.geohash);
|
||||
const node = this.network.createNode({
|
||||
type: this.sporeTypeToNodeType(params.spore.type),
|
||||
position: this.geohashToPosition(params.locationCommitment.geohash),
|
||||
strength: params.spore.nutrientCapacity / 100,
|
||||
data: {
|
||||
label: params.spore.id,
|
||||
position: { lat: position.y / 100, lng: position.x / 10000 },
|
||||
metadata: {
|
||||
sporeId: params.spore.id,
|
||||
planterPubKey: params.planterPubKey,
|
||||
sporeType: params.spore.type,
|
||||
strength: params.spore.nutrientCapacity / 100,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -276,21 +278,23 @@ export class SporeManager {
|
|||
const maxRange = Math.min(planted.spore.maxReach, other.spore.maxReach);
|
||||
if (distance <= maxRange) {
|
||||
// Create hypha connection
|
||||
const hypha = this.network.addHypha({
|
||||
const hypha = this.network.createHypha({
|
||||
type: this.getHyphaType(planted.spore.type, other.spore.type),
|
||||
fromId: planted.nodeId,
|
||||
toId: other.nodeId,
|
||||
sourceId: planted.nodeId,
|
||||
targetId: other.nodeId,
|
||||
strength: 0.5,
|
||||
data: {
|
||||
metadata: {
|
||||
plantedSporeIds: [planted.id, other.id],
|
||||
},
|
||||
});
|
||||
|
||||
planted.hyphaIds.push(hypha.id);
|
||||
other.hyphaIds.push(hypha.id);
|
||||
if (hypha) {
|
||||
planted.hyphaIds.push(hypha.id);
|
||||
other.hyphaIds.push(hypha.id);
|
||||
|
||||
// Check for fruiting body conditions
|
||||
this.checkFruitingConditions(planted);
|
||||
// Check for fruiting body conditions
|
||||
this.checkFruitingConditions(planted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -360,11 +364,12 @@ export class SporeManager {
|
|||
|
||||
// Find connections via hyphae
|
||||
for (const hyphaId of spore.hyphaIds) {
|
||||
const hypha = this.network.getHypha(hyphaId);
|
||||
const hyphae = this.network.getNodeHyphae(spore.nodeId);
|
||||
const hypha = hyphae.find((h) => h.id === hyphaId);
|
||||
if (hypha) {
|
||||
// Find the other node
|
||||
const otherNodeId =
|
||||
hypha.fromId === spore.nodeId ? hypha.toId : hypha.fromId;
|
||||
hypha.sourceId === spore.nodeId ? hypha.targetId : hypha.sourceId;
|
||||
|
||||
// Find spore by node ID
|
||||
for (const [id, s] of this.plantedSpores.entries()) {
|
||||
|
|
@ -404,9 +409,10 @@ export class SporeManager {
|
|||
type,
|
||||
locationCommitment: {
|
||||
geohash: centerGeohash,
|
||||
hash: '', // Would be calculated
|
||||
timestamp: now,
|
||||
precision: 7,
|
||||
commitment: '', // Would be calculated
|
||||
timestamp: now.getTime(),
|
||||
expiresAt: now.getTime() + lifespanMinutes * 60 * 1000,
|
||||
precision: 7 as GeohashPrecision,
|
||||
},
|
||||
sourceSporeIds: spores.map((s) => s.id),
|
||||
harvestableRewards: rewards,
|
||||
|
|
@ -420,15 +426,17 @@ export class SporeManager {
|
|||
|
||||
this.emit({ type: 'fruit:emerged', fruit });
|
||||
|
||||
// Notify network
|
||||
this.network.emit({
|
||||
id: `signal-fruit-${fruit.id}`,
|
||||
type: 'discovery',
|
||||
sourceId: spores[0].nodeId,
|
||||
strength: 1,
|
||||
timestamp: now,
|
||||
data: { fruitId: fruit.id, type },
|
||||
});
|
||||
// Emit signal to network
|
||||
this.network.emitSignal(
|
||||
spores[0].nodeId,
|
||||
`fruit-${fruit.id}`,
|
||||
{
|
||||
type: 'discovery',
|
||||
strength: 1,
|
||||
payload: { fruitId: fruit.id, type },
|
||||
ttl: lifespanMinutes * 60 * 1000,
|
||||
}
|
||||
);
|
||||
|
||||
return fruit;
|
||||
}
|
||||
|
|
@ -629,8 +637,7 @@ export class SporeManager {
|
|||
fruit.maturity = Math.min(100, (ageMs / lifespanMs) * 100 * 2); // Mature at 50% lifespan
|
||||
}
|
||||
|
||||
// Update network
|
||||
this.network.propagateSignals(0.9);
|
||||
// Network handles signal propagation internally via maintenance loop
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -640,11 +647,13 @@ export class SporeManager {
|
|||
const growthRate = spore.spore.growthRate * this.config.baseGrowthRate;
|
||||
|
||||
// Try to extend existing hyphae or create new connections
|
||||
for (const hyphaId of spore.hyphaIds) {
|
||||
const hypha = this.network.getHypha(hyphaId);
|
||||
if (hypha) {
|
||||
const hyphae = this.network.getNodeHyphae(spore.nodeId);
|
||||
for (const hypha of hyphae) {
|
||||
if (spore.hyphaIds.includes(hypha.id)) {
|
||||
// Strengthen existing connection
|
||||
hypha.strength = Math.min(1, hypha.strength + growthRate * 0.01);
|
||||
this.network.updateHypha(hypha.id, {
|
||||
strength: Math.min(1, hypha.strength + growthRate * 0.01),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,14 +45,14 @@ interface UseCollaborationReturn {
|
|||
|
||||
export function useCollaboration({
|
||||
sessionId,
|
||||
userId,
|
||||
userName,
|
||||
userColor = '#3B82F6',
|
||||
userId: _userId,
|
||||
userName: _userName,
|
||||
userColor: _userColor = '#3B82F6',
|
||||
serverUrl,
|
||||
onParticipantJoin,
|
||||
onParticipantLeave,
|
||||
onRouteUpdate,
|
||||
onWaypointUpdate,
|
||||
onParticipantJoin: _onParticipantJoin,
|
||||
onParticipantLeave: _onParticipantLeave,
|
||||
onRouteUpdate: _onRouteUpdate,
|
||||
onWaypointUpdate: _onWaypointUpdate,
|
||||
}: UseCollaborationOptions): UseCollaborationReturn {
|
||||
const [session, setSession] = useState<CollaborationSession | null>(null);
|
||||
const [participants, setParticipants] = useState<Participant[]>([]);
|
||||
|
|
@ -95,7 +95,7 @@ export function useCollaboration({
|
|||
setIsConnected(false);
|
||||
}, []);
|
||||
|
||||
const updateCursor = useCallback((coordinate: Coordinate) => {
|
||||
const updateCursor = useCallback((_coordinate: Coordinate) => {
|
||||
// TODO: Broadcast cursor position via Y.js awareness
|
||||
// awareness.setLocalStateField('cursor', coordinate);
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ interface UseMapInstanceReturn {
|
|||
}
|
||||
|
||||
const DEFAULT_VIEWPORT: MapViewport = {
|
||||
center: { lat: 40.7128, lng: -74.006 }, // NYC default
|
||||
center: [-74.006, 40.7128], // [lng, lat] NYC default
|
||||
zoom: 10,
|
||||
bearing: 0,
|
||||
pitch: 0,
|
||||
|
|
@ -183,10 +183,10 @@ export function useMapInstance({
|
|||
const map = new maplibregl.Map({
|
||||
container,
|
||||
style,
|
||||
center: [initialViewport.center.lng, initialViewport.center.lat],
|
||||
center: initialViewport.center,
|
||||
zoom: initialViewport.zoom,
|
||||
bearing: initialViewport.bearing,
|
||||
pitch: initialViewport.pitch,
|
||||
bearing: initialViewport.bearing ?? 0,
|
||||
pitch: initialViewport.pitch ?? 0,
|
||||
interactive,
|
||||
attributionControl: false,
|
||||
maxZoom: config.maxZoom ?? 22,
|
||||
|
|
@ -210,7 +210,7 @@ export function useMapInstance({
|
|||
map.on('move', () => {
|
||||
const center = map.getCenter();
|
||||
const newViewport: MapViewport = {
|
||||
center: { lat: center.lat, lng: center.lng },
|
||||
center: [center.lng, center.lat],
|
||||
zoom: map.getZoom(),
|
||||
bearing: map.getBearing(),
|
||||
pitch: map.getPitch(),
|
||||
|
|
@ -227,7 +227,7 @@ export function useMapInstance({
|
|||
map.on('moveend', () => {
|
||||
const center = map.getCenter();
|
||||
const finalViewport: MapViewport = {
|
||||
center: { lat: center.lat, lng: center.lng },
|
||||
center: [center.lng, center.lat],
|
||||
zoom: map.getZoom(),
|
||||
bearing: map.getBearing(),
|
||||
pitch: map.getPitch(),
|
||||
|
|
@ -269,19 +269,20 @@ export function useMapInstance({
|
|||
const currentPitch = map.getPitch();
|
||||
|
||||
// Only update if significantly different to avoid feedback loops
|
||||
const [lng, lat] = initialViewport.center;
|
||||
const centerChanged =
|
||||
Math.abs(currentCenter.lat - initialViewport.center.lat) > 0.0001 ||
|
||||
Math.abs(currentCenter.lng - initialViewport.center.lng) > 0.0001;
|
||||
Math.abs(currentCenter.lat - lat) > 0.0001 ||
|
||||
Math.abs(currentCenter.lng - lng) > 0.0001;
|
||||
const zoomChanged = Math.abs(currentZoom - initialViewport.zoom) > 0.01;
|
||||
const bearingChanged = Math.abs(currentBearing - initialViewport.bearing) > 0.1;
|
||||
const pitchChanged = Math.abs(currentPitch - initialViewport.pitch) > 0.1;
|
||||
const bearingChanged = Math.abs(currentBearing - (initialViewport.bearing ?? 0)) > 0.1;
|
||||
const pitchChanged = Math.abs(currentPitch - (initialViewport.pitch ?? 0)) > 0.1;
|
||||
|
||||
if (centerChanged || zoomChanged || bearingChanged || pitchChanged) {
|
||||
map.jumpTo({
|
||||
center: [initialViewport.center.lng, initialViewport.center.lat],
|
||||
center: initialViewport.center,
|
||||
zoom: initialViewport.zoom,
|
||||
bearing: initialViewport.bearing,
|
||||
pitch: initialViewport.pitch,
|
||||
bearing: initialViewport.bearing ?? 0,
|
||||
pitch: initialViewport.pitch ?? 0,
|
||||
});
|
||||
}
|
||||
}, [initialViewport, isLoaded]);
|
||||
|
|
@ -293,10 +294,10 @@ export function useMapInstance({
|
|||
|
||||
if (mapRef.current && isLoaded) {
|
||||
mapRef.current.jumpTo({
|
||||
center: [newViewport.center.lng, newViewport.center.lat],
|
||||
center: newViewport.center,
|
||||
zoom: newViewport.zoom,
|
||||
bearing: newViewport.bearing,
|
||||
pitch: newViewport.pitch,
|
||||
bearing: newViewport.bearing ?? 0,
|
||||
pitch: newViewport.pitch ?? 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -82,7 +82,15 @@ export class GPSCollaborationLayer {
|
|||
|
||||
constructor(map: maplibregl.Map, options: GPSLayerOptions = {}) {
|
||||
this.map = map;
|
||||
this.options = { ...DEFAULT_OPTIONS, ...options };
|
||||
// Deep merge markerStyle to ensure all properties exist
|
||||
this.options = {
|
||||
...DEFAULT_OPTIONS,
|
||||
...options,
|
||||
markerStyle: {
|
||||
...DEFAULT_OPTIONS.markerStyle,
|
||||
...options.markerStyle,
|
||||
},
|
||||
};
|
||||
this.injectStyles();
|
||||
}
|
||||
|
||||
|
|
@ -314,7 +322,7 @@ export class GPSCollaborationLayer {
|
|||
const el = document.createElement('div');
|
||||
el.className = `gps-marker ${isCurrentUser ? 'gps-marker-self' : 'gps-marker-peer'}`;
|
||||
|
||||
const { size, borderWidth } = this.options.markerStyle;
|
||||
const { size = 40, borderWidth = 3 } = this.options.markerStyle;
|
||||
const emoji = this.getPersonEmoji(user.userId);
|
||||
|
||||
el.style.cssText = `
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ export function transformIncentive(
|
|||
*/
|
||||
export function transformRelational(
|
||||
point: DataPoint,
|
||||
config: RelationalLensConfig,
|
||||
_config: RelationalLensConfig,
|
||||
viewport: LensState['viewport'],
|
||||
allPoints?: DataPoint[],
|
||||
layoutCache?: Map<string, { x: number; y: number }>
|
||||
|
|
|
|||
|
|
@ -30,10 +30,10 @@ import {
|
|||
getRadiusForPrecision,
|
||||
getPrecisionForTrustLevel,
|
||||
} from './types';
|
||||
import type { TrustLevel, GeohashCommitment } from '../privacy/types';
|
||||
import type { TrustLevel, GeohashCommitment, GeohashPrecision } from '../privacy/types';
|
||||
import { TrustCircleManager, createTrustCircleManager } from '../privacy/trustCircles';
|
||||
import { createCommitment } from '../privacy/commitments';
|
||||
import { encodeGeohash, decodeGeohash, getGeohashBounds } from '../privacy/geohash';
|
||||
import { createCommitment, generateSalt } from '../privacy/commitments';
|
||||
import { encodeGeohash, getGeohashBounds } from '../privacy/geohash';
|
||||
|
||||
// =============================================================================
|
||||
// Presence Manager
|
||||
|
|
@ -60,7 +60,7 @@ export class PresenceManager {
|
|||
...config,
|
||||
};
|
||||
|
||||
this.trustCircles = createTrustCircleManager(this.config.userPubKey);
|
||||
this.trustCircles = createTrustCircleManager(this.config.userPubKey, this.config.userPubKey);
|
||||
|
||||
this.state = {
|
||||
config: this.config,
|
||||
|
|
@ -175,14 +175,23 @@ export class PresenceManager {
|
|||
const isMoving = (coords.speed ?? 0) > 0.5; // > 0.5 m/s = moving
|
||||
|
||||
// Create zkGPS commitment for the location
|
||||
const geohash = encodeGeohash(coords.latitude, coords.longitude, 12);
|
||||
const commitment = await createCommitment(
|
||||
coords.latitude,
|
||||
coords.longitude,
|
||||
12,
|
||||
this.config.userPubKey,
|
||||
this.config.userPrivKey
|
||||
);
|
||||
const fullGeohash = encodeGeohash(coords.latitude, coords.longitude, 12);
|
||||
const salt = generateSalt();
|
||||
const baseCommitment = await createCommitment({
|
||||
coordinate: { lat: coords.latitude, lng: coords.longitude },
|
||||
precision: 12 as GeohashPrecision,
|
||||
salt,
|
||||
});
|
||||
|
||||
// Create GeohashCommitment from LocationCommitment
|
||||
const commitment: GeohashCommitment = {
|
||||
commitment: baseCommitment.commitment,
|
||||
geohash: fullGeohash,
|
||||
precision: baseCommitment.precision,
|
||||
timestamp: baseCommitment.timestamp,
|
||||
expiresAt: baseCommitment.expiresAt,
|
||||
salt,
|
||||
};
|
||||
|
||||
// Update self location
|
||||
this.state.self.location = {
|
||||
|
|
@ -223,13 +232,23 @@ export class PresenceManager {
|
|||
longitude: number,
|
||||
source: LocationSource = 'manual'
|
||||
): Promise<void> {
|
||||
const commitment = await createCommitment(
|
||||
latitude,
|
||||
longitude,
|
||||
12,
|
||||
this.config.userPubKey,
|
||||
this.config.userPrivKey
|
||||
);
|
||||
const salt = generateSalt();
|
||||
const fullGeohash = encodeGeohash(latitude, longitude, 12);
|
||||
const baseCommitment = await createCommitment({
|
||||
coordinate: { lat: latitude, lng: longitude },
|
||||
precision: 12 as GeohashPrecision,
|
||||
salt,
|
||||
});
|
||||
|
||||
// Create GeohashCommitment from LocationCommitment
|
||||
const commitment: GeohashCommitment = {
|
||||
commitment: baseCommitment.commitment,
|
||||
geohash: fullGeohash,
|
||||
precision: baseCommitment.precision,
|
||||
timestamp: baseCommitment.timestamp,
|
||||
expiresAt: baseCommitment.expiresAt,
|
||||
salt,
|
||||
};
|
||||
|
||||
this.state.self.location = {
|
||||
coordinates: {
|
||||
|
|
@ -528,7 +547,7 @@ export class PresenceManager {
|
|||
longitude: (bounds.minLng + bounds.maxLng) / 2,
|
||||
};
|
||||
|
||||
const ageSeconds = (Date.now() - payload.commitment.timestamp.getTime()) / 1000;
|
||||
const ageSeconds = (Date.now() - payload.commitment.timestamp) / 1000;
|
||||
|
||||
location = {
|
||||
geohash,
|
||||
|
|
|
|||
|
|
@ -427,3 +427,22 @@ export function precisionForRadius(radiusMeters: number): number {
|
|||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Convenience Aliases
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Alias for encode() - encode latitude/longitude to geohash
|
||||
*/
|
||||
export const encodeGeohash = encode;
|
||||
|
||||
/**
|
||||
* Alias for decode() - decode geohash to latitude/longitude
|
||||
*/
|
||||
export const decodeGeohash = decode;
|
||||
|
||||
/**
|
||||
* Alias for decodeBounds() - get bounding box for a geohash
|
||||
*/
|
||||
export const getGeohashBounds = decodeBounds;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export {
|
|||
encode as encodeGeohash,
|
||||
decode as decodeGeohash,
|
||||
decodeBounds,
|
||||
decodeBounds as getGeohashBounds,
|
||||
neighbors,
|
||||
contains,
|
||||
cellsInRadius,
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export class TrustCircleManager {
|
|||
customPrecision: params.customPrecision,
|
||||
members: [],
|
||||
updateInterval: params.updateInterval ?? this.getDefaultInterval(params.level),
|
||||
requireMutual: params.requireMutual ?? params.level === 'intimate' || params.level === 'close',
|
||||
requireMutual: params.requireMutual ?? (params.level === 'intimate' || params.level === 'close'),
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
|
|
@ -329,6 +329,47 @@ export class TrustCircleManager {
|
|||
return TRUST_LEVEL_PRECISION[level];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trust level for a contact based on their highest circle membership
|
||||
*/
|
||||
getTrustLevel(contactId: string): TrustLevel | null {
|
||||
const precision = this.getPrecisionForContact(contactId);
|
||||
if (precision === null) return null;
|
||||
return getTrustLevelFromPrecision(precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set trust level for a contact by adding them to the appropriate circle
|
||||
*/
|
||||
setTrustLevel(contactId: string, level: TrustLevel): void {
|
||||
// Remove from all current circles first
|
||||
for (const circle of this.circles.values()) {
|
||||
if (circle.members.includes(contactId)) {
|
||||
this.removeFromCircle(circle.id, contactId);
|
||||
}
|
||||
}
|
||||
|
||||
// Find or create a circle at this level
|
||||
let targetCircle: TrustCircle | undefined;
|
||||
for (const circle of this.circles.values()) {
|
||||
if (circle.level === level) {
|
||||
targetCircle = circle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetCircle) {
|
||||
// Create a new circle at this level
|
||||
targetCircle = this.createCircle({
|
||||
name: `${level.charAt(0).toUpperCase() + level.slice(1)} Circle`,
|
||||
level,
|
||||
});
|
||||
}
|
||||
|
||||
// Add to the circle
|
||||
this.addToCircle(targetCircle.id, contactId);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Broadcast Helpers
|
||||
// ===========================================================================
|
||||
|
|
@ -359,7 +400,7 @@ export class TrustCircleManager {
|
|||
* Returns a map of circleId -> encrypted commitment
|
||||
*/
|
||||
async createCircleCommitments(
|
||||
coordinate: { lat: number; lng: number },
|
||||
_coordinate: { lat: number; lng: number },
|
||||
createCommitmentFn: (precision: GeohashPrecision) => Promise<LocationCommitment>
|
||||
): Promise<Map<string, { commitment: LocationCommitment; precision: GeohashPrecision }>> {
|
||||
const commitments = new Map<
|
||||
|
|
|
|||
|
|
@ -4,7 +4,31 @@
|
|||
* Types for privacy-preserving location sharing protocol
|
||||
*/
|
||||
|
||||
import type { GeohashPrecision } from './geohash';
|
||||
import type { GeohashPrecision as _GeohashPrecision } from './geohash';
|
||||
|
||||
// Re-export GeohashPrecision for convenience
|
||||
export type GeohashPrecision = _GeohashPrecision;
|
||||
export { GEOHASH_PRECISION } from './geohash';
|
||||
export type { GeohashBounds } from './geohash';
|
||||
|
||||
/**
|
||||
* A geohash-based location commitment (alias for LocationCommitment)
|
||||
* Used in discovery and presence systems
|
||||
*/
|
||||
export interface GeohashCommitment {
|
||||
/** The commitment hash */
|
||||
commitment: string;
|
||||
/** The geohash prefix that is revealed */
|
||||
geohash: string;
|
||||
/** Precision level (1-12) */
|
||||
precision: GeohashPrecision;
|
||||
/** When this commitment was created */
|
||||
timestamp: number;
|
||||
/** When this commitment expires */
|
||||
expiresAt: number;
|
||||
/** Salt used in the commitment (for verification) */
|
||||
salt?: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Core Location Types
|
||||
|
|
|
|||
|
|
@ -50,9 +50,9 @@ export class OptimizationService {
|
|||
const vehicles = [{ id: 0, start: [waypoints[0].coordinate.lng, waypoints[0].coordinate.lat] }];
|
||||
try {
|
||||
const res = await fetch(this.config.baseUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jobs, vehicles }) });
|
||||
const data = await res.json();
|
||||
const data = await res.json() as { code: number; error?: string; routes: { steps: { type: string; job: number }[] }[]; summary: { distance: number; duration: number } };
|
||||
if (data.code !== 0) throw new Error(data.error);
|
||||
const indices = data.routes[0].steps.filter((s: any) => s.type === 'job').map((s: any) => s.job);
|
||||
const indices = data.routes[0].steps.filter((s) => s.type === 'job').map((s) => s.job);
|
||||
return { orderedWaypoints: indices.map((i: number) => waypoints[i]), totalDistance: data.summary.distance, totalDuration: data.summary.duration, estimatedCost: this.estimateCosts(data.summary.distance, data.summary.duration) };
|
||||
} catch { return this.nearestNeighbor(waypoints); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,9 +34,9 @@ export class RoutingService {
|
|||
const url = `${this.config.baseUrl}/trip/v1/driving/${coords}?roundtrip=false&source=first&destination=last`;
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
const data = await res.json() as { code: string; waypoints: { waypoint_index: number }[] };
|
||||
if (data.code !== 'Ok') return waypoints;
|
||||
return data.waypoints.map((wp: { waypoint_index: number }) => waypoints[wp.waypoint_index]);
|
||||
return data.waypoints.map((wp) => waypoints[wp.waypoint_index]);
|
||||
} catch { return waypoints; }
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ export class RoutingService {
|
|||
url.searchParams.set('steps', 'true');
|
||||
if (options?.alternatives) url.searchParams.set('alternatives', 'true');
|
||||
const res = await fetch(url.toString());
|
||||
const data = await res.json();
|
||||
const data = await res.json() as { code: string; message?: string; routes: unknown[] };
|
||||
if (data.code !== 'Ok') throw new Error(`OSRM error: ${data.message || data.code}`);
|
||||
return this.parseOSRMResponse(data, profile);
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ export class RoutingService {
|
|||
const costing = profile === 'bicycle' ? 'bicycle' : profile === 'foot' ? 'pedestrian' : 'auto';
|
||||
const body = { locations: coords.map((c) => ({ lat: c.lat, lon: c.lng })), costing, alternates: options?.alternatives ?? 0 };
|
||||
const res = await fetch(`${this.config.baseUrl}/route`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||
const data = await res.json();
|
||||
const data = await res.json() as { error?: string };
|
||||
if (data.error) throw new Error(`Valhalla error: ${data.error}`);
|
||||
return this.parseValhallaResponse(data, profile);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,10 +221,11 @@ export interface ParticipantPermissions {
|
|||
}
|
||||
|
||||
export interface MapViewport {
|
||||
center: Coordinate;
|
||||
/** Center as [lng, lat] tuple for MapLibre GL JS compatibility */
|
||||
center: [number, number];
|
||||
zoom: number;
|
||||
bearing: number;
|
||||
pitch: number;
|
||||
bearing?: number;
|
||||
pitch?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -26,8 +26,5 @@
|
|||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src", "worker", "src/client"],
|
||||
"exclude": [
|
||||
"src/open-mapping/**"
|
||||
],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,13 @@ export default defineConfig(({ mode }) => {
|
|||
host: wslIp,
|
||||
port: 5173,
|
||||
},
|
||||
// Proxy API requests to the worker server
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:5172',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
sourcemap: false, // Disable sourcemaps in production to reduce bundle size
|
||||
|
|
|
|||
Loading…
Reference in New Issue