Add POI feature and imporve the overall backend implementation
This commit is contained in:
247
frontend/src/types/poi.ts
Normal file
247
frontend/src/types/poi.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* POI (Point of Interest) Type Definitions
|
||||
*
|
||||
* Types for POI detection, context, and visualization
|
||||
*/
|
||||
|
||||
export interface POILocation {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface POI {
|
||||
osm_id: string;
|
||||
type: 'node' | 'way';
|
||||
lat: number;
|
||||
lon: number;
|
||||
tags: Record<string, string>;
|
||||
name: string;
|
||||
distance_m?: number;
|
||||
zone?: string;
|
||||
}
|
||||
|
||||
export interface POIFeatures {
|
||||
proximity_score: number;
|
||||
weighted_proximity_score: number;
|
||||
count_0_100m: number;
|
||||
count_100_300m: number;
|
||||
count_300_500m: number;
|
||||
count_500_1000m: number;
|
||||
total_count: number;
|
||||
distance_to_nearest_m: number;
|
||||
has_within_100m: boolean;
|
||||
has_within_300m: boolean;
|
||||
has_within_500m: boolean;
|
||||
}
|
||||
|
||||
export interface POICategoryData {
|
||||
pois: POI[];
|
||||
features: POIFeatures;
|
||||
count: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface POICategories {
|
||||
schools: POICategoryData;
|
||||
offices: POICategoryData;
|
||||
gyms_sports: POICategoryData;
|
||||
residential: POICategoryData;
|
||||
tourism: POICategoryData;
|
||||
competitors: POICategoryData;
|
||||
transport_hubs: POICategoryData;
|
||||
coworking: POICategoryData;
|
||||
retail: POICategoryData;
|
||||
}
|
||||
|
||||
export interface POISummary {
|
||||
total_pois_detected: number;
|
||||
categories_with_pois: string[];
|
||||
high_impact_categories: string[];
|
||||
categories_count: number;
|
||||
}
|
||||
|
||||
export interface CompetitorAnalysis {
|
||||
competitive_pressure_score: number;
|
||||
direct_competitors_count: number;
|
||||
nearby_competitors_count: number;
|
||||
market_competitors_count: number;
|
||||
total_competitors_count: number;
|
||||
competitive_zone: 'low_competition' | 'moderate_competition' | 'high_competition';
|
||||
market_type: 'underserved' | 'normal_market' | 'competitive_market' | 'bakery_district';
|
||||
competitive_advantage: 'first_mover' | 'local_leader' | 'quality_focused' | 'differentiation_required';
|
||||
competitor_details: POI[];
|
||||
nearest_competitor: POI | null;
|
||||
}
|
||||
|
||||
export interface POIContext {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
location: POILocation;
|
||||
poi_detection_results: POICategories;
|
||||
ml_features: Record<string, number>;
|
||||
total_pois_detected: number;
|
||||
high_impact_categories: string[];
|
||||
relevant_categories: string[];
|
||||
detection_timestamp: string;
|
||||
detection_source: string;
|
||||
detection_status: 'completed' | 'partial' | 'failed';
|
||||
detection_error?: string;
|
||||
next_refresh_date?: string;
|
||||
last_refreshed_at?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface RelevanceReportItem {
|
||||
category: string;
|
||||
relevant: boolean;
|
||||
reason: string;
|
||||
proximity_score: number;
|
||||
count: number;
|
||||
distance_to_nearest_m: number;
|
||||
}
|
||||
|
||||
export interface POIDetectionResponse {
|
||||
status: 'success' | 'error';
|
||||
source: 'detection' | 'cache';
|
||||
poi_context: POIContext;
|
||||
feature_selection?: {
|
||||
features: Record<string, number>;
|
||||
relevant_categories: string[];
|
||||
relevance_report: RelevanceReportItem[];
|
||||
total_features: number;
|
||||
total_relevant_categories: number;
|
||||
};
|
||||
competitor_analysis?: CompetitorAnalysis;
|
||||
competitive_insights?: string[];
|
||||
}
|
||||
|
||||
export interface POIContextResponse {
|
||||
poi_context: POIContext;
|
||||
is_stale: boolean;
|
||||
needs_refresh: boolean;
|
||||
}
|
||||
|
||||
export interface FeatureImportanceItem {
|
||||
category: string;
|
||||
is_relevant: boolean;
|
||||
proximity_score: number;
|
||||
weighted_score: number;
|
||||
total_count: number;
|
||||
distance_to_nearest_m: number;
|
||||
has_within_100m: boolean;
|
||||
rejection_reason?: string;
|
||||
}
|
||||
|
||||
export interface FeatureImportanceResponse {
|
||||
tenant_id: string;
|
||||
feature_importance: FeatureImportanceItem[];
|
||||
total_categories: number;
|
||||
relevant_categories: number;
|
||||
}
|
||||
|
||||
export interface POICacheStats {
|
||||
total_cached_locations: number;
|
||||
cache_ttl_days: number;
|
||||
coordinate_precision: number;
|
||||
}
|
||||
|
||||
// Category metadata for UI display
|
||||
export interface POICategoryMetadata {
|
||||
name: string;
|
||||
displayName: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const POI_CATEGORY_METADATA: Record<string, POICategoryMetadata> = {
|
||||
schools: {
|
||||
name: 'schools',
|
||||
displayName: 'Schools',
|
||||
icon: '🏫',
|
||||
color: '#22c55e',
|
||||
description: 'Educational institutions causing morning/afternoon rush patterns'
|
||||
},
|
||||
offices: {
|
||||
name: 'offices',
|
||||
displayName: 'Offices',
|
||||
icon: '🏢',
|
||||
color: '#3b82f6',
|
||||
description: 'Office buildings and business centers'
|
||||
},
|
||||
gyms_sports: {
|
||||
name: 'gyms_sports',
|
||||
displayName: 'Gyms & Sports',
|
||||
icon: '🏋️',
|
||||
color: '#8b5cf6',
|
||||
description: 'Fitness centers and sports facilities'
|
||||
},
|
||||
residential: {
|
||||
name: 'residential',
|
||||
displayName: 'Residential',
|
||||
icon: '🏘️',
|
||||
color: '#64748b',
|
||||
description: 'Residential buildings and housing'
|
||||
},
|
||||
tourism: {
|
||||
name: 'tourism',
|
||||
displayName: 'Tourism',
|
||||
icon: '🗼',
|
||||
color: '#f59e0b',
|
||||
description: 'Tourist attractions, hotels, and points of interest'
|
||||
},
|
||||
competitors: {
|
||||
name: 'competitors',
|
||||
displayName: 'Competitors',
|
||||
icon: '🥖',
|
||||
color: '#ef4444',
|
||||
description: 'Competing bakeries and pastry shops'
|
||||
},
|
||||
transport_hubs: {
|
||||
name: 'transport_hubs',
|
||||
displayName: 'Transport Hubs',
|
||||
icon: '🚇',
|
||||
color: '#06b6d4',
|
||||
description: 'Public transport stations and hubs'
|
||||
},
|
||||
coworking: {
|
||||
name: 'coworking',
|
||||
displayName: 'Coworking',
|
||||
icon: '💼',
|
||||
color: '#84cc16',
|
||||
description: 'Coworking spaces and shared offices'
|
||||
},
|
||||
retail: {
|
||||
name: 'retail',
|
||||
displayName: 'Retail',
|
||||
icon: '🛍️',
|
||||
color: '#ec4899',
|
||||
description: 'Retail shops and commercial areas'
|
||||
}
|
||||
};
|
||||
|
||||
export const IMPACT_LEVELS = {
|
||||
HIGH: { label: 'High Impact', color: '#22c55e', threshold: 2.0 },
|
||||
MODERATE: { label: 'Moderate Impact', color: '#f59e0b', threshold: 1.0 },
|
||||
LOW: { label: 'Low Impact', color: '#64748b', threshold: 0 }
|
||||
} as const;
|
||||
|
||||
export type ImpactLevel = keyof typeof IMPACT_LEVELS;
|
||||
|
||||
export function getImpactLevel(proximityScore: number): ImpactLevel {
|
||||
if (proximityScore >= IMPACT_LEVELS.HIGH.threshold) return 'HIGH';
|
||||
if (proximityScore >= IMPACT_LEVELS.MODERATE.threshold) return 'MODERATE';
|
||||
return 'LOW';
|
||||
}
|
||||
|
||||
export function formatDistance(meters: number): string {
|
||||
if (meters < 1000) {
|
||||
return `${Math.round(meters)}m`;
|
||||
}
|
||||
return `${(meters / 1000).toFixed(1)}km`;
|
||||
}
|
||||
|
||||
export function formatCategoryName(category: string): string {
|
||||
return POI_CATEGORY_METADATA[category]?.displayName || category.replace(/_/g, ' ');
|
||||
}
|
||||
Reference in New Issue
Block a user