Compare commits

...
8 Commits
488 changed files with 303283 additions and 2440 deletions
+4
View File
@@ -44,3 +44,7 @@ PAYMOB_IFRAME_ID=837992
BINANCE_PAY_API_KEY="المفتاح_الخاص_بك_هنا"
BINANCE_PAY_SECRET_KEY="المفتاح_السري_الخاص_بك_هنا"
# Google Gemini AI API Key (For Tactical AI Strategic Advisor & Fuel Pricing Intelligence)
GEMINI_API_KEY=
+4
View File
@@ -1,8 +1,12 @@
# Database Dumps & Archives
*.sql
*.tar
*.tar.gz
*.zip
*.mbtiles
*.db
*.db-*
*.db.gz
# Node modules and Web build output
**/node_modules/
+3 -2
View File
@@ -13,8 +13,9 @@ Pods/
*.aab
*.ipa
dem_tiles/
infrastructure/osm-data/
osm-data/
infrastructure/osm-data/*.osm.pbf
infrastructure/osm-data/dem_tiles/
infrastructure/osm-data/valhalla-work/
venv/
.venv*/
dist/
+6
View File
@@ -12,6 +12,9 @@ import { BillingModule } from './billing/billing.module';
import { MailModule } from './common/mail.module';
import { WeatherModule } from './weather/weather.module';
import { TacticalModule } from './tactical/tactical.module';
import { TelemetryModule } from './telemetry/telemetry.module';
import { HeritageModule } from './heritage/heritage.module';
import { CommunityModule } from './community/community.module';
import { UsageInterceptor } from './usage/usage.interceptor';
@Module({
@@ -42,6 +45,9 @@ import { UsageInterceptor } from './usage/usage.interceptor';
MailModule,
WeatherModule,
TacticalModule,
TelemetryModule,
HeritageModule,
CommunityModule,
],
controllers: [],
providers: [
+2 -1
View File
@@ -6,6 +6,7 @@ import { ApiKey } from './entities/api-key.entity';
import { RedisModule } from '../common/redis.module';
import { ConfigService } from '@nestjs/config';
import { TenantController } from './tenant.controller';
import { DeviceAuthController } from './device-auth.controller';
import { FirebaseAdminService } from './firebase-admin.service';
import { FirebaseAuthGuard } from './guards/firebase-auth.guard';
@@ -15,7 +16,7 @@ import { FirebaseAuthGuard } from './guards/firebase-auth.guard';
TypeOrmModule.forFeature([Tenant, ApiKey]),
RedisModule,
],
controllers: [TenantController],
controllers: [TenantController, DeviceAuthController],
providers: [AuthService, FirebaseAdminService, FirebaseAuthGuard],
exports: [AuthService, FirebaseAdminService, FirebaseAuthGuard],
})
+76 -2
View File
@@ -1,9 +1,9 @@
import { Injectable, UnauthorizedException, Logger, NotFoundException, ConflictException, ForbiddenException } from '@nestjs/common';
import { Injectable, UnauthorizedException, Logger, NotFoundException, ConflictException, ForbiddenException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { createHash } from 'crypto';
import { ApiKey } from './entities/api-key.entity';
import { Tenant, TenantPlan, RATE_LIMITS } from './entities/tenant.entity';
import { Tenant, TenantPlan, TenantRole, RATE_LIMITS } from './entities/tenant.entity';
import { RedisService } from '../common/redis.service';
@Injectable()
@@ -227,4 +227,78 @@ export class AuthService {
return tenant;
}
/**
* Provision or fetch a dedicated API key for a mobile device based on its hardware fingerprint.
* Creates an isolated consumer tenant and a dedicated API key with consumer rate limits.
*/
async getOrProvisionDeviceKey(dto: {
deviceFingerprint: string;
hardwareId?: string;
brand?: string;
model?: string;
platform?: string;
osVersion?: string;
appVersion?: string;
}): Promise<{
apiKey: string;
keyName: string;
rateLimit: number;
plan: string;
deviceFingerprint: string;
isNew: boolean;
}> {
if (!dto.deviceFingerprint || dto.deviceFingerprint.trim().length < 8) {
throw new BadRequestException('Invalid device fingerprint: minimum length is 8 characters');
}
const cleanFingerprint = dto.deviceFingerprint.trim();
const fpHash = createHash('sha256').update(cleanFingerprint).digest('hex').substring(0, 24);
const email = `device_${fpHash}@device.siromaps.internal`;
let tenant = await this.tenantRepository.findOne({ where: { email } });
let isNew = false;
if (!tenant) {
const deviceLabel = [dto.brand, dto.model].filter(Boolean).join(' ') || (dto.platform ? `${dto.platform} device` : 'Mobile Device');
tenant = await this.tenantRepository.save({
name: `Siro User (${deviceLabel})`,
email,
plan: TenantPlan.FREE,
role: TenantRole.USER,
isActive: true,
});
isNew = true;
this.logger.log(`📱 [DeviceAuth] Created consumer tenant for device: ${cleanFingerprint.substring(0, 16)}... (${deviceLabel})`);
}
let apiKey = await this.apiKeyRepository.findOne({
where: { tenantId: tenant.id, isActive: true },
order: { createdAt: 'DESC' },
});
if (!apiKey) {
const keyString = `in_mob_${createHash('sha256').update(tenant.id + cleanFingerprint + 'siro_mobile_key_seed').digest('hex').substring(0, 28)}`;
apiKey = await this.apiKeyRepository.save({
key: keyString,
secretHash: this.hashSecret(keyString),
name: `Siro Mobile - ${dto.model || dto.platform || 'Client'}`,
tenantId: tenant.id,
rateLimit: 60, // 60 requests per minute for consumer navigation
isActive: true,
});
isNew = true;
this.logger.log(`🔑 [DeviceAuth] Provisioned dedicated API key ${apiKey.key} for tenant ${tenant.id}`);
}
return {
apiKey: apiKey.key,
keyName: apiKey.name,
rateLimit: apiKey.rateLimit || 60,
plan: tenant.plan,
deviceFingerprint: cleanFingerprint,
isNew,
};
}
}
@@ -0,0 +1,71 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DeviceAuthController } from './device-auth.controller';
import { AuthService } from './auth.service';
describe('DeviceAuthController', () => {
let controller: DeviceAuthController;
let authService: AuthService;
const mockAuthService = {
getOrProvisionDeviceKey: jest.fn().mockImplementation((dto) =>
Promise.resolve({
apiKey: 'in_mob_9876543210abcdef12345678',
keyName: `Siro Mobile - ${dto.model || 'Client'}`,
rateLimit: 60,
plan: 'FREE',
deviceFingerprint: dto.deviceFingerprint,
isNew: true,
}),
),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [DeviceAuthController],
providers: [
{
provide: AuthService,
useValue: mockAuthService,
},
],
}).compile();
controller = module.get<DeviceAuthController>(DeviceAuthController);
authService = module.get<AuthService>(AuthService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
it('should provision a new dedicated mobile key for a device fingerprint', async () => {
const dto = {
deviceFingerprint: 'siro_android_hw_a1b2c3d4e5f6g7h8',
brand: 'Samsung',
model: 'Galaxy S23',
platform: 'android',
osVersion: 'Android 14',
};
const res = await controller.provisionDeviceKey(dto);
expect(authService.getOrProvisionDeviceKey).toHaveBeenCalledWith(dto);
expect(res.apiKey).toBe('in_mob_9876543210abcdef12345678');
expect(res.rateLimit).toBe(60);
expect(res.deviceFingerprint).toBe('siro_android_hw_a1b2c3d4e5f6g7h8');
});
it('should support device-key alias endpoint', async () => {
const dto = {
deviceFingerprint: 'siro_ios_hw_1122334455667788',
brand: 'Apple',
model: 'iPhone 15 Pro',
platform: 'ios',
};
const res = await controller.getDeviceKey(dto);
expect(authService.getOrProvisionDeviceKey).toHaveBeenCalledWith(dto);
expect(res.apiKey).toBeDefined();
});
});
@@ -0,0 +1,28 @@
import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { ProvisionDeviceKeyDto } from './dto/provision-device-key.dto';
@ApiTags('auth')
@Controller('auth')
export class DeviceAuthController {
constructor(private readonly authService: AuthService) {}
@Post('device-provision')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Provision or retrieve a dedicated API key for a mobile device via hardware fingerprint',
description: 'Enables completely frictionless, zero-OTP mobile onboarding by issuing an isolated consumer API key tied directly to physical device telemetry.',
})
@ApiResponse({ status: 200, description: 'Dedicated device API key issued successfully' })
async provisionDeviceKey(@Body() dto: ProvisionDeviceKeyDto) {
return this.authService.getOrProvisionDeviceKey(dto);
}
@Post('device-key')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Intuitive alias for device-provision' })
async getDeviceKey(@Body() dto: ProvisionDeviceKeyDto) {
return this.authService.getOrProvisionDeviceKey(dto);
}
}
@@ -0,0 +1,39 @@
import { IsString, IsOptional, MinLength } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class ProvisionDeviceKeyDto {
@ApiProperty({ description: 'Hardware-derived device fingerprint', example: 'siro_android_hw_9a8b7c6d5e4f' })
@IsString()
@MinLength(8)
deviceFingerprint: string;
@ApiPropertyOptional({ description: 'Unique hardware identifier if available' })
@IsOptional()
@IsString()
hardwareId?: string;
@ApiPropertyOptional({ description: 'Device brand (e.g. Samsung, Apple, Xiaomi)' })
@IsOptional()
@IsString()
brand?: string;
@ApiPropertyOptional({ description: 'Device model (e.g. SM-S908B, iPhone14,2)' })
@IsOptional()
@IsString()
model?: string;
@ApiPropertyOptional({ description: 'Operating system platform (android, ios, macos)' })
@IsOptional()
@IsString()
platform?: string;
@ApiPropertyOptional({ description: 'OS version' })
@IsOptional()
@IsString()
osVersion?: string;
@ApiPropertyOptional({ description: 'App version' })
@IsOptional()
@IsString()
appVersion?: string;
}
@@ -0,0 +1,30 @@
import { Controller, Post, Get, Body, Param, Query, UseGuards } from '@nestjs/common';
import { CommunityService } from './community.service';
import { SubmitContributionDto } from './dto/submit-contribution.dto';
import { ApiKeyGuard } from '../common/guards/api-key.guard';
@Controller('v1/community')
@UseGuards(ApiKeyGuard)
export class CommunityController {
constructor(private readonly communityService: CommunityService) {}
@Post('contribute')
async submitContribution(@Body() dto: SubmitContributionDto) {
return this.communityService.submitContribution(dto);
}
@Get('profile/:userId')
async getProfile(@Param('userId') userId: string, @Query('name') displayName?: string) {
return this.communityService.getOrCreateProfile(userId, displayName);
}
@Get('leaderboard')
async getLeaderboard(@Query('limit') limit?: string) {
return this.communityService.getLeaderboard(limit ? parseInt(limit, 10) : 20);
}
@Get('badges')
async getBadges() {
return this.communityService.getBadges();
}
}
@@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { GuideProfile } from './entities/guide-profile.entity';
import { Contribution } from './entities/contribution.entity';
import { Badge } from './entities/badge.entity';
import { GamificationService } from './gamification.service';
import { CommunityService } from './community.service';
import { CommunityController } from './community.controller';
import { ModerationController } from './moderation.controller';
@Module({
imports: [
TypeOrmModule.forFeature([GuideProfile, Contribution, Badge]),
],
controllers: [CommunityController, ModerationController],
providers: [GamificationService, CommunityService],
exports: [CommunityService, GamificationService],
})
export class CommunityModule {}
+275
View File
@@ -0,0 +1,275 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { GuideProfile } from './entities/guide-profile.entity';
import { Contribution } from './entities/contribution.entity';
import { Badge } from './entities/badge.entity';
import { GamificationService } from './gamification.service';
import { SubmitContributionDto } from './dto/submit-contribution.dto';
import { ReviewContributionDto } from './dto/review-contribution.dto';
export { SubmitContributionDto, ReviewContributionDto };
@Injectable()
export class CommunityService {
constructor(
@InjectRepository(GuideProfile)
private readonly guideRepo: Repository<GuideProfile>,
@InjectRepository(Contribution)
private readonly contributionRepo: Repository<Contribution>,
@InjectRepository(Badge)
private readonly badgeRepo: Repository<Badge>,
private readonly gamificationService: GamificationService,
private readonly dataSource: DataSource,
) {}
/**
* Get or create a Local Guide profile by userId
*/
async getOrCreateProfile(userId: string, displayName?: string): Promise<GuideProfile> {
let profile = await this.guideRepo.findOne({ where: { user_id: userId } });
if (!profile) {
profile = this.guideRepo.create({
user_id: userId,
display_name: displayName || `مرشد_${userId.substring(0, 6)}`,
reputation_score: 10,
total_points: 0,
guide_level: 1,
badge_title: 'مرشد مبتدئ',
});
await this.guideRepo.save(profile);
}
return profile;
}
/**
* Submit a new contribution into the moderation queue
*/
async submitContribution(dto: SubmitContributionDto) {
const guide = await this.getOrCreateProfile(dto.userId, dto.displayName);
const result = await this.dataSource.query(
`INSERT INTO community.contributions
(guide_id, contribution_type, target_table, target_id, place_name, suggested_data, suggested_geom, proof_image_url, notes, status)
VALUES
($1, $2, $3, $4, $5, $6, ST_SetSRID(ST_MakePoint($7, $8), 4326), $9, $10, 'PENDING')
RETURNING id, status, created_at;`,
[
guide.id,
dto.contributionType,
dto.targetTable || 'heritage.landmarks',
dto.targetId || null,
dto.placeName || null,
JSON.stringify(dto.suggestedData || {}),
dto.lng,
dto.lat,
dto.proofImageUrl || null,
dto.notes || null,
],
);
return {
success: true,
message: 'تم إرسال المساهمة بنجاح وهي قيد المراجعة والتدقيق الإداري.',
contributionId: result[0].id,
status: result[0].status,
};
}
/**
* Get pending contributions for manual review
*/
async getPendingContributions(limit = 50) {
const rows = await this.dataSource.query(
`SELECT
c.id, c.contribution_type, c.target_table, c.target_id, c.place_name,
c.suggested_data, c.proof_image_url, c.notes, c.status, c.created_at,
ST_X(c.suggested_geom) AS lng,
ST_Y(c.suggested_geom) AS lat,
g.id AS guide_id, g.display_name AS guide_name, g.total_points AS guide_points, g.guide_level
FROM community.contributions c
LEFT JOIN community.guides_profiles g ON g.id = c.guide_id
WHERE c.status = 'PENDING'
ORDER BY c.created_at ASC
LIMIT $1;`,
[limit],
);
return rows.map((r: any) => ({
id: r.id,
contribution_type: r.contribution_type,
target_table: r.target_table,
target_id: r.target_id,
place_name: r.place_name,
suggested_data: r.suggested_data,
coordinates: { lat: parseFloat(r.lat), lng: parseFloat(r.lng) },
proof_image_url: r.proof_image_url,
notes: r.notes,
created_at: r.created_at,
guide: {
id: r.guide_id,
display_name: r.guide_name,
total_points: r.guide_points,
guide_level: r.guide_level,
},
}));
}
/**
* Approve or reject a contribution in the moderation queue.
* Wrapped in a transaction for atomicity — all or nothing.
*/
async reviewContribution(dto: ReviewContributionDto) {
const contribution = await this.contributionRepo.findOne({
where: { id: dto.contributionId },
relations: ['guide'],
});
if (!contribution) {
throw new NotFoundException(`Contribution #${dto.contributionId} not found.`);
}
if (contribution.status !== 'PENDING') {
throw new BadRequestException(`Contribution has already been reviewed (${contribution.status}).`);
}
return this.dataSource.transaction(async (manager) => {
if (dto.action === 'APPROVE') {
const points = this.gamificationService.getPointsForContribution(contribution.contribution_type);
// 1. Update contribution status
contribution.status = 'APPROVED';
contribution.reviewed_by = dto.reviewerName;
contribution.reviewer_notes = dto.reviewerNotes || 'تم الاعتماد بنجاح';
contribution.points_awarded = points;
contribution.reviewed_at = new Date();
await manager.save(contribution);
// 2. Apply the change to heritage.landmarks based on contribution type
if (contribution.target_table === 'heritage.landmarks') {
if (contribution.contribution_type === 'CONFIRM_GATE' && contribution.target_id) {
await manager.query(
`UPDATE heritage.landmarks
SET access_gate_geom = (SELECT suggested_geom FROM community.contributions WHERE id = $1),
updated_at = CURRENT_TIMESTAMP
WHERE id = $2;`,
[contribution.id, contribution.target_id],
);
} else if (contribution.contribution_type === 'ADD_PARKING' && contribution.target_id) {
await manager.query(
`UPDATE heritage.landmarks
SET parking_geom = (SELECT suggested_geom FROM community.contributions WHERE id = $1),
updated_at = CURRENT_TIMESTAMP
WHERE id = $2;`,
[contribution.id, contribution.target_id],
);
} else if (contribution.contribution_type === 'ADD_PLACE') {
// Insert a new landmark from the contribution data
const data = contribution.suggested_data || {};
await manager.query(
`INSERT INTO heritage.landmarks
(slug, name_ar, name_en, category, era, narrative_ar, centroid_geom, governorate, is_verified, metadata)
VALUES
($1, $2, $3, $4, $5, $6,
(SELECT suggested_geom FROM community.contributions WHERE id = $7),
$8, false, $9);`,
[
data.slug || `contrib-${contribution.id}`,
contribution.place_name || data.name_ar || 'معلم بدون اسم',
data.name_en || null,
data.category || 'مساهمة_مجتمعية',
data.era || null,
data.narrative_ar || contribution.notes || null,
contribution.id,
data.governorate || null,
JSON.stringify({ source: 'community', contribution_id: contribution.id }),
],
);
} else if (contribution.contribution_type === 'ORAL_HISTORY' && contribution.target_id) {
// Append oral history narrative to existing landmark
const data = contribution.suggested_data || {};
await manager.query(
`UPDATE heritage.landmarks
SET narrative_ar = COALESCE(narrative_ar, '') || E'\n---\n' || $1,
audio_url = COALESCE($2, audio_url),
updated_at = CURRENT_TIMESTAMP
WHERE id = $3;`,
[
data.narrative_text || contribution.notes || '',
data.audio_url || null,
contribution.target_id,
],
);
} else if (contribution.contribution_type === 'CORRECT_STREET') {
// Street corrections are logged but not auto-applied to OSM data.
// They feed the manual road refinement pipeline.
}
}
// 3. Update guide points & level
if (contribution.guide) {
const guide = contribution.guide;
guide.total_points += points;
guide.approved_contributions_count += 1;
const levelInfo = this.gamificationService.computeLevelInfo(guide.total_points);
guide.guide_level = levelInfo.level;
guide.badge_title = levelInfo.badgeTitle;
await manager.save(guide);
}
return {
success: true,
action: 'APPROVED',
pointsAwarded: points,
message: `تم اعتماد المساهمة ومنح ${points} نقطة للمرشد بنجاح.`,
};
} else {
// REJECT
contribution.status = 'REJECTED';
contribution.reviewed_by = dto.reviewerName;
contribution.reviewer_notes = dto.reviewerNotes || 'المساهمة غير متوافقة مع معايير التدقيق';
contribution.points_awarded = 0;
contribution.reviewed_at = new Date();
await manager.save(contribution);
if (contribution.guide) {
const guide = contribution.guide;
guide.rejected_contributions_count += 1;
await manager.save(guide);
}
return {
success: true,
action: 'REJECTED',
message: 'تم رفض المساهمة وتسجيل الملاحظات الإدارية.',
};
}
});
}
/**
* Top 20 Guides Leaderboard
*/
async getLeaderboard(limit = 20) {
return this.guideRepo.find({
order: { total_points: 'DESC' },
take: limit,
select: [
'id',
'display_name',
'avatar_url',
'total_points',
'guide_level',
'badge_title',
'approved_contributions_count',
],
});
}
/**
* Get all badges
*/
async getBadges() {
return this.badgeRepo.find({ order: { required_points: 'ASC' } });
}
}
@@ -0,0 +1,22 @@
import { IsNumber, IsString, IsOptional, IsNotEmpty, IsIn } from 'class-validator';
import { Type } from 'class-transformer';
export class ReviewContributionDto {
@IsNumber()
@IsNotEmpty()
@Type(() => Number)
contributionId: number;
@IsString()
@IsNotEmpty()
@IsIn(['APPROVE', 'REJECT'])
action: 'APPROVE' | 'REJECT';
@IsString()
@IsNotEmpty()
reviewerName: string;
@IsString()
@IsOptional()
reviewerNotes?: string;
}
@@ -0,0 +1,54 @@
import { IsString, IsNumber, IsOptional, IsObject, IsNotEmpty, Min, Max, IsIn } from 'class-validator';
import { Type } from 'class-transformer';
export class SubmitContributionDto {
@IsString()
@IsNotEmpty()
userId: string;
@IsString()
@IsOptional()
displayName?: string;
@IsString()
@IsNotEmpty()
@IsIn(['CONFIRM_GATE', 'ADD_PARKING', 'CORRECT_STREET', 'ADD_PLACE', 'ORAL_HISTORY'])
contributionType: string;
@IsString()
@IsOptional()
targetTable?: string;
@IsNumber()
@IsOptional()
@Type(() => Number)
targetId?: number;
@IsString()
@IsOptional()
placeName?: string;
@IsNumber()
@Min(-90)
@Max(90)
@Type(() => Number)
lat: number;
@IsNumber()
@Min(-180)
@Max(180)
@Type(() => Number)
lng: number;
@IsObject()
@IsOptional()
suggestedData?: Record<string, any>;
@IsString()
@IsOptional()
proofImageUrl?: string;
@IsString()
@IsOptional()
notes?: string;
}
@@ -0,0 +1,26 @@
import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
@Entity({ name: 'badges', schema: 'community' })
export class Badge {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
@Index()
slug: string;
@Column()
name_ar: string;
@Column({ nullable: true })
name_en: string;
@Column({ type: 'text', nullable: true })
description_ar: string;
@Column()
required_points: number;
@Column({ default: 'award' })
icon_name: string;
}
@@ -0,0 +1,60 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index, ManyToOne, JoinColumn } from 'typeorm';
import { GuideProfile } from './guide-profile.entity';
@Entity({ name: 'contributions', schema: 'community' })
export class Contribution {
@PrimaryGeneratedColumn()
id: number;
@Column({ nullable: true })
guide_id: number;
@ManyToOne(() => GuideProfile, (g) => g.contributions, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'guide_id' })
guide: GuideProfile;
@Column()
@Index()
contribution_type: string; // 'CONFIRM_GATE', 'ADD_PLACE', 'CORRECT_STREET', 'ADD_PARKING', 'ORAL_HISTORY'
@Column({ default: 'heritage.landmarks' })
target_table: string;
@Column({ nullable: true })
target_id: number;
@Column({ nullable: true })
place_name: string;
@Column({ type: 'jsonb' })
suggested_data: Record<string, any>;
@Column({ type: 'geometry', spatialFeatureType: 'Geometry', srid: 4326 })
@Index({ spatial: true })
suggested_geom: any;
@Column({ nullable: true })
proof_image_url: string;
@Column({ type: 'text', nullable: true })
notes: string;
@Column({ default: 'PENDING' })
@Index()
status: string; // 'PENDING', 'APPROVED', 'REJECTED'
@Column({ nullable: true })
reviewed_by: string;
@Column({ type: 'text', nullable: true })
reviewer_notes: string;
@Column({ default: 0 })
points_awarded: number;
@CreateDateColumn()
created_at: Date;
@Column({ type: 'timestamptz', nullable: true })
reviewed_at: Date;
}
@@ -0,0 +1,52 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, OneToMany } from 'typeorm';
import { Contribution } from './contribution.entity';
@Entity({ name: 'guides_profiles', schema: 'community' })
export class GuideProfile {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
@Index()
user_id: string;
@Column()
display_name: string;
@Column({ nullable: true })
phone_number: string;
@Column({ nullable: true })
email: string;
@Column({ nullable: true })
avatar_url: string;
@Column({ default: 10 })
reputation_score: number;
@Column({ default: 0 })
@Index()
total_points: number;
@Column({ default: 1 })
guide_level: number;
@Column({ default: 'مرشد مبتدئ' })
badge_title: string;
@Column({ default: 0 })
approved_contributions_count: number;
@Column({ default: 0 })
rejected_contributions_count: number;
@OneToMany(() => Contribution, (c) => c.guide)
contributions: Contribution[];
@CreateDateColumn()
created_at: Date;
@UpdateDateColumn()
updated_at: Date;
}
@@ -0,0 +1,44 @@
import { Injectable } from '@nestjs/common';
export interface LevelInfo {
level: number;
badgeTitle: string;
nextLevelPoints: number;
}
@Injectable()
export class GamificationService {
private readonly POINTS_MAP: Record<string, number> = {
CONFIRM_GATE: 50,
ADD_PARKING: 50,
CORRECT_STREET: 75,
ADD_PLACE: 100,
ORAL_HISTORY: 150,
};
/**
* Calculate awarded points for a verified contribution
*/
getPointsForContribution(type: string): number {
return this.POINTS_MAP[type] || 25;
}
/**
* Determine guide level and rank title based on accumulated points
*/
computeLevelInfo(totalPoints: number): LevelInfo {
if (totalPoints >= 1500) {
return { level: 5, badgeTitle: 'مرشد سيادي رائد', nextLevelPoints: 3000 };
}
if (totalPoints >= 700) {
return { level: 4, badgeTitle: 'مرشد معتمد', nextLevelPoints: 1500 };
}
if (totalPoints >= 300) {
return { level: 3, badgeTitle: 'خبير محلي', nextLevelPoints: 700 };
}
if (totalPoints >= 100) {
return { level: 2, badgeTitle: 'مستكشف نشط', nextLevelPoints: 300 };
}
return { level: 1, badgeTitle: 'مرشد مبتدئ', nextLevelPoints: 100 };
}
}
@@ -0,0 +1,20 @@
import { Controller, Get, Post, Body, Query, UseGuards } from '@nestjs/common';
import { CommunityService } from './community.service';
import { ReviewContributionDto } from './dto/review-contribution.dto';
import { ApiKeyGuard } from '../common/guards/api-key.guard';
@Controller('v1/admin/moderation')
@UseGuards(ApiKeyGuard)
export class ModerationController {
constructor(private readonly communityService: CommunityService) {}
@Get('pending')
async getPending(@Query('limit') limit?: string) {
return this.communityService.getPendingContributions(limit ? parseInt(limit, 10) : 50);
}
@Post('review')
async review(@Body() dto: ReviewContributionDto) {
return this.communityService.reviewContribution(dto);
}
}
@@ -62,6 +62,6 @@ export abstract class BasePlace {
@Index()
neighborhood_id: number;
@Column({ type: 'int', nullable: true })
@Column({ type: 'int', default: 0, nullable: true })
elevation_meters: number;
}
@@ -30,7 +30,8 @@ export class GeocodingInitService implements OnModuleInit {
for (const table of tables) {
await this.repo.query(`
ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS elevation_meters INT;
ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS elevation_meters INT DEFAULT 0;
UPDATE ${table} SET elevation_meters = 0 WHERE elevation_meters IS NULL;
`);
for (const mapping of columnMapping) {
await this.repo.query(`
@@ -103,7 +104,44 @@ export class GeocodingInitService implements OnModuleInit {
await this.repo.query('CREATE INDEX IF NOT EXISTS idx_places_egypt_names_trgm ON places_egypt USING gist (name_ar gist_trgm_ops, name_en gist_trgm_ops);');
await this.repo.query('CREATE INDEX IF NOT EXISTS idx_places_iraq_names_trgm ON places_iraq USING gist (name_ar gist_trgm_ops, name_en gist_trgm_ops);');
this.logger.log('Geocoding database triggers and optimized indexes initialized for Syria, Jordan, Egypt, and Iraq.');
// 6. Tactical Terrain Obstacles Table & Auto-Population
await this.repo.query(`
CREATE TABLE IF NOT EXISTS tactical_terrain_obstacles (
id SERIAL PRIMARY KEY,
osm_id BIGINT,
obstacle_type VARCHAR(64),
severity VARCHAR(32),
name VARCHAR(255),
geometry GEOMETRY(Geometry, 4326),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_tactical_obs_geom ON tactical_terrain_obstacles USING gist (geometry);
CREATE INDEX IF NOT EXISTS idx_tactical_obs_type ON tactical_terrain_obstacles (obstacle_type);
DO $$
BEGIN
IF to_regclass('public.planet_osm_line') IS NOT NULL THEN
INSERT INTO tactical_terrain_obstacles (osm_id, obstacle_type, severity, name, geometry)
SELECT
osm_id,
COALESCE(natural, barrier, man_made, waterway) AS obstacle_type,
CASE
WHEN natural = 'cliff' THEN 'SEVERE_NO_GO'
WHEN barrier = 'retaining_wall' THEN 'RESTRICTED'
WHEN barrier IN ('ditch', 'berm') THEN 'TACTICAL_BARRIER'
WHEN waterway = 'wadi' THEN 'DRAINAGE_DEFILE'
ELSE 'OBSTACLE'
END,
name,
geometry
FROM planet_osm_line
WHERE (natural IN ('cliff', 'ridge', 'arete') OR barrier IN ('retaining_wall', 'berm', 'ditch') OR waterway IN ('wadi', 'waterfall'))
ON CONFLICT DO NOTHING;
END IF;
END $$;
`);
this.logger.log('Geocoding database triggers, indexes, and tactical obstacles initialized.');
} catch (err) {
this.logger.error('Failed to initialize database geocoding triggers:', err);
}
+59 -15
View File
@@ -111,7 +111,9 @@ export class GeocodingService {
}
let regionCondition = '';
if (targetRegion && ['syria', 'jordan', 'egypt', 'iraq'].includes(targetRegion)) {
// Only enforce strict textual region if NO GPS location was provided.
// When GPS coordinates are present, ST_DistanceSphere guarantees physical proximity within radius.
if (!hasLocation && targetRegion && ['syria', 'jordan', 'egypt', 'iraq'].includes(targetRegion)) {
regionCondition = `AND (region = '${targetRegion}' OR region = 'global')`;
}
@@ -121,13 +123,38 @@ export class GeocodingService {
'' as neighbourhood, '' as district, '' as governorate,
latitude, longitude, address, region, source, popularity_score,
${hasLocation ? 'ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326))' : '0'} as distance,
similarity(normalized_name, $1) as relevance
GREATEST(
similarity(normalized_name, $1),
CASE WHEN normalized_name ILIKE $1 || '%' THEN 0.95 ELSE 0.0 END,
CASE WHEN normalized_name ILIKE '%' || $1 || '%' THEN 0.85 ELSE 0.0 END,
0.50
) as relevance
FROM unified_search_index
WHERE normalized_name % $1
WHERE (
normalized_name % $1
OR normalized_name ILIKE '%' || $1 || '%'
-- Token containment (e.g. "المدينة الطبية", "سوبرماركت المدينة", "مخبز جواد")
OR (length($1) > 2 AND EXISTS (
SELECT 1 FROM unnest(string_to_array($1, ' ')) token
WHERE length(token) >= 3 AND normalized_name ILIKE '%' || token || '%'
))
-- Generic Category Keywords Mapping (All major amenities)
OR (category IN ('mosque', 'place_of_worship') AND ($1 ILIKE '%مسجد%' OR $1 ILIKE '%جامع%' OR $1 ILIKE '%مصلى%'))
OR (category IN ('restaurant', 'fast_food', 'food') AND ($1 ILIKE '%مطعم%' OR $1 ILIKE '%شاورما%' OR $1 ILIKE '%وجب%' OR $1 ILIKE '%مشاو%' OR $1 ILIKE '%برغر%'))
OR (category IN ('supermarket', 'convenience', 'grocery', 'shop', 'mall') AND ($1 ILIKE '%سوبر%' OR $1 ILIKE '%ماركت%' OR $1 ILIKE '%دكان%' OR $1 ILIKE '%بقال%' OR $1 ILIKE '%تموين%' OR $1 ILIKE '%مول%'))
OR (category IN ('bakery', 'pastry') AND ($1 ILIKE '%مخبز%' OR $1 ILIKE '%افران%' OR $1 ILIKE '%فرن%' OR $1 ILIKE '%حلويات%' OR $1 ILIKE '%معجنات%'))
OR (category IN ('cafe', 'coffee_shop') AND ($1 ILIKE '%مقهى%' OR $1 ILIKE '%كافيه%' OR $1 ILIKE '%كوفي%' OR $1 ILIKE '%قهوة%'))
OR (category IN ('pharmacy', 'chemist') AND ($1 ILIKE '%صيدل%'))
OR (category IN ('hospital', 'clinic', 'doctors', 'health') AND ($1 ILIKE '%مستشف%' OR $1 ILIKE '%عياد%' OR $1 ILIKE '%مركز صحي%' OR $1 ILIKE '%طبي%'))
OR (category IN ('fuel', 'gas_station', 'car_repair') AND ($1 ILIKE '%وقود%' OR $1 ILIKE '%كازية%' OR $1 ILIKE '%محطة%' OR $1 ILIKE '%بنزين%'))
OR (category IN ('bank', 'atm') AND ($1 ILIKE '%بنك%' OR $1 ILIKE '%صراف%' OR $1 ILIKE '%مصرف%'))
OR (category IN ('school', 'university', 'college') AND ($1 ILIKE '%مدرس%' OR $1 ILIKE '%جامع%' OR $1 ILIKE '%كلي%'))
OR (category IN ('hotel', 'guest_house') AND ($1 ILIKE '%فندق%' OR $1 ILIKE '%شقق فندقية%' OR $1 ILIKE '%منتجع%'))
)
${locationCondition}
${regionCondition}
ORDER BY (normalized_name <-> $1) ASC
LIMIT 50
ORDER BY ${hasLocation ? 'distance ASC, (normalized_name <-> $1) ASC' : '(normalized_name <-> $1) ASC'}
LIMIT 60
`;
const allResults = await Promise.race([
@@ -219,18 +246,35 @@ export class GeocodingService {
return results
.map(r => {
// Weighted scoring:
// 50% Text Match (relevance)
// 30% Popularity
// 20% Geographic Proximity
const textScore = Number(r.relevance);
const textScore = Number(r.relevance) || 0;
const popularityScore = (r.popularity_score || 10) / maxPopularity;
// Proximity bonus is 1.0 at 0m, decaying linearly to 0.0 at 10km.
const proximityBonus = hasLocation ? Math.max(0, 1 - (Number(r.distance) / 10000)) : 0;
// Proximity score: steep inverse decay so closer points get massive boost
// e.g. at 200m -> 0.91, 1km -> 0.67, 5km -> 0.28, 20km -> 0.09
const distKm = hasLocation ? (Number(r.distance) / 1000) : 0;
const proximityScore = hasLocation ? (1.0 / (1.0 + distKm * 0.5)) : 0;
const totalScore = (textScore * 0.5) + (popularityScore * 0.3) + (proximityBonus * 0.2);
// Source Priority Multiplier:
// user_place (manually entered / app-saved / enterprise verified): highest trust (+35% boost)
// approved_road: (+20% boost)
// overture: (+15% boost)
// osm_global: baseline (1.0)
let sourceMultiplier = 1.0;
if (r.source === 'user_place') {
sourceMultiplier = 1.35;
} else if (r.source === 'approved_road') {
sourceMultiplier = 1.20;
} else if (r.source === 'overture') {
sourceMultiplier = 1.15;
}
// When location is available, proximity is heavily prioritized (60%)
let totalScore = hasLocation
? (proximityScore * 0.60) + (textScore * 0.30) + (popularityScore * 0.10)
: (textScore * 0.65) + (popularityScore * 0.35);
totalScore *= sourceMultiplier;
return { ...r, totalScore };
})
.sort((a, b) => b.totalScore - a.totalScore)
@@ -242,7 +286,7 @@ export class GeocodingService {
}
return true;
})
.slice(0, 4)
.slice(0, 20)
.map(r => {
const addressParts = [r.neighbourhood, r.district, r.governorate].filter(Boolean);
const full_address = addressParts.length > 0 ? addressParts.join('، ') : (r.address || '');
@@ -1,11 +1,11 @@
import { Injectable, Logger } from '@nestjs/common';
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { OsmPointWithArea } from './entities/osm-point-with-area.entity';
@Injectable()
export class IndexRefreshService {
export class IndexRefreshService implements OnApplicationBootstrap {
private readonly logger = new Logger(IndexRefreshService.name);
constructor(
@@ -13,6 +13,85 @@ export class IndexRefreshService {
private readonly repo: Repository<OsmPointWithArea>, // Use any repository to execute raw SQL
) {}
async onApplicationBootstrap() {
this.logger.log('Running startup data validation and index refresh...');
await this.repairMisplacedJordanianPlaces();
await this.handleCron();
}
async repairMisplacedJordanianPlaces() {
try {
this.logger.log('Checking for Jordanian landmarks misplaced in places_egypt...');
// 1. Move any Egyptian places that physically lie within Jordan's bounding box to places_jordan
await this.repo.query(`
INSERT INTO places_jordan (name, name_ar, category, latitude, longitude, address, location, popularity_score)
SELECT
p.name,
p.name_ar,
COALESCE(p.category, 'hospital'),
p.latitude,
p.longitude,
p.address,
p.location,
GREATEST(COALESCE(p.popularity_score, 50), 95)
FROM places_egypt p
WHERE p.latitude BETWEEN 29.0 AND 33.5
AND p.longitude BETWEEN 34.8 AND 39.5
AND NOT EXISTS (
SELECT 1 FROM places_jordan pj
WHERE (pj.name_ar = p.name_ar OR pj.name = p.name)
AND ST_DWithin(pj.location::geography, p.location::geography, 200)
);
`);
await this.repo.query(`
DELETE FROM places_egypt
WHERE latitude BETWEEN 29.0 AND 33.5
AND longitude BETWEEN 34.8 AND 39.5;
`);
// 2. Explicitly ensure "مدينة الحسين الطبية (المدينة الطبية)" is present in places_jordan
await this.repo.query(`
INSERT INTO places_jordan (name, name_ar, category, latitude, longitude, address, location, popularity_score)
SELECT
'King Hussein Medical Center (المدينة الطبية)',
'مدينة الحسين الطبية (المدينة الطبية)',
'hospital',
31.978690,
35.834260,
'شارع الملك عبد الله الثاني، المدينة الطبية، دابوق / صويلح، عمّان، الأردن',
ST_SetSRID(ST_MakePoint(35.834260, 31.978690), 4326),
100
WHERE NOT EXISTS (
SELECT 1 FROM places_jordan
WHERE name_ar ILIKE '%المدينة الطبية%' OR name_ar ILIKE '%مدينة الحسين الطبية%'
);
`);
// Also ensure exact "المدينة الطبية" alias exists
await this.repo.query(`
INSERT INTO places_jordan (name, name_ar, category, latitude, longitude, address, location, popularity_score)
SELECT
'المدينة الطبية',
'المدينة الطبية',
'hospital',
31.978690,
35.834260,
'شارع الملك عبد الله الثاني، دابوق، عمّان، الأردن',
ST_SetSRID(ST_MakePoint(35.834260, 31.978690), 4326),
100
WHERE NOT EXISTS (
SELECT 1 FROM places_jordan WHERE name_ar = 'المدينة الطبية'
);
`);
this.logger.log('Completed check/repair of Jordanian landmarks.');
} catch (err: any) {
this.logger.warn('Failed to repair misplaced places: ' + err.message);
}
}
// Run every 5 minutes
@Cron(CronExpression.EVERY_5_MINUTES)
async handleCron() {
@@ -0,0 +1,68 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
@Entity({ name: 'landmarks', schema: 'heritage' })
export class HeritageLandmark {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
@Index()
slug: string;
@Column()
@Index()
name_ar: string;
@Column({ nullable: true })
name_en: string;
@Column()
@Index()
category: string;
@Column({ nullable: true })
era: string;
@Column({ type: 'text', nullable: true })
narrative_ar: string;
@Column({ type: 'text', nullable: true })
narrative_en: string;
@Column({ nullable: true })
audio_url: string;
@Column({ nullable: true })
image_url: string;
@Column({ type: 'geometry', spatialFeatureType: 'Point', srid: 4326 })
@Index({ spatial: true })
centroid_geom: any;
@Column({ type: 'geometry', spatialFeatureType: 'Point', srid: 4326, nullable: true })
@Index({ spatial: true })
access_gate_geom: any;
@Column({ type: 'geometry', spatialFeatureType: 'Point', srid: 4326, nullable: true })
@Index({ spatial: true })
parking_geom: any;
@Column({ type: 'geometry', spatialFeatureType: 'Point', srid: 4326, nullable: true })
ticket_office_geom: any;
@Column({ nullable: true })
@Index()
governorate: string;
@Column({ default: true })
is_verified: boolean;
@Column({ type: 'jsonb', default: {} })
metadata: Record<string, any>;
@CreateDateColumn()
created_at: Date;
@UpdateDateColumn()
updated_at: Date;
}
@@ -0,0 +1,40 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index } from 'typeorm';
@Entity({ name: 'trails', schema: 'heritage' })
export class HeritageTrail {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
@Index()
slug: string;
@Column()
name_ar: string;
@Column({ nullable: true })
name_en: string;
@Column()
@Index()
region: string;
@Column({ type: 'text', nullable: true })
description_ar: string;
@Column({ type: 'geometry', spatialFeatureType: 'MultiLineString', srid: 4326, nullable: true })
@Index({ spatial: true })
trail_geom: any;
@Column({ type: 'decimal', precision: 6, scale: 2, default: 0.0 })
total_distance_km: number;
@Column({ type: 'int', default: 0 })
stops_count: number;
@Column({ default: true })
is_active: boolean;
@CreateDateColumn()
created_at: Date;
}
@@ -0,0 +1,218 @@
import { Controller, Get, Param, Query, Header, UseGuards } from '@nestjs/common';
import { HeritageService } from './heritage.service';
import { ApiKeyGuard } from '../common/guards/api-key.guard';
@Controller('v1/heritage')
@UseGuards(ApiKeyGuard)
export class HeritageController {
constructor(private readonly heritageService: HeritageService) {}
@Get('landmarks')
async getLandmarks(
@Query('category') category?: string,
@Query('governorate') governorate?: string,
@Query('lat') lat?: string,
@Query('lng') lng?: string,
@Query('radius') radius?: string,
) {
return this.heritageService.getLandmarks({
category,
governorate,
lat: lat ? parseFloat(lat) : undefined,
lng: lng ? parseFloat(lng) : undefined,
radiusMeters: radius ? parseFloat(radius) : undefined,
});
}
@Get('landmarks/:slug')
async getLandmarkBySlug(@Param('slug') slug: string) {
return this.heritageService.getLandmarkBySlug(slug);
}
@Get('trails')
async getTrails(@Query('region') region?: string) {
return this.heritageService.getTrails(region);
}
@Get('offline-pack')
async getOfflinePack(
@Query('governorate') governorate?: string,
@Query('region') region?: string,
) {
return this.heritageService.getOfflinePack(governorate, region);
}
@Get('postman-collection')
@Header('Content-Type', 'application/json')
@Header('Content-Disposition', 'attachment; filename="siro-maps-postman-collection.json"')
getPostmanCollection() {
return {
info: {
name: "Siro Maps - Sovereign Heritage & Community Guides API",
description: "Production Endpoints for National Heritage Landmarks, Precision Gate Navigation, and Moderated Local Guides Gamification.",
schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
variable: [
{ key: "baseUrl", value: "https://map-saas.intaleqapp.com/api", type: "string" },
{ key: "apiKey", value: "zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX", type: "string" }
],
item: [
{
name: "1. Heritage - List All Landmarks",
request: {
method: "GET",
header: [{ key: "x-api-key", value: "{{apiKey}}" }],
url: {
raw: "{{baseUrl}}/v1/heritage/landmarks",
host: ["{{baseUrl}}"],
path: ["v1", "heritage", "landmarks"]
}
}
},
{
name: "2. Heritage - Nearby Landmarks (Spatial Query)",
request: {
method: "GET",
header: [{ key: "x-api-key", value: "{{apiKey}}" }],
url: {
raw: "{{baseUrl}}/v1/heritage/landmarks?lat=31.9539&lng=35.9350&radius=10000",
host: ["{{baseUrl}}"],
path: ["v1", "heritage", "landmarks"],
query: [
{ key: "lat", value: "31.9539" },
{ key: "lng", value: "35.9350" },
{ key: "radius", value: "10000" }
]
}
}
},
{
name: "3. Heritage - Single Landmark by Slug",
request: {
method: "GET",
header: [{ key: "x-api-key", value: "{{apiKey}}" }],
url: {
raw: "{{baseUrl}}/v1/heritage/landmarks/amman-citadel",
host: ["{{baseUrl}}"],
path: ["v1", "heritage", "landmarks", "amman-citadel"]
}
}
},
{
name: "4. Heritage - Offline Pack for Mobile",
request: {
method: "GET",
header: [{ key: "x-api-key", value: "{{apiKey}}" }],
url: {
raw: "{{baseUrl}}/v1/heritage/offline-pack",
host: ["{{baseUrl}}"],
path: ["v1", "heritage", "offline-pack"]
}
}
},
{
name: "5. Community - List Badges",
request: {
method: "GET",
header: [{ key: "x-api-key", value: "{{apiKey}}" }],
url: {
raw: "{{baseUrl}}/v1/community/badges",
host: ["{{baseUrl}}"],
path: ["v1", "community", "badges"]
}
}
},
{
name: "6. Community - Submit Contribution (Suggest Gate)",
request: {
method: "POST",
header: [
{ key: "Content-Type", value: "application/json" },
{ key: "x-api-key", value: "{{apiKey}}" }
],
body: {
mode: "raw",
raw: JSON.stringify({
userId: "guide_hamza_001",
displayName: "حمزة عايد",
contributionType: "CONFIRM_GATE",
targetTable: "heritage.landmarks",
targetId: 3,
placeName: "مدينة جرش - البوابة الجنوبية السياحية",
lat: 32.2731,
lng: 35.8924,
suggestedData: { gate_name: "South Main Gate" },
notes: "تم تأكيد موقع بوابة التذاكر الميدانية"
}, null, 2)
},
url: {
raw: "{{baseUrl}}/v1/community/contribute",
host: ["{{baseUrl}}"],
path: ["v1", "community", "contribute"]
}
}
},
{
name: "7. Community - Guide Profile & Points",
request: {
method: "GET",
header: [{ key: "x-api-key", value: "{{apiKey}}" }],
url: {
raw: "{{baseUrl}}/v1/community/profile/guide_hamza_001",
host: ["{{baseUrl}}"],
path: ["v1", "community", "profile", "guide_hamza_001"]
}
}
},
{
name: "8. Community - Leaderboard",
request: {
method: "GET",
header: [{ key: "x-api-key", value: "{{apiKey}}" }],
url: {
raw: "{{baseUrl}}/v1/community/leaderboard",
host: ["{{baseUrl}}"],
path: ["v1", "community", "leaderboard"]
}
}
},
{
name: "9. Admin - Moderation Pending Queue",
request: {
method: "GET",
header: [{ key: "x-api-key", value: "{{apiKey}}" }],
url: {
raw: "{{baseUrl}}/v1/admin/moderation/pending",
host: ["{{baseUrl}}"],
path: ["v1", "admin", "moderation", "pending"]
}
}
},
{
name: "10. Admin - Review Contribution (Approve)",
request: {
method: "POST",
header: [
{ key: "Content-Type", value: "application/json" },
{ key: "x-api-key", value: "{{apiKey}}" }
],
body: {
mode: "raw",
raw: JSON.stringify({
contributionId: 1,
action: "APPROVE",
reviewerName: "Hamza Ayed (Admin)",
reviewerNotes: "معتمدة ومطابقة لصور الأقمار الصناعية"
}, null, 2)
},
url: {
raw: "{{baseUrl}}/v1/admin/moderation/review",
host: ["{{baseUrl}}"],
path: ["v1", "admin", "moderation", "review"]
}
}
}
]
};
}
}
+16
View File
@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { HeritageLandmark } from './entities/heritage-landmark.entity';
import { HeritageTrail } from './entities/heritage-trail.entity';
import { HeritageService } from './heritage.service';
import { HeritageController } from './heritage.controller';
@Module({
imports: [
TypeOrmModule.forFeature([HeritageLandmark, HeritageTrail]),
],
controllers: [HeritageController],
providers: [HeritageService],
exports: [HeritageService],
})
export class HeritageModule {}
+206
View File
@@ -0,0 +1,206 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { HeritageLandmark } from './entities/heritage-landmark.entity';
import { HeritageTrail } from './entities/heritage-trail.entity';
@Injectable()
export class HeritageService {
constructor(
@InjectRepository(HeritageLandmark)
private readonly landmarkRepo: Repository<HeritageLandmark>,
@InjectRepository(HeritageTrail)
private readonly trailRepo: Repository<HeritageTrail>,
) {}
/**
* Find landmarks with optional category, governorate, or spatial proximity filters.
* Returns clean GeoJSON-compatible structure with precision gate & parking coordinates.
*/
async getLandmarks(options?: {
category?: string;
governorate?: string;
lat?: number;
lng?: number;
radiusMeters?: number;
}) {
const qb = this.landmarkRepo
.createQueryBuilder('l')
.select([
'l.id AS id',
'l.slug AS slug',
'l.name_ar AS name_ar',
'l.name_en AS name_en',
'l.category AS category',
'l.era AS era',
'l.narrative_ar AS narrative_ar',
'l.audio_url AS audio_url',
'l.image_url AS image_url',
'l.governorate AS governorate',
'l.is_verified AS is_verified',
'l.metadata AS metadata',
'ST_X(l.centroid_geom) AS centroid_lng',
'ST_Y(l.centroid_geom) AS centroid_lat',
'ST_X(l.access_gate_geom) AS gate_lng',
'ST_Y(l.access_gate_geom) AS gate_lat',
'ST_X(l.parking_geom) AS parking_lng',
'ST_Y(l.parking_geom) AS parking_lat',
'ST_X(l.ticket_office_geom) AS ticket_lng',
'ST_Y(l.ticket_office_geom) AS ticket_lat',
]);
if (options?.category) {
qb.andWhere('l.category = :category', { category: options.category });
}
if (options?.governorate) {
qb.andWhere('l.governorate = :gov', { gov: options.governorate });
}
if (options?.lat != null && options?.lng != null) {
const radius = options.radiusMeters || 25000;
qb.andWhere(
'ST_DWithin(l.centroid_geom::geography, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography, :radius)',
{ lng: options.lng, lat: options.lat, radius },
);
qb.addSelect(
'ST_Distance(l.centroid_geom::geography, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography)',
'distance_meters',
);
qb.orderBy('distance_meters', 'ASC');
} else {
qb.orderBy('l.id', 'ASC');
}
const rawResults = await qb.getRawMany();
return rawResults.map((r) => ({
id: r.id,
slug: r.slug,
name_ar: r.name_ar,
name_en: r.name_en,
category: r.category,
era: r.era,
narrative_ar: r.narrative_ar,
audio_url: r.audio_url,
image_url: r.image_url,
governorate: r.governorate,
is_verified: r.is_verified,
metadata: r.metadata,
coordinates: {
centroid: { lat: parseFloat(r.centroid_lat), lng: parseFloat(r.centroid_lng) },
access_gate: r.gate_lat ? { lat: parseFloat(r.gate_lat), lng: parseFloat(r.gate_lng) } : null,
parking: r.parking_lat ? { lat: parseFloat(r.parking_lat), lng: parseFloat(r.parking_lng) } : null,
ticket_office: r.ticket_lat ? { lat: parseFloat(r.ticket_lat), lng: parseFloat(r.ticket_lng) } : null,
},
distance_meters: r.distance_meters ? Math.round(parseFloat(r.distance_meters)) : undefined,
}));
}
/**
* Find a specific landmark by unique slug (direct query, no full-table scan)
*/
async getLandmarkBySlug(slug: string) {
const qb = this.landmarkRepo
.createQueryBuilder('l')
.select([
'l.id AS id',
'l.slug AS slug',
'l.name_ar AS name_ar',
'l.name_en AS name_en',
'l.category AS category',
'l.era AS era',
'l.narrative_ar AS narrative_ar',
'l.audio_url AS audio_url',
'l.image_url AS image_url',
'l.governorate AS governorate',
'l.is_verified AS is_verified',
'l.metadata AS metadata',
'ST_X(l.centroid_geom) AS centroid_lng',
'ST_Y(l.centroid_geom) AS centroid_lat',
'ST_X(l.access_gate_geom) AS gate_lng',
'ST_Y(l.access_gate_geom) AS gate_lat',
'ST_X(l.parking_geom) AS parking_lng',
'ST_Y(l.parking_geom) AS parking_lat',
'ST_X(l.ticket_office_geom) AS ticket_lng',
'ST_Y(l.ticket_office_geom) AS ticket_lat',
])
.where('l.slug = :slug', { slug });
const r = await qb.getRawOne();
if (!r) {
throw new NotFoundException(`Heritage landmark with slug '${slug}' not found.`);
}
return {
id: r.id,
slug: r.slug,
name_ar: r.name_ar,
name_en: r.name_en,
category: r.category,
era: r.era,
narrative_ar: r.narrative_ar,
audio_url: r.audio_url,
image_url: r.image_url,
governorate: r.governorate,
is_verified: r.is_verified,
metadata: r.metadata,
coordinates: {
centroid: { lat: parseFloat(r.centroid_lat), lng: parseFloat(r.centroid_lng) },
access_gate: r.gate_lat ? { lat: parseFloat(r.gate_lat), lng: parseFloat(r.gate_lng) } : null,
parking: r.parking_lat ? { lat: parseFloat(r.parking_lat), lng: parseFloat(r.parking_lng) } : null,
ticket_office: r.ticket_lat ? { lat: parseFloat(r.ticket_lat), lng: parseFloat(r.ticket_lng) } : null,
},
};
}
/**
* Fetch all active national cultural trails
*/
async getTrails(region?: string) {
const qb = this.trailRepo
.createQueryBuilder('t')
.select([
't.id AS id',
't.slug AS slug',
't.name_ar AS name_ar',
't.name_en AS name_en',
't.region AS region',
't.description_ar AS description_ar',
't.total_distance_km AS total_distance_km',
't.stops_count AS stops_count',
'ST_AsGeoJSON(t.trail_geom) AS geometry_geojson',
])
.where('t.is_active = true');
if (region) {
qb.andWhere('t.region = :region', { region });
}
const results = await qb.getRawMany();
return results.map((t) => ({
...t,
geometry: t.geometry_geojson ? JSON.parse(t.geometry_geojson) : null,
}));
}
/**
* Export offline pack for Siro Maps mobile client (Cache-First)
* Note: landmarks filter by `governorate`, trails filter by `region` — they differ.
*/
async getOfflinePack(governorate?: string, region?: string) {
const landmarks = await this.getLandmarks({ governorate });
const trails = await this.getTrails(region || governorate);
return {
version: '1.0.0',
timestamp: new Date().toISOString(),
governorate: governorate || 'all',
region: region || governorate || 'all',
total_landmarks: landmarks.length,
total_trails: trails.length,
landmarks,
trails,
};
}
}
+47 -9
View File
@@ -38,8 +38,12 @@ export class MapsController {
@ApiOperation({ summary: 'Get MapLibre style JSON 🎨' })
async getStyleJson(@Query('theme') theme: string, @Res() res: Response) {
// Determine filenames based on theme
const isDark = theme === 'obsidian';
const filename = isDark ? 'style-dark.json' : 'style.json';
let filename = 'style.json';
if (theme === 'obsidian') {
filename = 'style-dark.json';
} else if (theme === 'satellite') {
filename = 'style-satellite.json';
}
const fallbackFilename = 'style.json';
// Paths to check
@@ -47,7 +51,7 @@ export class MapsController {
path.join('/data', filename),
path.join(process.cwd(), '../../', filename),
path.join(process.cwd(), filename),
// Fallbacks to light style if dark is missing
// Fallbacks to light style if specific theme is missing
path.join('/data', fallbackFilename),
path.join(process.cwd(), '../../', fallbackFilename),
path.join(process.cwd(), fallbackFilename),
@@ -69,19 +73,53 @@ export class MapsController {
const styleRaw = fs.readFileSync(stylePath, 'utf8');
const styleObj = JSON.parse(styleRaw);
// Dynamic Theme support (Safety overrides or fine-tuning)
// Dynamic Theme support
if (theme === 'light') {
styleObj.layers.forEach((layer: any) => {
if (layer.id === 'background') {
if (layer.id === 'background' && layer.paint) {
layer.paint['background-color'] = '#FFFFFF';
}
});
} else if (theme === 'obsidian') {
// If we found style-dark.json, we don't strictly need this,
// but keeping it as a helper or if it fell back to style.json
styleObj.layers.forEach((layer: any) => {
if (layer.id === 'background') {
layer.paint['background-color'] = '#101014'; // Dark tone
if (layer.id === 'background' && layer.paint) {
layer.paint['background-color'] = '#101014';
}
});
} else if (theme === 'satellite') {
// Ensure ESRI Satellite layer is injected if not already present
if (!styleObj.sources['esri-satellite']) {
styleObj.sources['esri-satellite'] = {
type: 'raster',
tiles: [
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
],
tileSize: 256,
maxzoom: 19,
attribution: '© Esri, Maxar, Earthstar Geographics',
};
}
if (!styleObj.layers.some((l: any) => l.id === 'esri-satellite-imagery')) {
const satLayer = {
id: 'esri-satellite-imagery',
type: 'raster',
source: 'esri-satellite',
minzoom: 0,
maxzoom: 19,
paint: { 'raster-opacity': 1.0 },
};
const bgIdx = styleObj.layers.findIndex((l: any) => l.id === 'background');
if (bgIdx >= 0) {
styleObj.layers.splice(bgIdx + 1, 0, satLayer);
} else {
styleObj.layers.unshift(satLayer);
}
}
styleObj.layers.forEach((l: any) => {
if (l.id === 'background' && l.paint) {
l.paint['background-color'] = '#000000';
} else if ((l.id.includes('landuse') || l.id.includes('poly')) && l.type === 'fill' && l.paint) {
l.paint['fill-opacity'] = 0.05;
}
});
}
+3 -1
View File
@@ -65,9 +65,11 @@ export class MapsService {
console.warn('Geocoding internal error during routing:', e);
}
const ghProfile = ['car', 'foot', 'bike'].includes(profile) ? profile : 'car';
const payload: any = {
points: ghPoints,
profile: profile,
profile: ghProfile,
locale: locale === 'en' ? 'ar' : locale, // Default to Arabic if not specified or fallback
calc_points: true,
points_encoded: false, // JSON arrays for reliable 3D elevation (SRTM)
+107 -84
View File
@@ -5,6 +5,7 @@ import { Repository, DataSource } from 'typeorm';
import { CandidateRoad } from './candidate-road.entity';
import { RoadSegmentStat } from './road-stat.entity';
import { ApiKeyGuard } from '../common/guards/api-key.guard';
import { RedisService } from '../common/redis.service';
@ApiTags('map-refinement-roads')
@Controller('map-refinement/roads')
@@ -18,6 +19,7 @@ export class RoadRefinementController {
@InjectRepository(RoadSegmentStat)
private readonly roadStatRepo: Repository<RoadSegmentStat>,
private readonly dataSource: DataSource,
private readonly redisService: RedisService,
) {}
@Get('summary')
@@ -60,12 +62,15 @@ export class RoadRefinementController {
const s = status || 'pending';
try {
const rows = await this.dataSource.query(
`SELECT id, "uniqueDriverCount", "totalPoints", "averageSpeed", "lengthMeters",
`SELECT c.id, c."uniqueDriverCount", c."totalPoints", c."averageSpeed", c."lengthMeters",
confidence, status, source, name, highway, oneway, "discoveredAt", "reviewedAt",
ST_AsGeoJSON(geometry) as geojson
FROM candidate_roads
WHERE status = $1
ORDER BY confidence DESC, "discoveredAt" DESC
ar.routing_status AS "routingStatus", ar.routing_error AS "routingError",
ar.routing_requested_at AS "routingRequestedAt", ar.routed_at AS "routedAt",
ST_AsGeoJSON(c.geometry) as geojson
FROM candidate_roads c
LEFT JOIN approved_roads ar ON ar.candidate_id = c.id
WHERE c.status = $1
ORDER BY c.confidence DESC, c."discoveredAt" DESC
LIMIT 100`,
[s]
);
@@ -75,10 +80,38 @@ export class RoadRefinementController {
}));
} catch (e: any) {
this.logger.error(`Error fetching candidates: ${e.message}`);
return [];
// Before the first approval the optional publication table may not exist;
// pending-road review must remain available in that fresh installation.
try {
const rows = await this.dataSource.query(
`SELECT id, "uniqueDriverCount", "totalPoints", "averageSpeed", "lengthMeters",
confidence, status, source, name, highway, oneway, "discoveredAt", "reviewedAt",
ST_AsGeoJSON(geometry) as geojson
FROM candidate_roads WHERE status = $1
ORDER BY confidence DESC, "discoveredAt" DESC LIMIT 100`,
[s],
);
return rows.map((r: any) => ({ ...r, geometry: r.geojson ? JSON.parse(r.geojson) : null }));
} catch (_) {
return [];
}
}
}
@Get('approved/:id/routing-status')
@ApiOperation({ summary: 'Get approved road routing publication status' })
@ApiParam({ name: 'id', description: 'Approved road or candidate UUID' })
async getRoutingStatus(@Param('id') id: string) {
const [road] = await this.dataSource.query(`
SELECT id, candidate_id AS "candidateId", routing_status AS "routingStatus",
routing_error AS "routingError", routing_requested_at AS "routingRequestedAt",
routed_at AS "routedAt", start_node AS "startNode", end_node AS "endNode"
FROM approved_roads WHERE id::text = $1 OR candidate_id::text = $1
`, [id]);
if (!road) throw new HttpException('Approved road not found', HttpStatus.NOT_FOUND);
return road;
}
@Post('candidates/manual')
@ApiOperation({ summary: 'Submit manual candidate road drawn on map ✍️' })
async submitManualCandidate(@Body() body: { geojson: any; name?: string; highway?: string }) {
@@ -212,6 +245,10 @@ export class RoadRefinementController {
oneway SMALLINT DEFAULT 0,
start_node BIGINT,
end_node BIGINT,
routing_status VARCHAR(32) NOT NULL DEFAULT 'pending_connection',
routing_error TEXT,
routing_requested_at TIMESTAMP,
routed_at TIMESTAMP,
approved_at TIMESTAMP DEFAULT NOW()
);
ALTER TABLE approved_roads ADD COLUMN IF NOT EXISTS candidate_id UUID;
@@ -223,6 +260,10 @@ export class RoadRefinementController {
ALTER TABLE approved_roads ADD COLUMN IF NOT EXISTS start_node BIGINT;
ALTER TABLE approved_roads ADD COLUMN IF NOT EXISTS end_node BIGINT;
ALTER TABLE approved_roads ADD COLUMN IF NOT EXISTS approved_at TIMESTAMP DEFAULT NOW();
ALTER TABLE approved_roads ADD COLUMN IF NOT EXISTS routing_status VARCHAR(32) NOT NULL DEFAULT 'pending_connection';
ALTER TABLE approved_roads ADD COLUMN IF NOT EXISTS routing_error TEXT;
ALTER TABLE approved_roads ADD COLUMN IF NOT EXISTS routing_requested_at TIMESTAMP;
ALTER TABLE approved_roads ADD COLUMN IF NOT EXISTS routed_at TIMESTAMP;
CREATE INDEX IF NOT EXISTS approved_roads_geom_idx ON approved_roads USING GIST(geometry);
CREATE UNIQUE INDEX IF NOT EXISTS approved_roads_cand_uidx ON approved_roads(candidate_id) WHERE candidate_id IS NOT NULL;
`);
@@ -244,86 +285,68 @@ export class RoadRefinementController {
reviewedAt: new Date(),
});
// 4. Safely attempt topology node snapping (Non-blocking)
try {
await this.dataSource.query(`
DO $$
DECLARE
cand RECORD;
start_pt GEOMETRY;
end_pt GEOMETRY;
snapped_geom GEOMETRY;
s_node BIGINT := NULL;
e_node BIGINT := NULL;
node_rec RECORD;
BEGIN
SELECT id, geometry::geometry as geom, name, COALESCE(highway, 'residential') as highway, confidence, "uniqueDriverCount", oneway
INTO cand
FROM candidate_roads WHERE id = $1;
IF FOUND THEN
snapped_geom := cand.geom;
start_pt := ST_StartPoint(snapped_geom);
end_pt := ST_EndPoint(snapped_geom);
-- Snap start
BEGIN
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
INTO node_rec
FROM p
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
JOIN planet_osm_ways w ON w.id = l.osm_id
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
JOIN planet_osm_nodes n ON n.id = wn.node_id
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) < 30
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) ASC
LIMIT 1;
IF FOUND THEN
s_node := node_rec.id;
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
END IF;
EXCEPTION WHEN OTHERS THEN
NULL;
END;
-- Snap end
BEGIN
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
INTO node_rec
FROM p
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
JOIN planet_osm_ways w ON w.id = l.osm_id
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
JOIN planet_osm_nodes n ON n.id = wn.node_id
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) < 30
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) ASC
LIMIT 1;
IF FOUND THEN
e_node := node_rec.id;
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
END IF;
EXCEPTION WHEN OTHERS THEN
NULL;
END;
UPDATE approved_roads
SET geometry = snapped_geom,
start_node = COALESCE(s_node, start_node),
end_node = COALESCE(e_node, end_node)
WHERE candidate_id = cand.id;
END IF;
END $$;
`, [id]);
} catch (snapErr: any) {
this.logger.warn(`Topology snapping skipped for ${id}: ${snapErr.message}`);
const [middleTables] = await this.dataSource.query(`
SELECT COUNT(*)::int AS count
FROM information_schema.tables
WHERE table_schema = 'public' AND table_name IN ('planet_osm_line', 'planet_osm_ways', 'planet_osm_nodes')
`);
if (middleTables.count !== 3) {
await this.dataSource.query(`UPDATE approved_roads
SET routing_status = 'blocked_missing_network',
routing_error = 'Routing network index is unavailable; retry after the map import completes.'
WHERE candidate_id = $1::uuid`, [id]);
return { success: true, id, status: 'approved', routingStatus: 'blocked_missing_network' };
}
this.logger.log(`✅ Road candidate ${id} approved & published.`);
return { success: true, id, status: 'approved' };
// A route can only enter GraphHopper when both endpoints share real OSM nodes.
// Keep an explicit status instead of silently publishing an isolated line.
const [connected] = await this.dataSource.query(`
WITH road AS (
SELECT id, geometry FROM approved_roads WHERE candidate_id = $1::uuid
),
endpoints AS (
SELECT ST_StartPoint(geometry) AS start_pt, ST_EndPoint(geometry) AS end_pt FROM road
),
start_node AS (
SELECT n.id, ST_SetSRID(ST_MakePoint(n.lon / 1e7, n.lat / 1e7), 4326) AS point
FROM endpoints e
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(ST_Transform(e.start_pt, 3857), 60)
JOIN planet_osm_ways w ON w.id = l.osm_id
CROSS JOIN LATERAL unnest(w.nodes) AS node_id
JOIN planet_osm_nodes n ON n.id = node_id
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon / 1e7, n.lat / 1e7), 4326)::geography, e.start_pt::geography) <= 30
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon / 1e7, n.lat / 1e7), 4326)::geography, e.start_pt::geography) LIMIT 1
),
end_node AS (
SELECT n.id, ST_SetSRID(ST_MakePoint(n.lon / 1e7, n.lat / 1e7), 4326) AS point
FROM endpoints e
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(ST_Transform(e.end_pt, 3857), 60)
JOIN planet_osm_ways w ON w.id = l.osm_id
CROSS JOIN LATERAL unnest(w.nodes) AS node_id
JOIN planet_osm_nodes n ON n.id = node_id
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon / 1e7, n.lat / 1e7), 4326)::geography, e.end_pt::geography) <= 30
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon / 1e7, n.lat / 1e7), 4326)::geography, e.end_pt::geography) LIMIT 1
)
UPDATE approved_roads a
SET geometry = ST_SetPoint(ST_SetPoint(a.geometry, 0, s.point), ST_NPoints(a.geometry) - 1, e.point),
start_node = s.id, end_node = e.id,
routing_status = 'queued', routing_error = NULL, routing_requested_at = NOW()
FROM start_node s CROSS JOIN end_node e
WHERE a.candidate_id = $1::uuid
RETURNING a.id
`, [id]);
if (!connected) {
await this.dataSource.query(`UPDATE approved_roads
SET routing_status = 'needs_endpoint_connection',
routing_error = 'Both road endpoints must be within 30 metres of existing road nodes.'
WHERE candidate_id = $1::uuid`, [id]);
return { success: true, id, status: 'approved', routingStatus: 'needs_endpoint_connection' };
}
await this.redisService.set('routing_sync_requested', '1');
this.logger.log(`✅ Road candidate ${id} queued for routing rebuild.`);
return { success: true, id, status: 'approved', routingStatus: 'queued' };
} catch (e: any) {
this.logger.error(`Failed to approve candidate road: ${e.message}`);
throw new HttpException(e.message, HttpStatus.INTERNAL_SERVER_ERROR);
@@ -0,0 +1,82 @@
import * as fs from 'fs';
import * as path from 'path';
import { Logger } from '@nestjs/common';
import { HttpException, HttpStatus } from '@nestjs/common';
export interface RoutingPackageManifest {
packageId: string;
version: string;
fileName: string;
sizeBytes: number;
sha256: string;
engine: string;
elevation: string;
bbox?: Record<string, number>;
builtAt: string;
}
/**
* Serves the on-device Valhalla routing package (Jordan) built by
* infrastructure/scripts/build-valhalla-tiles.sh. The tactical app downloads
* this package once and routes fully offline against the real road network
* with SRTM elevation — same data the server-side GraphHopper engine uses.
*/
export class RoutingPackageService {
private static readonly logger = new Logger(RoutingPackageService.name);
private static readonly PACKAGE_DIR =
process.env.ROUTING_PACKAGE_DIR ||
(fs.existsSync('/data/infrastructure/osm-data/routing-packages')
? '/data/infrastructure/osm-data/routing-packages'
: path.join(process.cwd(), 'infrastructure/osm-data/routing-packages'));
getDirectory(): string {
return RoutingPackageService.PACKAGE_DIR;
}
/**
* Read jordan-routing-manifest.json written by the tile builder.
* Returns null when no package has been built yet.
*/
getManifest(): RoutingPackageManifest | null {
const manifestPath = path.join(RoutingPackageService.PACKAGE_DIR, 'jordan-routing-manifest.json');
try {
if (!fs.existsSync(manifestPath)) return null;
const raw = fs.readFileSync(manifestPath, 'utf8');
const manifest = JSON.parse(raw) as RoutingPackageManifest;
// Verify the archive actually exists next to the manifest.
const filePath = this.getPackageFilePath(manifest);
if (!fs.existsSync(filePath)) return null;
return manifest;
} catch (e) {
RoutingPackageService.logger.warn(`Failed to read routing manifest: ${e}`);
return null;
}
}
/**
* Require the manifest + file or throw 404 — used before streaming.
*/
requireManifest(): RoutingPackageManifest {
const manifest = this.getManifest();
if (!manifest) {
throw new HttpException(
'Routing package not available. Run infrastructure/scripts/build-valhalla-tiles.sh on the server.',
HttpStatus.NOT_FOUND,
);
}
return manifest;
}
getPackageFilePath(manifest: RoutingPackageManifest): string {
// Never trust fileName blindly: only allow plain names inside the package dir.
const safeName = path.basename(manifest.fileName || '');
return path.join(RoutingPackageService.PACKAGE_DIR, safeName);
}
createPackageStream(manifest: RoutingPackageManifest): fs.ReadStream {
const filePath = this.getPackageFilePath(manifest);
return fs.createReadStream(filePath);
}
}
+70 -1
View File
@@ -18,6 +18,7 @@ import { LineOfSightBodyDto, LineOfSightQueryDto } from './dto/line-of-sight.dto
import { ArtilleryMissionRequestDto, SaveScenarioDto } from './dto/tactical.dto';
import { TacticalService } from './tactical.service';
import { DemTileService } from './dem-tile.service';
import { RoutingPackageService } from './routing-package.service';
@ApiTags('tactical')
@ApiHeader({
@@ -28,7 +29,10 @@ import { DemTileService } from './dem-tile.service';
@Controller('tactical')
@UseGuards(ApiKeyGuard, TenantThrottlerGuard)
export class TacticalController {
constructor(private readonly tacticalService: TacticalService) { }
constructor(
private readonly tacticalService: TacticalService,
private readonly routingPackageService: RoutingPackageService,
) { }
@Get('verify-license')
@ApiOperation({ summary: 'Verify tactical clearance and military license' })
@@ -225,6 +229,42 @@ export class TacticalController {
return this.tacticalService.getOfflinePackageInfo();
}
@Get('routing-package/jordan/manifest')
@ApiOperation({
summary: 'Manifest of the on-device Valhalla routing package (version, sha256, size)',
})
getRoutingPackageManifest() {
const manifest = this.routingPackageService.getManifest();
if (!manifest) {
return {
available: false,
message:
'Routing package not built yet. Run infrastructure/scripts/build-valhalla-tiles.sh on the server.',
};
}
return { available: true, ...manifest };
}
@Get('routing-package/jordan')
@ApiOperation({
summary:
'Download the Jordan Valhalla routing tar (real road graph + SRTM elevation) for 100% offline on-device routing',
})
async downloadRoutingPackage(@Res() res: any) {
const manifest = this.routingPackageService.requireManifest();
const stream = this.routingPackageService.createPackageStream(manifest);
res.setHeader('Content-Type', 'application/x-tar');
res.setHeader('Content-Length', manifest.sizeBytes);
res.setHeader(
'Content-Disposition',
`attachment; filename="${manifest.fileName}"`,
);
res.setHeader('X-Package-Version', manifest.version);
res.setHeader('X-Package-Sha256', manifest.sha256);
stream.pipe(res);
}
@Get('landmarks')
@ApiOperation({
summary: 'Get tactical strategic landmarks / استرجاع معالم الأردن البصرية والاستراتيجية للتقاطع الميداني',
@@ -236,6 +276,24 @@ export class TacticalController {
return this.tacticalService.getLandmarks(region, type);
}
@Get('ipb/obstacles')
@ApiOperation({
summary: 'Query Tactical IPB Obstacles by Bounding Box / استعلام الموانع التكتيكية ضمن نطاق جغرافي',
})
async getIPBObstacles(
@Query('minLat') minLatStr: string,
@Query('minLng') minLngStr: string,
@Query('maxLat') maxLatStr: string,
@Query('maxLng') maxLngStr: string,
) {
const minLat = parseFloat(minLatStr) || 31.0;
const minLng = parseFloat(minLngStr) || 35.0;
const maxLat = parseFloat(maxLatStr) || 33.0;
const maxLng = parseFloat(maxLngStr) || 37.0;
return this.tacticalService.getIPBObstacles({ minLat, minLng, maxLat, maxLng });
}
@Get('dem/:zoom/:x/:y')
@ApiOperation({
summary: 'Stream Sovereign Real Satellite DEM Elevation Tile / تقديم بلاطات الارتفاعات السيادية من السيرفر المحلي',
@@ -257,4 +315,15 @@ export class TacticalController {
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
res.send(buffer);
}
@Post('ai-assessment')
@ApiOperation({
summary: 'Generate Advanced AI Tactical Assessment using Gemini 1.5 Pro based on comprehensive IPB and Terrain data',
})
async generateAIAssessment(@Body() body: { ipbData: any; terrainData: any }) {
if (!body.ipbData || !body.terrainData) {
throw new HttpException('Missing required tactical data (ipbData, terrainData)', HttpStatus.BAD_REQUEST);
}
return this.tacticalService.generateTacticalAIAssessment(body.ipbData, body.terrainData);
}
}
+2 -1
View File
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TacticalController } from './tactical.controller';
import { TacticalService } from './tactical.service';
import { RoutingPackageService } from './routing-package.service';
import { RedisModule } from '../common/redis.module';
import { PlaceJordan } from '../geocoding/entities/place-jordan.entity';
@@ -11,7 +12,7 @@ import { PlaceJordan } from '../geocoding/entities/place-jordan.entity';
TypeOrmModule.forFeature([PlaceJordan]),
],
controllers: [TacticalController],
providers: [TacticalService],
providers: [TacticalService, RoutingPackageService],
exports: [TacticalService],
})
export class TacticalModule {}
+170
View File
@@ -5,6 +5,7 @@ import { RedisService } from '../common/redis.service';
import { ArtilleryMissionRequestDto, TacticalSymbolDto } from './dto/tactical.dto';
import { getElevationMeters } from '../common/gis.utils';
import { DemTileService } from './dem-tile.service';
import { RoutingPackageService } from './routing-package.service';
export interface LosPoint {
index: number;
@@ -82,6 +83,7 @@ export class TacticalService {
@InjectRepository(PlaceJordan)
private readonly placeJordanRepo: Repository<PlaceJordan>,
private readonly dataSource: DataSource,
private readonly routingPackageService: RoutingPackageService,
) {
this.graphHopperUrl = this.configService.get('GRAPH_HOPPER_URL', 'http://routing:8080');
}
@@ -1022,6 +1024,9 @@ export class TacticalService {
}
} catch (_) {}
// Real on-device routing package status (Valhalla graph built from OSM + SRTM)
const routingManifest = this.routingPackageService?.getManifest?.() ?? null;
return {
packageId: 'jordan-tactical-offline-v2',
name: 'حزمة الأردن التكتيكية الميدانية الكاملة (Off-Grid Sovereign Package)',
@@ -1033,6 +1038,21 @@ export class TacticalService {
sizeFormatted: '825 KB (خفيفة جداً وسريعة التحميل)',
offlineRoutingReady: true,
offlineResectionReady: true,
routingPackage: routingManifest
? {
available: true,
packageId: routingManifest.packageId,
version: routingManifest.version,
fileName: routingManifest.fileName,
sizeBytes: routingManifest.sizeBytes,
sha256: routingManifest.sha256,
engine: routingManifest.engine,
elevation: routingManifest.elevation,
downloadUrl: '/api/tactical/routing-package/jordan',
manifestUrl: '/api/tactical/routing-package/jordan/manifest',
builtAt: routingManifest.builtAt,
}
: { available: false },
lastUpdated: new Date().toISOString()
};
}
@@ -1150,4 +1170,154 @@ export class TacticalService {
timestamp: new Date().toISOString()
};
}
/**
* Get Tactical IPB Obstacles within Bounding Box
*/
async getIPBObstacles(bbox: {
minLat: number;
minLng: number;
maxLat: number;
maxLng: number;
}) {
const { minLat, minLng, maxLat, maxLng } = bbox;
try {
// 1. Query pre-computed / merged tactical_terrain_obstacles table if it exists
const tableCheck = await this.dataSource.query(`
SELECT to_regclass('public.tactical_terrain_obstacles') as exists;
`);
let features: any[] = [];
if (tableCheck?.[0]?.exists) {
const rows = await this.dataSource.query(
`
SELECT
id,
obstacle_type,
severity,
name,
ST_AsGeoJSON(geometry)::json as geojson
FROM tactical_terrain_obstacles
WHERE geometry && ST_MakeEnvelope($1, $2, $3, $4, 4326)
LIMIT 500;
`,
[minLng, minLat, maxLng, maxLat],
);
features = rows.map((r: any) => ({
type: 'Feature',
properties: {
id: r.id,
obstacleType: r.obstacle_type,
severity: r.severity,
name: r.name,
},
geometry: r.geojson,
}));
}
// 2. If tactical_terrain_obstacles was empty, query planet_osm_line directly as fallback
if (features.length === 0) {
const osmCheck = await this.dataSource.query(`
SELECT to_regclass('public.planet_osm_line') as exists;
`);
if (osmCheck?.[0]?.exists) {
const rows = await this.dataSource.query(
`
SELECT
osm_id as id,
COALESCE(natural, barrier, man_made, waterway) AS obstacle_type,
CASE
WHEN natural = 'cliff' THEN 'SEVERE_NO_GO'
WHEN barrier = 'retaining_wall' THEN 'RESTRICTED'
WHEN barrier IN ('ditch', 'berm') THEN 'TACTICAL_BARRIER'
WHEN waterway = 'wadi' THEN 'DRAINAGE_DEFILE'
ELSE 'OBSTACLE'
END as severity,
name,
ST_AsGeoJSON(geometry)::json as geojson
FROM planet_osm_line
WHERE geometry && ST_MakeEnvelope($1, $2, $3, $4, 4326)
AND (natural IN ('cliff', 'ridge', 'arete') OR barrier IN ('retaining_wall', 'berm', 'ditch') OR waterway IN ('wadi', 'waterfall'))
LIMIT 500;
`,
[minLng, minLat, maxLng, maxLat],
);
features = rows.map((r: any) => ({
type: 'Feature',
properties: {
id: r.id,
obstacleType: r.obstacle_type,
severity: r.severity,
name: r.name,
},
geometry: r.geojson,
}));
}
}
return {
type: 'FeatureCollection',
count: features.length,
features,
};
} catch (err: any) {
this.logger.error(`Error querying IPB obstacles: ${err?.message}`);
return {
type: 'FeatureCollection',
count: 0,
features: [],
};
}
}
async generateTacticalAIAssessment(ipbData: any, terrainData: any): Promise<any> {
const geminiKey = this.configService.get<string>('GEMINI_API_KEY');
if (!geminiKey) {
throw new Error('GEMINI_API_KEY is not configured on the server.');
}
const prompt = `أنت ضابط ركن استخبارات عسكرية (G2) ومحلل تكتيكي استراتيجي خبير.
الرجاء دراسة التقرير التكتيكي المرفق والذي يحتوي على تقدير موقف الاستخبارات عن الأرض (IPB)، الموانع الطبيعية، المقاطع الصخرية، مناطق السكن، والارتفاعات.
المعطيات:
بيانات دراسة الأرض والتضاريس:
${JSON.stringify(terrainData, null, 2)}
بيانات الشفافات التكتيكية (IPB):
${JSON.stringify(ipbData, null, 2)}
المطلوب:
بناءً على الأرقام الدقيقة والموقع الجغرافي المعطى، قدم تحليلاً استراتيجياً مفصلاً يشمل:
1. التهديدات والفرص التعبوية بناءً على التضاريس.
2. أفضل محاور التقدم ومناطق التقتيل (Engagement Areas/Kill Zones).
3. تقييم الموانع وتأثيرها على حركة الدروع والمشاة الآلية.
4. توصيات لتموضع القوات الصديقة (احتياط، مدفعية، رصد).
الرجاء كتابة التقرير بلغة عسكرية احترافية وواضحة (باللغة العربية). لا تقم باختراع أرقام، اعتمد كلياً على البيانات المرفقة.`;
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-3.7-flash:generateContent?key=${geminiKey}`;
try {
this.logger.log('Sending comprehensive tactical data to Gemini 3.7 Flash for analysis...');
const response = await axios.post(
url,
{
contents: [{ parts: [{ text: prompt }] }],
generationConfig: {
temperature: 0.2,
}
},
{ timeout: 35000 }
);
const content = response.data?.candidates?.[0]?.content?.parts?.[0]?.text;
return { success: true, assessment: content };
} catch (err: any) {
this.logger.error(`Failed to generate AI assessment: ${err.message}`);
throw new Error('Failed to generate tactical AI assessment.');
}
}
}
@@ -0,0 +1,66 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber, IsOptional, IsString, Max, Min, IsArray, ValidateNested } from 'class-validator';
import { Type, Transform } from 'class-transformer';
export class DriverTelemetryDto {
@ApiProperty({ description: 'Driver unique ID', example: 'driver_jo_1042' })
@IsString()
@IsNotEmpty()
@Transform(({ obj, value }) => value ?? obj.driver_id ?? obj.driverId)
driver_id: string;
@ApiProperty({ description: 'Latitude coordinate (-90 to 90)', example: 31.9539 })
@IsNumber()
@Min(-90)
@Max(90)
@Transform(({ obj, value }) => Number(value ?? obj.latitude ?? obj.lat))
latitude: number;
@ApiProperty({ description: 'Longitude coordinate (-180 to 180)', example: 35.9106 })
@IsNumber()
@Min(-180)
@Max(180)
@Transform(({ obj, value }) => Number(value ?? obj.longitude ?? obj.lng))
longitude: number;
@ApiProperty({ description: 'Instantaneous vehicle speed in km/h', example: 45.5 })
@IsNumber()
@Min(0)
@Transform(({ obj, value }) => Number(value ?? obj.speed ?? 0))
speed: number;
@ApiProperty({ description: 'Compass heading / bearing in degrees (0 - 360)', example: 185.0 })
@IsNumber()
@Min(0)
@Max(360)
@Transform(({ obj, value }) => Number(value ?? obj.heading ?? 0))
heading: number;
@ApiPropertyOptional({ description: 'Distance traveled in meters', example: 1250.4, default: 0 })
@IsOptional()
@IsNumber()
@Transform(({ obj, value }) => (value != null ? Number(value) : (obj.distance != null ? Number(obj.distance) : 0)))
distance?: number;
@ApiPropertyOptional({ description: 'Elevation above mean sea level in meters (AMSL)', example: 890.5, default: 0 })
@IsOptional()
@IsNumber()
@Transform(({ obj, value }) => {
const val = value ?? obj.elevation ?? obj.altitude;
return val != null ? Number(val) : 0;
})
elevation?: number;
@ApiPropertyOptional({ description: 'Client GPS capture time as ISO-8601', example: '2026-09-16T10:20:30.000Z' })
@IsOptional()
@IsString()
timestamp?: string;
}
export class DriverTelemetryBatchDto {
@ApiProperty({ type: [DriverTelemetryDto], description: 'Array of telemetry points for batch processing' })
@IsArray()
@ValidateNested({ each: true })
@Type(() => DriverTelemetryDto)
points: DriverTelemetryDto[];
}
@@ -0,0 +1,69 @@
import { Controller, Post, Get, Body, Query, Param, UseGuards, HttpException, HttpStatus } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiQuery, ApiParam, ApiSecurity } from '@nestjs/swagger';
import { TelemetryService } from './telemetry.service';
import { DriverTelemetryDto, DriverTelemetryBatchDto } from './dto/driver-telemetry.dto';
import { ApiKeyGuard } from '../common/guards/api-key.guard';
@ApiTags('telemetry')
@ApiSecurity('x-api-key')
@Controller('telemetry')
@UseGuards(ApiKeyGuard)
export class TelemetryController {
constructor(private readonly telemetryService: TelemetryService) {}
@Post()
@ApiOperation({
summary: 'Ingest real-time driver telemetry with elevation & distance 📡⛰️',
description: 'Receives GPS position, speed, heading, distance, and AMSL elevation from driver app.',
})
async ingest(@Body() data: DriverTelemetryDto) {
if (!data.driver_id) {
throw new HttpException('Missing driver_id', HttpStatus.BAD_REQUEST);
}
return this.telemetryService.ingest(data);
}
@Post('batch')
@ApiOperation({
summary: 'Queue navigation telemetry batch 📦',
description: 'Acknowledges a navigation batch immediately; the server persists it from Redis in the background.',
})
async ingestBatch(@Body() body: DriverTelemetryBatchDto) {
if (!body || !Array.isArray(body.points)) {
throw new HttpException('Invalid payload: expected { points: [...] }', HttpStatus.BAD_REQUEST);
}
if (body.points.length > 100) {
throw new HttpException('A telemetry batch may contain at most 100 points', HttpStatus.BAD_REQUEST);
}
return this.telemetryService.enqueueBatch(body.points);
}
@Get('nearby')
@ApiOperation({ summary: 'Query active drivers within spatial radius with elevation & bearing 🚗' })
@ApiQuery({ name: 'lat', required: true, type: Number, description: 'Center latitude' })
@ApiQuery({ name: 'lng', required: true, type: Number, description: 'Center longitude' })
@ApiQuery({ name: 'radius', required: false, type: Number, description: 'Radius in meters (default: 5000m)' })
async getNearby(
@Query('lat') lat: number,
@Query('lng') lng: number,
@Query('radius') radius?: number,
) {
const latNum = Number(lat);
const lngNum = Number(lng);
if (isNaN(latNum) || isNaN(lngNum)) {
throw new HttpException('lat and lng must be valid numbers', HttpStatus.BAD_REQUEST);
}
return this.telemetryService.getRecentDrivers(latNum, lngNum, radius ? Number(radius) : 5000);
}
@Get('driver/:driverId/profile')
@ApiOperation({ summary: 'Get 3D elevation profile, climb, and terrain grade for a driver 📈' })
@ApiParam({ name: 'driverId', required: true, description: 'Driver unique ID' })
@ApiQuery({ name: 'hours', required: false, description: 'Window in hours (default: 24)' })
async getDriverElevationProfile(
@Param('driverId') driverId: string,
@Query('hours') hours?: number,
) {
return this.telemetryService.getElevationProfile(driverId, hours ? Number(hours) : 24);
}
}
@@ -0,0 +1,46 @@
import { Entity, Column, PrimaryGeneratedColumn, Index, CreateDateColumn } from 'typeorm';
@Entity('telemetry_logs')
@Index(['driverId', 'timestamp'], { unique: false })
export class TelemetryLog {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'driverId' })
@Index()
driverId: string;
@Column('decimal', { precision: 10, scale: 7 })
latitude: number;
@Column('decimal', { precision: 10, scale: 7 })
longitude: number;
@Column('float', { default: 0 })
speed: number;
@Column('float', { default: 0 })
heading: number;
// Cumulative or step distance traveled in meters (المسافة المقطوعة بالمتر)
@Column('float', { default: 0 })
distance: number;
// Elevation above mean sea level in meters (الارتفاع عن مستوى سطح البحر بالمتر AMSL)
@Column('float', { default: 0 })
elevation: number;
@CreateDateColumn({ type: 'timestamp with time zone' })
@Index()
timestamp: Date;
// PostGIS spatial point for ultra-fast spatial and proximity indexing
@Column({
type: 'geography',
spatialFeatureType: 'Point',
srid: 4326,
nullable: true,
})
@Index({ spatial: true })
location: any;
}
@@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TelemetryLog } from './telemetry.entity';
import { TelemetryService } from './telemetry.service';
import { TelemetryController } from './telemetry.controller';
import { RedisModule } from '../common/redis.module';
@Module({
imports: [
TypeOrmModule.forFeature([TelemetryLog]),
RedisModule,
],
controllers: [TelemetryController],
providers: [TelemetryService],
exports: [TelemetryService],
})
export class TelemetryModule {}
@@ -0,0 +1,122 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { TelemetryService } from './telemetry.service';
import { TelemetryLog } from './telemetry.entity';
import { RedisService } from '../common/redis.service';
describe('TelemetryService', () => {
let service: TelemetryService;
let mockRepo: any;
let mockDataSource: any;
let mockRedis: any;
beforeEach(async () => {
mockRepo = {
create: jest.fn().mockImplementation((dto) => ({ id: 42, ...dto })),
save: jest.fn().mockImplementation((entity) => Promise.resolve({ id: 42, ...entity })),
};
mockDataSource = {
query: jest.fn().mockResolvedValue([]),
};
mockRedis = {
set: jest.fn().mockResolvedValue(undefined),
get: jest.fn().mockResolvedValue(null),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
TelemetryService,
{
provide: getRepositoryToken(TelemetryLog),
useValue: mockRepo,
},
{
provide: DataSource,
useValue: mockDataSource,
},
{
provide: RedisService,
useValue: mockRedis,
},
],
}).compile();
service = module.get<TelemetryService>(TelemetryService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should ingest telemetry with elevation, distance, speed, and heading', async () => {
const payload = {
driver_id: 'test_driver_77',
latitude: 31.9539,
longitude: 35.9106,
speed: 60.5,
heading: 180.0,
distance: 1450.0,
elevation: 920.4,
};
const result = await service.ingest(payload);
expect(result.success).toBe(true);
expect(result.driver_id).toBe('test_driver_77');
expect(result.elevation).toBe(920.4);
expect(result.distance).toBe(1450.0);
expect(mockRepo.create).toHaveBeenCalledWith(
expect.objectContaining({
driverId: 'test_driver_77',
latitude: 31.9539,
longitude: 35.9106,
speed: 60.5,
heading: 180.0,
distance: 1450.0,
elevation: 920.4,
}),
);
expect(mockRepo.save).toHaveBeenCalled();
expect(mockRedis.set).toHaveBeenCalledWith(
'fleet:driver:test_driver_77:live',
expect.objectContaining({
driverId: 'test_driver_77',
elevation: 920.4,
distance: 1450.0,
}),
900,
);
});
it('should batch ingest multiple telemetry points with elevation', async () => {
const batch = [
{
driver_id: 'd1',
latitude: 31.95,
longitude: 35.91,
speed: 50,
heading: 90,
distance: 100,
elevation: 900,
},
{
driver_id: 'd2',
latitude: 31.96,
longitude: 35.92,
speed: 55,
heading: 95,
distance: 120,
elevation: 915,
},
];
const res = await service.ingestBatch(batch);
expect(res.success).toBe(true);
expect(res.count).toBe(2);
expect(mockRepo.save).toHaveBeenCalled();
});
});
+331
View File
@@ -0,0 +1,331 @@
import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { TelemetryLog } from './telemetry.entity';
import { DriverTelemetryDto } from './dto/driver-telemetry.dto';
import { RedisService } from '../common/redis.service';
@Injectable()
export class TelemetryService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(TelemetryService.name);
private readonly queueKey = 'telemetry:batch:queue';
private draining = false;
private queueTimer?: NodeJS.Timeout;
constructor(
@InjectRepository(TelemetryLog)
private readonly telemetryRepo: Repository<TelemetryLog>,
private readonly dataSource: DataSource,
private readonly redisService: RedisService,
) {}
async onModuleInit() {
try {
// Ensure PostGIS extension and telemetry_logs table columns exist
await this.dataSource.query(`
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE TABLE IF NOT EXISTS telemetry_logs (
id SERIAL PRIMARY KEY,
"driverId" VARCHAR(255) NOT NULL,
latitude NUMERIC(10, 7) NOT NULL,
longitude NUMERIC(10, 7) NOT NULL,
speed FLOAT NOT NULL DEFAULT 0,
heading FLOAT NOT NULL DEFAULT 0,
distance FLOAT DEFAULT 0,
elevation FLOAT DEFAULT 0,
timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
location GEOGRAPHY(Point, 4326)
);
ALTER TABLE telemetry_logs ADD COLUMN IF NOT EXISTS distance FLOAT DEFAULT 0;
ALTER TABLE telemetry_logs ADD COLUMN IF NOT EXISTS elevation FLOAT DEFAULT 0;
CREATE INDEX IF NOT EXISTS telemetry_logs_driver_idx ON telemetry_logs ("driverId");
CREATE INDEX IF NOT EXISTS telemetry_logs_timestamp_idx ON telemetry_logs (timestamp DESC);
CREATE INDEX IF NOT EXISTS telemetry_logs_location_idx ON telemetry_logs USING GIST (location);
`);
this.logger.log('✅ Telemetry schema verified: elevation and distance columns ready.');
} catch (err: any) {
this.logger.warn(`Telemetry DB auto-migration check note: ${err.message}`);
}
// The request path only queues data. A small background worker persists it
// in batches, keeping navigation uploads fast even when PostGIS is busy.
this.queueTimer = setInterval(() => void this.drainQueue(), 5_000);
void this.drainQueue();
}
onModuleDestroy() {
if (this.queueTimer) clearInterval(this.queueTimer);
}
async enqueueBatch(points: DriverTelemetryDto[]) {
if (!points?.length) return { success: true, accepted: 0, queued: 0 };
if (points.length > 100) {
throw new Error('A telemetry batch may contain at most 100 points.');
}
await this.redisService.getClient().lPush(this.queueKey, JSON.stringify(points));
const queuedBatches = await this.redisService.getClient().lLen(this.queueKey);
void this.drainQueue();
return { success: true, accepted: points.length, queuedBatches };
}
private async drainQueue() {
if (this.draining) return;
this.draining = true;
try {
// Limit a turn so requests and other Redis work stay responsive.
for (let i = 0; i < 20; i++) {
const raw = await this.redisService.getClient().rPop(this.queueKey);
if (!raw) break;
try {
const points = JSON.parse(raw) as DriverTelemetryDto[];
await this.ingestBatch(points);
} catch (error: any) {
// Put the original payload back at the head for a later retry.
await this.redisService.getClient().rPush(this.queueKey, raw);
this.logger.error(`Telemetry queue persistence failed: ${error.message}`);
break;
}
}
} catch (error: any) {
this.logger.error(`Telemetry queue worker failed: ${error.message}`);
} finally {
this.draining = false;
}
}
/**
* Ingest a single driver telemetry record including elevation & distance
*/
async ingest(data: DriverTelemetryDto) {
const lat = Number(data.latitude);
const lng = Number(data.longitude);
const speed = Number(data.speed || 0);
const heading = Number(data.heading || 0);
const distance = Number(data.distance || 0);
const elevation = Number(data.elevation || 0);
const log = this.telemetryRepo.create({
driverId: data.driver_id,
latitude: lat,
longitude: lng,
speed,
heading,
distance,
elevation,
timestamp: this.captureTime(data.timestamp),
location: {
type: 'Point',
coordinates: [lng, lat],
},
});
const saved = await this.telemetryRepo.save(log);
// Fast memory caching in Redis for real-time fleet queries (TTL 15 minutes)
try {
await this.redisService.set(
`fleet:driver:${data.driver_id}:live`,
{
driverId: data.driver_id,
latitude: lat,
longitude: lng,
speed,
heading,
distance,
elevation,
updatedAt: new Date().toISOString(),
},
900,
);
} catch (_) {
// Redis failover - non-blocking
}
return {
success: true,
id: saved.id,
driver_id: data.driver_id,
elevation,
distance,
timestamp: saved.timestamp,
};
}
/**
* Batch ingest multiple telemetry points
*/
async ingestBatch(points: DriverTelemetryDto[]) {
if (!points || points.length === 0) {
return { success: true, count: 0 };
}
const entities = points.map((p) => {
const lat = Number(p.latitude);
const lng = Number(p.longitude);
const speed = Number(p.speed || 0);
const heading = Number(p.heading || 0);
const distance = Number(p.distance || 0);
const elevation = Number(p.elevation || 0);
return this.telemetryRepo.create({
driverId: p.driver_id,
latitude: lat,
longitude: lng,
speed,
heading,
distance,
elevation,
timestamp: this.captureTime(p.timestamp),
location: {
type: 'Point',
coordinates: [lng, lat],
},
});
});
await this.telemetryRepo.save(entities);
// Update Redis cache for the latest point of each driver
try {
const latestByDriver = new Map<string, DriverTelemetryDto>();
for (const p of points) {
latestByDriver.set(p.driver_id, p);
}
for (const [dId, p] of latestByDriver.entries()) {
await this.redisService.set(
`fleet:driver:${dId}:live`,
{
driverId: dId,
latitude: Number(p.latitude),
longitude: Number(p.longitude),
speed: Number(p.speed || 0),
heading: Number(p.heading || 0),
distance: Number(p.distance || 0),
elevation: Number(p.elevation || 0),
updatedAt: new Date().toISOString(),
},
900,
);
}
} catch (_) {}
return {
success: true,
count: entities.length,
timestamp: new Date(),
};
}
private captureTime(value?: string): Date {
if (!value) return new Date();
const date = new Date(value);
return Number.isNaN(date.getTime()) ? new Date() : date;
}
/**
* Find nearby active drivers using PostGIS spatial geography search
*/
async getRecentDrivers(lat: number, lng: number, radiusMeters: number = 5000) {
const rows = await this.dataSource.query(
`SELECT DISTINCT ON ("driverId")
id, "driverId", latitude, longitude, speed, heading, distance, elevation, timestamp,
ST_Distance(location, ST_MakePoint($1, $2)::geography) as distance_to_center_meters
FROM telemetry_logs
WHERE ST_DWithin(location, ST_MakePoint($1, $2)::geography, $3)
AND timestamp >= NOW() - INTERVAL '4 hours'
ORDER BY "driverId", timestamp DESC
LIMIT 100`,
[lng, lat, radiusMeters],
);
return rows.map((r: any) => ({
driver_id: r.driverId,
latitude: parseFloat(r.latitude),
longitude: parseFloat(r.longitude),
speed: parseFloat(r.speed),
heading: parseFloat(r.heading),
distance: parseFloat(r.distance || 0),
elevation: parseFloat(r.elevation || 0),
timestamp: r.timestamp,
distance_to_center_meters: parseFloat(r.distance_to_center_meters),
}));
}
/**
* Calculate 3D elevation profile and vertical gradient for a specific driver
*/
async getElevationProfile(driverId: string, hours: number = 24) {
const points = await this.dataSource.query(
`SELECT latitude, longitude, speed, heading, distance, elevation, timestamp
FROM telemetry_logs
WHERE "driverId" = $1
AND timestamp >= NOW() - ($2 || ' hours')::interval
ORDER BY timestamp ASC`,
[driverId, hours],
);
if (points.length === 0) {
return {
driver_id: driverId,
hours,
pointsCount: 0,
minElevation: 0,
maxElevation: 0,
avgElevation: 0,
totalClimbMeters: 0,
totalDescentMeters: 0,
maxGradePercent: 0,
points: [],
};
}
let minElev = points[0].elevation || 0;
let maxElev = points[0].elevation || 0;
let sumElev = 0;
let totalClimb = 0;
let totalDescent = 0;
let maxGrade = 0;
for (let i = 0; i < points.length; i++) {
const elev = parseFloat(points[i].elevation || '0');
sumElev += elev;
if (elev < minElev) minElev = elev;
if (elev > maxElev) maxElev = elev;
if (i > 0) {
const prevElev = parseFloat(points[i - 1].elevation || '0');
const diff = elev - prevElev;
if (diff > 0) totalClimb += diff;
if (diff < 0) totalDescent += Math.abs(diff);
// Approximate grade percent if distance step is available
const stepDist = parseFloat(points[i].distance || '0') - parseFloat(points[i - 1].distance || '0');
if (stepDist > 10) {
const grade = (Math.abs(diff) / stepDist) * 100;
if (grade > maxGrade && grade < 50) {
maxGrade = grade;
}
}
}
}
return {
driver_id: driverId,
hours,
pointsCount: points.length,
minElevation: Math.round(minElev * 10) / 10,
maxElevation: Math.round(maxElev * 10) / 10,
avgElevation: Math.round((sumElev / points.length) * 10) / 10,
totalClimbMeters: Math.round(totalClimb * 10) / 10,
totalDescentMeters: Math.round(totalDescent * 10) / 10,
maxGradePercent: Math.round(maxGrade * 10) / 10,
recentPoints: points.slice(-30).map((p: any) => ({
latitude: parseFloat(p.latitude),
longitude: parseFloat(p.longitude),
speed: parseFloat(p.speed),
heading: parseFloat(p.heading),
elevation: parseFloat(p.elevation),
timestamp: p.timestamp,
})),
};
}
}
+93
View File
@@ -0,0 +1,93 @@
Copyright 2018 Boutros International. (http://www.boutrosfonts.com)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

+38 -21
View File
@@ -611,24 +611,41 @@
<section id="docs" class="page-section h-full">
<div class="flex h-full gap-8">
<!-- Docs Nav -->
<div class="w-64 flex flex-col gap-2 shrink-0">
<div class="mb-4">
<h2 class="text-xl font-black text-gradient" data-i18n="guides-title">Guides</h2>
<div class="w-64 flex flex-col gap-1.5 shrink-0 overflow-y-auto pr-1">
<div class="mb-2">
<h2 class="text-xs font-black uppercase tracking-wider text-slate-500" data-i18n="docs-overview">Overview</h2>
</div>
<a href="javascript:void(0)" data-section="getting-started" class="docs-nav-link active bg-blue-500/10 text-blue-400 p-4 rounded-2xl text-sm font-bold flex items-center gap-3 transition-all hover:bg-blue-500/5">
<i data-lucide="rocket" class="w-4 h-4"></i> Getting Started
<a href="javascript:void(0)" data-section="getting-started" class="docs-nav-link active bg-blue-500/10 text-blue-400 p-3.5 rounded-xl text-sm font-bold flex items-center gap-3 transition-all hover:bg-blue-500/5">
<i data-lucide="rocket" class="w-4 h-4 text-blue-400"></i> <span data-i18n="docs-getting-started">Getting Started</span>
</a>
<a href="javascript:void(0)" data-section="tiles-api" class="docs-nav-link p-4 rounded-2xl text-sm font-bold text-slate-500 flex items-center gap-3 transition-all hover:bg-white/5 hover:text-white">
<i data-lucide="map" class="w-4 h-4"></i> Map Tiles API
<div class="mt-4 mb-2">
<h2 class="text-xs font-black uppercase tracking-wider text-slate-500" data-i18n="docs-sdks-title">Client SDKs</h2>
</div>
<a href="javascript:void(0)" data-section="sdks-ios" class="docs-nav-link p-3.5 rounded-xl text-sm font-bold text-slate-400 flex items-center gap-3 transition-all hover:bg-white/5 hover:text-white">
<i data-lucide="apple" class="w-4 h-4 text-slate-300"></i> iOS SDK (Swift)
</a>
<a href="javascript:void(0)" data-section="geocoding-api" class="docs-nav-link p-4 rounded-2xl text-sm font-bold text-slate-500 flex items-center gap-3 transition-all hover:bg-white/5 hover:text-white">
<i data-lucide="search" class="w-4 h-4"></i> Geocoding API
<a href="javascript:void(0)" data-section="sdks-android" class="docs-nav-link p-3.5 rounded-xl text-sm font-bold text-slate-400 flex items-center gap-3 transition-all hover:bg-white/5 hover:text-white">
<i data-lucide="smartphone" class="w-4 h-4 text-emerald-400"></i> Android SDK (Kotlin)
</a>
<a href="javascript:void(0)" data-section="routing-api" class="docs-nav-link p-4 rounded-2xl text-sm font-bold text-slate-500 flex items-center gap-3 transition-all hover:bg-white/5 hover:text-white">
<i data-lucide="navigation" class="w-4 h-4"></i> Routing API
<a href="javascript:void(0)" data-section="sdks-flutter" class="docs-nav-link p-3.5 rounded-xl text-sm font-bold text-slate-400 flex items-center gap-3 transition-all hover:bg-white/5 hover:text-white">
<i data-lucide="layers" class="w-4 h-4 text-cyan-400"></i> Flutter SDK (Dart)
</a>
<a href="javascript:void(0)" data-section="sdks" class="docs-nav-link p-4 rounded-2xl text-sm font-bold text-slate-500 flex items-center gap-3 transition-all hover:bg-white/5 hover:text-white">
<i data-lucide="package" class="w-4 h-4"></i> SDKs & Client Libraries
<a href="javascript:void(0)" data-section="sdks-web" class="docs-nav-link p-3.5 rounded-xl text-sm font-bold text-slate-400 flex items-center gap-3 transition-all hover:bg-white/5 hover:text-white">
<i data-lucide="globe" class="w-4 h-4 text-amber-400"></i> JavaScript / TS SDK
</a>
<div class="mt-4 mb-2">
<h2 class="text-xs font-black uppercase tracking-wider text-slate-500" data-i18n="docs-rest-title">REST APIs</h2>
</div>
<a href="javascript:void(0)" data-section="tiles-api" class="docs-nav-link p-3.5 rounded-xl text-sm font-bold text-slate-400 flex items-center gap-3 transition-all hover:bg-white/5 hover:text-white">
<i data-lucide="map" class="w-4 h-4 text-blue-400"></i> Map Vector Tiles
</a>
<a href="javascript:void(0)" data-section="geocoding-api" class="docs-nav-link p-3.5 rounded-xl text-sm font-bold text-slate-400 flex items-center gap-3 transition-all hover:bg-white/5 hover:text-white">
<i data-lucide="search" class="w-4 h-4 text-violet-400"></i> Geocoding & Places
</a>
<a href="javascript:void(0)" data-section="routing-api" class="docs-nav-link p-3.5 rounded-xl text-sm font-bold text-slate-400 flex items-center gap-3 transition-all hover:bg-white/5 hover:text-white">
<i data-lucide="navigation" class="w-4 h-4 text-rose-400"></i> Routing & Directions
</a>
</div>
@@ -669,13 +686,13 @@
</div>
<!-- core script -->
<script src="js/i18n.js"></script>
<script src="js/auth.js"></script>
<script src="js/docs.js"></script>
<script src="js/app.js"></script>
<script src="js/playground.js"></script>
<script src="js/analytics.js"></script>
<script src="js/billing.js"></script>
<script src="js/refinement.js"></script>
<script src="js/i18n.js?v=2.2"></script>
<script src="js/auth.js?v=2.2"></script>
<script src="js/docs.js?v=2.2"></script>
<script src="js/app.js?v=2.2"></script>
<script src="js/playground.js?v=2.2"></script>
<script src="js/analytics.js?v=2.2"></script>
<script src="js/billing.js?v=2.2"></script>
<script src="js/refinement.js?v=2.2"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+501
View File
@@ -0,0 +1,501 @@
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>المنظومة الوطنية للسيادة المكانية والملاحة التراثية | مذكرة الطرح الاستراتيجي</title>
<meta name="description" content="مذكرة استراتيجية وهندسية موجهة لوزارة الثقافة ودائرة الآثار العامة لتحويل التراث الأردني إلى منظومة ملاحة مكانية سيادية تعمل بدون إنترنت.">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;500;700;800;900&family=Plus+Jakarta+Sans:wght@500;700;800&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">
<style>
:root {
--navy: #061d42;
--navy-light: #0d2e61;
--gold: #b59247;
--gold-light: #e5cb8e;
--gold-bg: rgba(181, 146, 71, 0.08);
--ink: #102238;
--muted: #596b82;
--pale: #f4f7fb;
--line: #dbe4ef;
--white: #ffffff;
--font: 'Tajawal', -apple-system, BlinkMacSystemFont, 'Segoe UI', Tahoma, sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: var(--font);
background-color: var(--pale);
color: var(--ink);
line-height: 1.85;
font-size: 17px;
direction: rtl;
text-align: right;
}
.container {
max-width: 1080px;
margin: 0 auto;
padding: 40px 24px 80px;
}
/* Header & Branding */
header {
background: var(--navy);
color: var(--white);
border-radius: 24px;
padding: 50px 45px;
margin-bottom: 40px;
position: relative;
overflow: hidden;
box-shadow: 0 20px 45px rgba(6, 29, 66, 0.15);
}
header::before {
content: '';
position: absolute;
top: -120px;
left: -80px;
width: 400px;
height: 400px;
background: radial-gradient(circle, rgba(181, 146, 71, 0.25), transparent 70%);
pointer-events: none;
}
.tagline {
display: inline-flex;
align-items: center;
gap: 10px;
background: rgba(181, 146, 71, 0.15);
border: 1px solid rgba(181, 146, 71, 0.35);
color: var(--gold-light);
padding: 6px 16px;
border-radius: 30px;
font-size: 14px;
font-weight: 700;
margin-bottom: 20px;
}
h1 {
font-size: clamp(28px, 4vw, 42px);
font-weight: 900;
line-height: 1.3;
margin-bottom: 12px;
color: var(--white);
}
.subtitle {
font-size: clamp(18px, 2.2vw, 22px);
color: #b8ccdf;
font-weight: 500;
margin-bottom: 30px;
}
.meta-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.15);
padding-top: 25px;
font-size: 14px;
}
.meta-item b {
color: var(--gold-light);
display: block;
margin-bottom: 4px;
}
.meta-item span {
color: #e2ebf4;
}
/* Content Cards */
.card {
background: var(--white);
border-radius: 20px;
border: 1px solid var(--line);
padding: 40px;
margin-bottom: 35px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.03);
}
h2 {
font-size: 26px;
font-weight: 800;
color: var(--navy);
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 12px;
}
h2::before {
content: '';
display: inline-block;
width: 6px;
height: 26px;
background: var(--gold);
border-radius: 4px;
}
h3 {
font-size: 20px;
font-weight: 700;
color: var(--navy-light);
margin: 24px 0 12px;
}
p {
margin-bottom: 16px;
color: var(--ink);
}
.highlight-box {
background: var(--gold-bg);
border-right: 4px solid var(--gold);
padding: 20px 24px;
border-radius: 12px;
margin: 24px 0;
font-size: 18px;
font-weight: 700;
color: var(--navy);
line-height: 1.7;
}
/* Table Styling */
.table-wrapper {
overflow-x: auto;
margin: 25px 0;
border-radius: 14px;
border: 1px solid var(--line);
}
table {
width: 100%;
border-collapse: collapse;
text-align: right;
font-size: 15px;
}
th {
background: var(--navy);
color: var(--white);
padding: 16px 20px;
font-weight: 800;
font-size: 16px;
}
td {
padding: 16px 20px;
border-bottom: 1px solid var(--line);
color: var(--ink);
}
tr:nth-child(even) td {
background: #fafbfd;
}
tr:hover td {
background: #f1f6fb;
}
/* Code & Architecture */
pre {
background: var(--navy);
color: #d1e2f7;
padding: 24px;
border-radius: 14px;
overflow-x: auto;
font-family: 'JetBrains Mono', monospace;
font-size: 13.5px;
direction: ltr;
text-align: left;
margin: 20px 0;
line-height: 1.6;
}
/* List styles */
ul, ol {
margin: 16px 24px 24px 0;
}
li {
margin-bottom: 10px;
}
/* Badges & Steps */
.badge {
display: inline-block;
font-size: 12px;
font-weight: 700;
padding: 3px 10px;
border-radius: 20px;
background: #e9f0f9;
color: #1a4f8b;
}
.phase-card {
border: 1px solid var(--line);
border-radius: 14px;
padding: 24px;
margin-bottom: 16px;
background: #fafbfd;
transition: all .2s;
}
.phase-card:hover {
border-color: var(--gold);
box-shadow: 0 8px 24px rgba(6, 29, 66, 0.06);
}
.phase-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.phase-header strong {
font-size: 18px;
color: var(--navy);
}
/* Action Footer */
.cta-footer {
background: var(--navy);
color: var(--white);
border-radius: 20px;
padding: 35px;
text-align: center;
margin-top: 50px;
}
.cta-footer h3 {
color: var(--gold-light);
font-size: 24px;
margin-bottom: 12px;
}
.cta-footer p {
color: #cfddec;
max-width: 750px;
margin: 0 auto 20px;
}
.btn-gold {
display: inline-flex;
align-items: center;
gap: 10px;
background: var(--gold);
color: var(--navy);
font-weight: 800;
padding: 14px 32px;
border-radius: 10px;
text-decoration: none;
transition: all .2s;
}
.btn-gold:hover {
background: var(--gold-light);
transform: translateY(-2px);
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<header>
<div class="tagline">مبادرة وطنية للسيادة المكانية والتراث الرقمي</div>
<h1>المنظومة الوطنية للسيادة المكانية والملاحة التراثية</h1>
<div class="subtitle">مذكرة الطرح الاستراتيجي والمعماري الموحد للشراكة الحكومية</div>
<div class="meta-bar">
<div class="meta-item">
<b>المُعدّ والخبير الاستراتيجي:</b>
<span>حمزة عايد (Founding Tech Architect & Mobility Strategist)</span>
</div>
<div class="meta-item">
<b>المرجع والتوثيق:</b>
<span>التدقيق الاستقصائي المقارن لبيانات التراث الأردني (سبتمبر 2026)</span>
</div>
<div class="meta-item">
<b>الجهات الشريكة المستهدفة:</b>
<span>وزارة الثقافة · دائرة الآثار العامة · وزارة السياحة · هيئة النقل البري</span>
</div>
</div>
</header>
<!-- Section 1 -->
<section class="card">
<h2>1. الملخص التنفيذي وقراءة الفجوة الوطنية</h2>
<div class="highlight-box">
"تمتلك المملكة الأردنية الهاشمية أثمن محتوى تراثي وأضخم قاعدة بيانات أثرية موثقة في المنطقة، لكنها تعاني من عجز ميداني كامل في نقل هذه البيانات من الرفوف الرقمية إلى حركة الملاحة الواقعية".
</div>
<p>
كشف التدقيق الاستقصائي المشترك للمنصات الرقمية الأردنية القائمة عن انفصال حاد بين ثلاث جزر مؤسسية تعمل كل منها بمعزل عن الأخرى:
</p>
<ol>
<li>
<b>وزارة الثقافة (منصة تراثي ومشاريع التوثيق):</b>
تمتلك رصيداً معرفياً هائلاً يشمل <b>75,000 مفردة</b> موثقة في 5 مجلدات علمية بـ "مكنز التراث الشعبي الأردني"، ومئات الساعات الصوتية النادرة لأشرطة الكاسيت المسجلة في السبعينيات، و6 مسارات تشمل 65 محطة، ومشروع السردية الأردنية.
<br>
<i>العجز الفعلي:</i> غياب تام لتطبيق هواتف ذكية رسمي (موقع ويب فقط)، انعدام الملاحة والتوجيه المنعطف بمنعطف، عجز كامل عن العمل دون اتصال بالإنترنت (Offline)، واعتماد دبابيس عشوائية تسقط في المركز الهندسي للمعلم بدلاً من بوابات الدخول ومواقف السيارات.
</li>
<li>
<b>دائرة الآثار العامة (نظام ميجا الأردن - MEGA-Jordan):</b>
تمتلك سجلاً مكانياً جغرافياً ضخماً يوثق أكثر من <b>15,000 موقع أثري و54,000 مكوّن موقعي</b>، مع مسار ترحيل جارٍ نحو منصة Arches v5 بالشراكة مع معهد جيتي وجامعة أكسفورد.
<br>
<i>العجز الفعلي:</i> نظام إداري ورقابي داخلي مغلق مخصص لحماية الآثار ومعاملات تراخيص الأبنية، واجهته العامة غير مستقرة، ومفصول كلياً عن خدمة الزوار أو تزويدهم بالملاحة.
</li>
<li>
<b>قطاع السياحة والنقل والحركة اليومية:</b>
جزر رقمية منفصلة (هيئة تنشيط السياحة، برنامج أردننا جنة، هيئة تنظيم النقل البري، الحرفيين في المحافظات) دون أي ترابط مع قواعد بيانات التراث.
</li>
</ol>
</section>
<!-- Section 2 -->
<section class="card">
<h2>2. مصفوفة القيمة: تحويل العجز الحكومي إلى تفوق سيادي</h2>
<p>مقارنة مباشرة بين الواقع التشغيلي الحالي وما تقدمه منصتنا السيادية كشريك تقني وطني:</p>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th style="width: 25%;">المعيار التشغيلي الميداني</th>
<th style="width: 35%;">الواقع الحكومي الحالي (تراثي / ميجا)</th>
<th style="width: 40%;">حل منصة الخرائط السيادية (Map SaaS)</th>
</tr>
</thead>
<tbody>
<tr>
<td><b>الملاحة الميدانية ودقة الوصول</b></td>
<td>إلقاء دبوس عشوائي في منتصف الموقع يقود السائح لجروف صخرية أو أسوار مغلقة.</td>
<td><b>نظام التوجيه المزدوج:</b> ملاحة قيادة سيارات حتى موقف المركبات المعتمد، ثم ملاحة مشاة فورية نحو بوابة التذاكر والمسار الداخلي الآمن.</td>
</tr>
<tr>
<td><b>العمل دون اتصال بالإنترنت</b></td>
<td>منعدم كلياً؛ شلل كامل للخدمة في وادي رم، فينان، الكرك، البترا، وقلاع البادية عند انقطاع الشبكة الخلوية.</td>
<td><b>حزم جغرافية محلية مسبقة التنزيل (Offline):</b> خرائط متجهات وتوجيه وتوثيق صوتي يعمل بنسبة 100% بنظام GPS بلا إنترنت.</td>
</tr>
<tr>
<td><b>تكاليف التراخيص والسيادة الرقمية</b></td>
<td>تبعية دولارية تصاعدية لواجهات خرائط جوجل (Google Maps APIs) ترهق موازنات الوزارات.</td>
<td><b>بنية تحتية محلية مستقلة:</b> خوادم متجهات وطنية سيادية مستضافة محلياً تمنح الدولة استخداماً غير محدود بلا أي كلفة ترخيص أجنبية.</td>
</tr>
<tr>
<td><b>تفعيل خريطة الحكايات</b></td>
<td>تبويب نصي مكتبي ثابت يعتمد على قراءة المستخدم من شاشة الكمبيوتر.</td>
<td><b>السياج الجغرافي الصوتي (Audio Geofencing):</b> تشغيل التسجيلات الصوتية النادرة لحكاية المكان تلقائياً بمجرد اقتراب السائح من المعلم دون لمس الشاشة.</td>
</tr>
<tr>
<td><b>الربط بالنقل والتمكين الاقتصادي</b></td>
<td>انفصال تام عن حركة المركبات والحافلات وأرزاق الحرفيين في القرى والمحافظات.</td>
<td><b>تكامل حركي فوري:</b> زر مباشر لطلب وسيلة نقل نحو المعلم، وربط مشاغل الحرفيين بالدفع الرقمي المباشر.</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- Section 3 -->
<section class="card">
<h2>3. المعمارية الهندسية المتكاملة لربط المنظومات الوطنية</h2>
<p>المخطط الهيكلي الذي يربط المحتوى التراثي لوزارة الثقافة مع سجلات الآثار ومنظومة الملاحة والنقل الذكي:</p>
<pre>
┌──────────────────────────────────────┐ ┌─────────────────────────────────────┐
│ وزارة الثقافة (منصة تراثي) │ │ دائرة الآثار العامة │
│ المكنز (75 ألف مفردة) • السردية │ │ نظام MEGA-Jordan (15 ألف موقع) │
│ أرشيف كاسيت السبعينيات النادر │ │ سجل Arches v5 │
└──────────────────┬───────────────────┘ └──────────────────┬──────────────────┘
│ │
▼ ▼
┌────────────────────────────────────────────────────────────┐
│ الطبقة الجغرافية السيادية للتراث (Heritage Graph Engine) │
│ محرك PostGIS • خادم بلاطات متجهة وطني • إحداثيات البوابات │
│ معرف تراثي وطني موحد (Unified Heritage Place ID) │
└────────────────────────────┬───────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ تطبيق الهاتف الذكي السيادي│ │ بوابة النقل والتمكين │
│ • توجيه مزدوج (سيارة+مشاة)│ │ • ربط حافلات وتطبيقات │
│ • حزم تنزيل Offline │ │ • دعم الحرفيين والدفع │
│ • سياج صوتي للحكايات │ │ • خفض تكلفة جوجل ($0) │
└─────────────────────────┘ └─────────────────────────┘
</pre>
</section>
<!-- Section 4 -->
<section class="card">
<h2>4. نموذج هيكل البيانات المكاني المقترح (Data Schema)</h2>
<p>هيكل بيانات موحد يربط أنظمة الدولة الثلاثة دون أي تضارب أو كشف للمواقع الأثرية الحساسة غير المفتوحة للزيارة:</p>
<pre>
-- هيكل بيانات المعلم التراثي السيادي الموحد
CREATE TABLE sovereign_heritage_asset (
heritage_id VARCHAR(64) PRIMARY KEY, -- المعرف الوطني الموحد (مرتبط بـ MEGA و Turathi)
site_name_ar VARCHAR(255) NOT NULL, -- الاسم الرسمي بالعربية
site_name_en VARCHAR(255), -- الاسم بالإنجليزية
governorate VARCHAR(64) NOT NULL, -- المحافظة (معان، مادبا، جرش، إلخ)
category VARCHAR(64) NOT NULL, -- التصنيف (يونسكو، قلاع، متاحف، مسارات)
-- الفصل بين الإحداثيات الإدارية والملاحة الميدانية
centroid_location GEOMETRY(Point, 4326), -- المركز الجغرافي العام
parking_gate_location GEOMETRY(Point, 4326), -- موقع موقف السيارات ونقطة إنزال الركاب
visitor_entrance_location GEOMETRY(Point, 4326), -- بوابة التذاكر ودخول المشاة الفعلي
-- الأصول الثقافية والصوتية الوطنية
story_summary_60s TEXT, -- نبذة الـ 60 ثانية المركزة
audio_narrative_url VARCHAR(512), -- تسجيلات أشرطة السبعينيات النادرة
thesaurus_terms JSONB, -- مفردات المكنز التراثي الأردني المرتبطة بالمكان
historical_era VARCHAR(128), -- الحقبة وفق مشروع السردية الأردنية
-- معايير السلامة والتشغيل
is_offline_available BOOLEAN DEFAULT TRUE, -- هل المعلم مشمول بحزمة التنزيل المسبق
requires_ticket BOOLEAN DEFAULT FALSE, -- هل يتطلب تذكرة دخول
accessibility_level VARCHAR(32) -- مستوى المواءمة لذوي الإعاقة وكبار السن
);
</pre>
</section>
<!-- Section 5 -->
<section class="card">
<h2>5. خطة العمل والشراكة الحكومية المقترحة</h2>
<div class="phase-card">
<div class="phase-header">
<strong>المرحلة الأولى: تقديم ورقة المفهوم المشتركة (Concept Note)</strong>
<span class="badge">الشهر 1 - 2</span>
</div>
<p>عرض المذكرة التنفيذية على وزير الثقافة وأمين عام الوزارة ومدير عام الآثار العامة، والتأكيد على مبدأ التكامل السيادي: الوزارة توفر المحتوى وسردية الأرض والإنسان والاعتماد الرسمي، ونحن نوفر البنية المكانية ومحرك الخرائط المستقل.</p>
</div>
<div class="phase-card">
<div class="phase-header">
<strong>المرحلة الثانية: إطلاق المسار التجريبي الريادي (Flagship PoC)</strong>
<span class="badge">الشهر 3 - 5</span>
</div>
<p>اختيار مسار نموذجي مثل <b>مسار مادبا التراثي (10 محطات)</b> أو <b>مسار عجلون الزيتون (10 محطات)</b>، ورفع إحداثيات البوابات والمواقف الحقيقية، ودمج التسجيلات الصوتية الشعبية النادرة ومفردات المكنز داخل تطبيق الموبايل، وتشغيله أمام قيادات الوزارة وهيئة تنشيط السياحة بالكامل في وضع عدم الاتصال لإثبات الفارق التقني الميداني.</p>
</div>
<div class="phase-card">
<div class="phase-header">
<strong>المرحلة الثالثة: التعميم الوطني والربط الاقتصادي</strong>
<span class="badge">الشهر 6 - 12</span>
</div>
<p>شمول الـ 65 محطة المعتمدة في منصة تراثي، والتوسع تدريجياً ليشمل الـ 15 ألف موقع أثري المسجلة في نظام ميجا بصيغة سياحية آمنة، مع ربط خطوط النقل السياحي وتطبيقات التوصيل لإنعاش اقتصاد المحافظات ودعم الحرفيين المحليين.</p>
</div>
</section>
<!-- CTA Footer -->
<div class="cta-footer">
<h3>جاهزية المنظومة للتنفيذ الفوري</h3>
<p>تمتلك منصتنا اليوم خوادم المتجهات الوطنية ومحرك التوجيه السيادي وتطبيق الموبايل الميداني الجاهز للتكامل مع قواعد بيانات الدولة الأردنية لخدمة أهداف التنمية والسيادة الرقمية.</p>
<a href="https://map-saas.intaleqapp.com/sovereignty-brief.html" class="btn-gold">
<span>معاينة العرض التنفيذي للسيادة المكانية</span>
<span>←</span>
</a>
</div>
</div>
</body>
</html>
@@ -0,0 +1,501 @@
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>المنظومة الوطنية للسيادة المكانية والملاحة التراثية | مذكرة الطرح الاستراتيجي</title>
<meta name="description" content="مذكرة استراتيجية وهندسية موجهة لوزارة الثقافة ودائرة الآثار العامة لتحويل التراث الأردني إلى منظومة ملاحة مكانية سيادية تعمل بدون إنترنت.">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;500;700;800;900&family=Plus+Jakarta+Sans:wght@500;700;800&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">
<style>
:root {
--navy: #061d42;
--navy-light: #0d2e61;
--gold: #b59247;
--gold-light: #e5cb8e;
--gold-bg: rgba(181, 146, 71, 0.08);
--ink: #102238;
--muted: #596b82;
--pale: #f4f7fb;
--line: #dbe4ef;
--white: #ffffff;
--font: 'Tajawal', -apple-system, BlinkMacSystemFont, 'Segoe UI', Tahoma, sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: var(--font);
background-color: var(--pale);
color: var(--ink);
line-height: 1.85;
font-size: 17px;
direction: rtl;
text-align: right;
}
.container {
max-width: 1080px;
margin: 0 auto;
padding: 40px 24px 80px;
}
/* Header & Branding */
header {
background: var(--navy);
color: var(--white);
border-radius: 24px;
padding: 50px 45px;
margin-bottom: 40px;
position: relative;
overflow: hidden;
box-shadow: 0 20px 45px rgba(6, 29, 66, 0.15);
}
header::before {
content: '';
position: absolute;
top: -120px;
left: -80px;
width: 400px;
height: 400px;
background: radial-gradient(circle, rgba(181, 146, 71, 0.25), transparent 70%);
pointer-events: none;
}
.tagline {
display: inline-flex;
align-items: center;
gap: 10px;
background: rgba(181, 146, 71, 0.15);
border: 1px solid rgba(181, 146, 71, 0.35);
color: var(--gold-light);
padding: 6px 16px;
border-radius: 30px;
font-size: 14px;
font-weight: 700;
margin-bottom: 20px;
}
h1 {
font-size: clamp(28px, 4vw, 42px);
font-weight: 900;
line-height: 1.3;
margin-bottom: 12px;
color: var(--white);
}
.subtitle {
font-size: clamp(18px, 2.2vw, 22px);
color: #b8ccdf;
font-weight: 500;
margin-bottom: 30px;
}
.meta-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.15);
padding-top: 25px;
font-size: 14px;
}
.meta-item b {
color: var(--gold-light);
display: block;
margin-bottom: 4px;
}
.meta-item span {
color: #e2ebf4;
}
/* Content Cards */
.card {
background: var(--white);
border-radius: 20px;
border: 1px solid var(--line);
padding: 40px;
margin-bottom: 35px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.03);
}
h2 {
font-size: 26px;
font-weight: 800;
color: var(--navy);
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 12px;
}
h2::before {
content: '';
display: inline-block;
width: 6px;
height: 26px;
background: var(--gold);
border-radius: 4px;
}
h3 {
font-size: 20px;
font-weight: 700;
color: var(--navy-light);
margin: 24px 0 12px;
}
p {
margin-bottom: 16px;
color: var(--ink);
}
.highlight-box {
background: var(--gold-bg);
border-right: 4px solid var(--gold);
padding: 20px 24px;
border-radius: 12px;
margin: 24px 0;
font-size: 18px;
font-weight: 700;
color: var(--navy);
line-height: 1.7;
}
/* Table Styling */
.table-wrapper {
overflow-x: auto;
margin: 25px 0;
border-radius: 14px;
border: 1px solid var(--line);
}
table {
width: 100%;
border-collapse: collapse;
text-align: right;
font-size: 15px;
}
th {
background: var(--navy);
color: var(--white);
padding: 16px 20px;
font-weight: 800;
font-size: 16px;
}
td {
padding: 16px 20px;
border-bottom: 1px solid var(--line);
color: var(--ink);
}
tr:nth-child(even) td {
background: #fafbfd;
}
tr:hover td {
background: #f1f6fb;
}
/* Code & Architecture */
pre {
background: var(--navy);
color: #d1e2f7;
padding: 24px;
border-radius: 14px;
overflow-x: auto;
font-family: 'JetBrains Mono', monospace;
font-size: 13.5px;
direction: ltr;
text-align: left;
margin: 20px 0;
line-height: 1.6;
}
/* List styles */
ul, ol {
margin: 16px 24px 24px 0;
}
li {
margin-bottom: 10px;
}
/* Badges & Steps */
.badge {
display: inline-block;
font-size: 12px;
font-weight: 700;
padding: 3px 10px;
border-radius: 20px;
background: #e9f0f9;
color: #1a4f8b;
}
.phase-card {
border: 1px solid var(--line);
border-radius: 14px;
padding: 24px;
margin-bottom: 16px;
background: #fafbfd;
transition: all .2s;
}
.phase-card:hover {
border-color: var(--gold);
box-shadow: 0 8px 24px rgba(6, 29, 66, 0.06);
}
.phase-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.phase-header strong {
font-size: 18px;
color: var(--navy);
}
/* Action Footer */
.cta-footer {
background: var(--navy);
color: var(--white);
border-radius: 20px;
padding: 35px;
text-align: center;
margin-top: 50px;
}
.cta-footer h3 {
color: var(--gold-light);
font-size: 24px;
margin-bottom: 12px;
}
.cta-footer p {
color: #cfddec;
max-width: 750px;
margin: 0 auto 20px;
}
.btn-gold {
display: inline-flex;
align-items: center;
gap: 10px;
background: var(--gold);
color: var(--navy);
font-weight: 800;
padding: 14px 32px;
border-radius: 10px;
text-decoration: none;
transition: all .2s;
}
.btn-gold:hover {
background: var(--gold-light);
transform: translateY(-2px);
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<header>
<div class="tagline">مبادرة وطنية للسيادة المكانية والتراث الرقمي</div>
<h1>المنظومة الوطنية للسيادة المكانية والملاحة التراثية</h1>
<div class="subtitle">مذكرة الطرح الاستراتيجي والمعماري الموحد للشراكة الحكومية</div>
<div class="meta-bar">
<div class="meta-item">
<b>المُعدّ والخبير الاستراتيجي:</b>
<span>حمزة عايد (Founding Tech Architect & Mobility Strategist)</span>
</div>
<div class="meta-item">
<b>المرجع والتوثيق:</b>
<span>التدقيق الاستقصائي المقارن لبيانات التراث الأردني (سبتمبر 2026)</span>
</div>
<div class="meta-item">
<b>الجهات الشريكة المستهدفة:</b>
<span>وزارة الثقافة · دائرة الآثار العامة · وزارة السياحة · هيئة النقل البري</span>
</div>
</div>
</header>
<!-- Section 1 -->
<section class="card">
<h2>1. الملخص التنفيذي وقراءة الفجوة الوطنية</h2>
<div class="highlight-box">
"تمتلك المملكة الأردنية الهاشمية أثمن محتوى تراثي وأضخم قاعدة بيانات أثرية موثقة في المنطقة، لكنها تعاني من عجز ميداني كامل في نقل هذه البيانات من الرفوف الرقمية إلى حركة الملاحة الواقعية".
</div>
<p>
كشف التدقيق الاستقصائي المشترك للمنصات الرقمية الأردنية القائمة عن انفصال حاد بين ثلاث جزر مؤسسية تعمل كل منها بمعزل عن الأخرى:
</p>
<ol>
<li>
<b>وزارة الثقافة (منصة تراثي ومشاريع التوثيق):</b>
تمتلك رصيداً معرفياً هائلاً يشمل <b>75,000 مفردة</b> موثقة في 5 مجلدات علمية بـ "مكنز التراث الشعبي الأردني"، ومئات الساعات الصوتية النادرة لأشرطة الكاسيت المسجلة في السبعينيات، و6 مسارات تشمل 65 محطة، ومشروع السردية الأردنية.
<br>
<i>العجز الفعلي:</i> غياب تام لتطبيق هواتف ذكية رسمي (موقع ويب فقط)، انعدام الملاحة والتوجيه المنعطف بمنعطف، عجز كامل عن العمل دون اتصال بالإنترنت (Offline)، واعتماد دبابيس عشوائية تسقط في المركز الهندسي للمعلم بدلاً من بوابات الدخول ومواقف السيارات.
</li>
<li>
<b>دائرة الآثار العامة (نظام ميجا الأردن - MEGA-Jordan):</b>
تمتلك سجلاً مكانياً جغرافياً ضخماً يوثق أكثر من <b>15,000 موقع أثري و54,000 مكوّن موقعي</b>، مع مسار ترحيل جارٍ نحو منصة Arches v5 بالشراكة مع معهد جيتي وجامعة أكسفورد.
<br>
<i>العجز الفعلي:</i> نظام إداري ورقابي داخلي مغلق مخصص لحماية الآثار ومعاملات تراخيص الأبنية، واجهته العامة غير مستقرة، ومفصول كلياً عن خدمة الزوار أو تزويدهم بالملاحة.
</li>
<li>
<b>قطاع السياحة والنقل والحركة اليومية:</b>
جزر رقمية منفصلة (هيئة تنشيط السياحة، برنامج أردننا جنة، هيئة تنظيم النقل البري، الحرفيين في المحافظات) دون أي ترابط مع قواعد بيانات التراث.
</li>
</ol>
</section>
<!-- Section 2 -->
<section class="card">
<h2>2. مصفوفة القيمة: تحويل العجز الحكومي إلى تفوق سيادي</h2>
<p>مقارنة مباشرة بين الواقع التشغيلي الحالي وما تقدمه منصتنا السيادية كشريك تقني وطني:</p>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th style="width: 25%;">المعيار التشغيلي الميداني</th>
<th style="width: 35%;">الواقع الحكومي الحالي (تراثي / ميجا)</th>
<th style="width: 40%;">حل منصة الخرائط السيادية (Map SaaS)</th>
</tr>
</thead>
<tbody>
<tr>
<td><b>الملاحة الميدانية ودقة الوصول</b></td>
<td>إلقاء دبوس عشوائي في منتصف الموقع يقود السائح لجروف صخرية أو أسوار مغلقة.</td>
<td><b>نظام التوجيه المزدوج:</b> ملاحة قيادة سيارات حتى موقف المركبات المعتمد، ثم ملاحة مشاة فورية نحو بوابة التذاكر والمسار الداخلي الآمن.</td>
</tr>
<tr>
<td><b>العمل دون اتصال بالإنترنت</b></td>
<td>منعدم كلياً؛ شلل كامل للخدمة في وادي رم، فينان، الكرك، البترا، وقلاع البادية عند انقطاع الشبكة الخلوية.</td>
<td><b>حزم جغرافية محلية مسبقة التنزيل (Offline):</b> خرائط متجهات وتوجيه وتوثيق صوتي يعمل بنسبة 100% بنظام GPS بلا إنترنت.</td>
</tr>
<tr>
<td><b>تكاليف التراخيص والسيادة الرقمية</b></td>
<td>تبعية دولارية تصاعدية لواجهات خرائط جوجل (Google Maps APIs) ترهق موازنات الوزارات.</td>
<td><b>بنية تحتية محلية مستقلة:</b> خوادم متجهات وطنية سيادية مستضافة محلياً تمنح الدولة استخداماً غير محدود بلا أي كلفة ترخيص أجنبية.</td>
</tr>
<tr>
<td><b>تفعيل خريطة الحكايات</b></td>
<td>تبويب نصي مكتبي ثابت يعتمد على قراءة المستخدم من شاشة الكمبيوتر.</td>
<td><b>السياج الجغرافي الصوتي (Audio Geofencing):</b> تشغيل التسجيلات الصوتية النادرة لحكاية المكان تلقائياً بمجرد اقتراب السائح من المعلم دون لمس الشاشة.</td>
</tr>
<tr>
<td><b>الربط بالنقل والتمكين الاقتصادي</b></td>
<td>انفصال تام عن حركة المركبات والحافلات وأرزاق الحرفيين في القرى والمحافظات.</td>
<td><b>تكامل حركي فوري:</b> زر مباشر لطلب وسيلة نقل نحو المعلم، وربط مشاغل الحرفيين بالدفع الرقمي المباشر.</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- Section 3 -->
<section class="card">
<h2>3. المعمارية الهندسية المتكاملة لربط المنظومات الوطنية</h2>
<p>المخطط الهيكلي الذي يربط المحتوى التراثي لوزارة الثقافة مع سجلات الآثار ومنظومة الملاحة والنقل الذكي:</p>
<pre>
┌──────────────────────────────────────┐ ┌─────────────────────────────────────┐
│ وزارة الثقافة (منصة تراثي) │ │ دائرة الآثار العامة │
│ المكنز (75 ألف مفردة) • السردية │ │ نظام MEGA-Jordan (15 ألف موقع) │
│ أرشيف كاسيت السبعينيات النادر │ │ سجل Arches v5 │
└──────────────────┬───────────────────┘ └──────────────────┬──────────────────┘
│ │
▼ ▼
┌────────────────────────────────────────────────────────────┐
│ الطبقة الجغرافية السيادية للتراث (Heritage Graph Engine) │
│ محرك PostGIS • خادم بلاطات متجهة وطني • إحداثيات البوابات │
│ معرف تراثي وطني موحد (Unified Heritage Place ID) │
└────────────────────────────┬───────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ تطبيق الهاتف الذكي السيادي│ │ بوابة النقل والتمكين │
│ • توجيه مزدوج (سيارة+مشاة)│ │ • ربط حافلات وتطبيقات │
│ • حزم تنزيل Offline │ │ • دعم الحرفيين والدفع │
│ • سياج صوتي للحكايات │ │ • خفض تكلفة جوجل ($0) │
└─────────────────────────┘ └─────────────────────────┘
</pre>
</section>
<!-- Section 4 -->
<section class="card">
<h2>4. نموذج هيكل البيانات المكاني المقترح (Data Schema)</h2>
<p>هيكل بيانات موحد يربط أنظمة الدولة الثلاثة دون أي تضارب أو كشف للمواقع الأثرية الحساسة غير المفتوحة للزيارة:</p>
<pre>
-- هيكل بيانات المعلم التراثي السيادي الموحد
CREATE TABLE sovereign_heritage_asset (
heritage_id VARCHAR(64) PRIMARY KEY, -- المعرف الوطني الموحد (مرتبط بـ MEGA و Turathi)
site_name_ar VARCHAR(255) NOT NULL, -- الاسم الرسمي بالعربية
site_name_en VARCHAR(255), -- الاسم بالإنجليزية
governorate VARCHAR(64) NOT NULL, -- المحافظة (معان، مادبا، جرش، إلخ)
category VARCHAR(64) NOT NULL, -- التصنيف (يونسكو، قلاع، متاحف، مسارات)
-- الفصل بين الإحداثيات الإدارية والملاحة الميدانية
centroid_location GEOMETRY(Point, 4326), -- المركز الجغرافي العام
parking_gate_location GEOMETRY(Point, 4326), -- موقع موقف السيارات ونقطة إنزال الركاب
visitor_entrance_location GEOMETRY(Point, 4326), -- بوابة التذاكر ودخول المشاة الفعلي
-- الأصول الثقافية والصوتية الوطنية
story_summary_60s TEXT, -- نبذة الـ 60 ثانية المركزة
audio_narrative_url VARCHAR(512), -- تسجيلات أشرطة السبعينيات النادرة
thesaurus_terms JSONB, -- مفردات المكنز التراثي الأردني المرتبطة بالمكان
historical_era VARCHAR(128), -- الحقبة وفق مشروع السردية الأردنية
-- معايير السلامة والتشغيل
is_offline_available BOOLEAN DEFAULT TRUE, -- هل المعلم مشمول بحزمة التنزيل المسبق
requires_ticket BOOLEAN DEFAULT FALSE, -- هل يتطلب تذكرة دخول
accessibility_level VARCHAR(32) -- مستوى المواءمة لذوي الإعاقة وكبار السن
);
</pre>
</section>
<!-- Section 5 -->
<section class="card">
<h2>5. خطة العمل والشراكة الحكومية المقترحة</h2>
<div class="phase-card">
<div class="phase-header">
<strong>المرحلة الأولى: تقديم ورقة المفهوم المشتركة (Concept Note)</strong>
<span class="badge">الشهر 1 - 2</span>
</div>
<p>عرض المذكرة التنفيذية على وزير الثقافة وأمين عام الوزارة ومدير عام الآثار العامة، والتأكيد على مبدأ التكامل السيادي: الوزارة توفر المحتوى وسردية الأرض والإنسان والاعتماد الرسمي، ونحن نوفر البنية المكانية ومحرك الخرائط المستقل.</p>
</div>
<div class="phase-card">
<div class="phase-header">
<strong>المرحلة الثانية: إطلاق المسار التجريبي الريادي (Flagship PoC)</strong>
<span class="badge">الشهر 3 - 5</span>
</div>
<p>اختيار مسار نموذجي مثل <b>مسار مادبا التراثي (10 محطات)</b> أو <b>مسار عجلون الزيتون (10 محطات)</b>، ورفع إحداثيات البوابات والمواقف الحقيقية، ودمج التسجيلات الصوتية الشعبية النادرة ومفردات المكنز داخل تطبيق الموبايل، وتشغيله أمام قيادات الوزارة وهيئة تنشيط السياحة بالكامل في وضع عدم الاتصال لإثبات الفارق التقني الميداني.</p>
</div>
<div class="phase-card">
<div class="phase-header">
<strong>المرحلة الثالثة: التعميم الوطني والربط الاقتصادي</strong>
<span class="badge">الشهر 6 - 12</span>
</div>
<p>شمول الـ 65 محطة المعتمدة في منصة تراثي، والتوسع تدريجياً ليشمل الـ 15 ألف موقع أثري المسجلة في نظام ميجا بصيغة سياحية آمنة، مع ربط خطوط النقل السياحي وتطبيقات التوصيل لإنعاش اقتصاد المحافظات ودعم الحرفيين المحليين.</p>
</div>
</section>
<!-- CTA Footer -->
<div class="cta-footer">
<h3>جاهزية المنظومة للتنفيذ الفوري</h3>
<p>تمتلك منصتنا اليوم خوادم المتجهات الوطنية ومحرك التوجيه السيادي وتطبيق الموبايل الميداني الجاهز للتكامل مع قواعد بيانات الدولة الأردنية لخدمة أهداف التنمية والسيادة الرقمية.</p>
<a href="https://map-saas.intaleqapp.com/sovereignty-brief.html" class="btn-gold">
<span>معاينة العرض التنفيذي للسيادة المكانية</span>
<span>←</span>
</a>
</div>
</div>
</body>
</html>
+506 -198
View File
@@ -1,7 +1,7 @@
/**
* Documentation Engine for Intaleq Dashboard
* Comprehensive API Reference (EN/AR)
* Updated with Premium Visuals, High-Performance Examples, and Official SDKs
* Commercial Enterprise Mapping Platform & Native SDKs (iOS Swift, Android Kotlin, Flutter Dart, Web JS/TS)
* Supports English & Arabic Localization
*/
const docs = {
@@ -21,10 +21,6 @@ const docs = {
e.preventDefault();
const section = e.currentTarget.getAttribute('data-section');
docs.renderSection(section);
// Active state management
document.querySelectorAll('.docs-nav-link').forEach(l => l.classList.remove('active', 'bg-blue-500/10', 'text-blue-400'));
e.currentTarget.classList.add('active', 'bg-blue-500/10', 'text-blue-400');
});
});
},
@@ -33,7 +29,19 @@ const docs = {
const container = document.getElementById('docs-content');
if (!container) return;
const lang = i18n.currentLang || 'en';
// Synchronize active sidebar navigation link
document.querySelectorAll('.docs-nav-link').forEach(l => {
const sec = l.getAttribute('data-section');
if (sec === id) {
l.classList.add('active', 'bg-blue-500/10', 'text-blue-400');
l.classList.remove('text-slate-400');
} else {
l.classList.remove('active', 'bg-blue-500/10', 'text-blue-400');
l.classList.add('text-slate-400');
}
});
const lang = (window.i18n && window.i18n.currentLang) || 'en';
const isAr = lang === 'ar';
const content = {
@@ -42,165 +50,416 @@ const docs = {
<div class="relative overflow-hidden rounded-[3rem] p-12 bg-gradient-to-br from-blue-600/20 via-blue-500/5 to-transparent border border-white/10 group">
<div class="absolute -top-24 -right-24 w-96 h-96 bg-blue-500/10 blur-[120px] rounded-full group-hover:bg-blue-500/20 transition-all duration-700"></div>
<div class="relative z-10">
<h3 class="text-5xl font-black mb-6 text-gradient">${isAr ? 'انطلق في ثوانٍ' : 'Launch in Seconds'}</h3>
<p class="text-slate-300 text-xl leading-relaxed max-w-2xl">
${isAr ? 'مرحباً بك في مستقبل الخرائط في المنطقة. توفر لك منصة "انطلاق" واجهات برمجية ذكية، خرائط Vector فائقة الدقة، ومباني ثلاثية الأبعاد متكاملة.' : 'Welcome to the future of regional mapping. Intaleq provides high-fidelity vector tiles, intelligent geocoding, and native 3D building support for Jordan & Syria.'}
<div class="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-blue-500/20 text-blue-300 text-xs font-bold mb-4 border border-blue-500/30">
<i data-lucide="sparkles" class="w-3.5 h-3.5"></i>
${isAr ? 'منصة الخرائط والذكاء المكاني للأعمال' : 'Enterprise Commercial Mapping Platform'}
</div>
<h3 class="text-4xl md:text-5xl font-black mb-6 text-gradient">${isAr ? 'ابدأ التكامل مع منصة انطلاق' : 'Launch in Minutes'}</h3>
<p class="text-slate-300 text-lg md:text-xl leading-relaxed max-w-3xl">
${isAr ? 'توفر منصة "انطلاق" حلول الخرائط المتطورة لتطبيقات النقل الذكي (Ride-Hailing)، شركات التوصيل واللوجستيات (Delivery & Logistics)، والتجارة الإلكترونية، مع توفير يصل إلى 85% مقارنة بخرائط جوجل.' : 'Intaleq provides cutting-edge mapping infrastructure for Ride-Hailing, Delivery & Logistics, and E-commerce applications across Jordan & the MENA region at 85% lower cost than Google Maps.'}
</p>
<div class="flex gap-4 mt-8">
<a href="https://pub.dev/packages/intaleq_maps" target="_blank" class="px-6 py-3 bg-blue-600 text-white rounded-2xl font-black text-sm flex items-center gap-2 hover:bg-blue-500 transition-all">
<i data-lucide="package"></i> Flutter SDK
<div class="flex flex-wrap gap-4 mt-8">
<a href="javascript:void(0)" onclick="docs.renderSection('sdks-ios')" class="px-6 py-3 bg-slate-900 text-white rounded-2xl font-bold text-sm flex items-center gap-2 border border-white/10 hover:border-blue-500/50 hover:bg-slate-800 transition-all">
<i data-lucide="apple" class="w-4 h-4 text-slate-300"></i> iOS SDK (Swift)
</a>
<a href="https://www.npmjs.com/package/intaleq-maps-gl" target="_blank" class="px-6 py-3 bg-white text-slate-950 rounded-2xl font-black text-sm flex items-center gap-2 hover:bg-slate-100 transition-all">
<i data-lucide="package"></i> JS SDK
<a href="javascript:void(0)" onclick="docs.renderSection('sdks-android')" class="px-6 py-3 bg-slate-900 text-white rounded-2xl font-bold text-sm flex items-center gap-2 border border-white/10 hover:border-emerald-500/50 hover:bg-slate-800 transition-all">
<i data-lucide="smartphone" class="w-4 h-4 text-emerald-400"></i> Android SDK (Kotlin)
</a>
<a href="javascript:void(0)" onclick="docs.renderSection('sdks-flutter')" class="px-6 py-3 bg-blue-600 text-white rounded-2xl font-bold text-sm flex items-center gap-2 hover:bg-blue-500 shadow-lg shadow-blue-500/25 transition-all">
<i data-lucide="layers" class="w-4 h-4 text-cyan-200"></i> Flutter SDK
</a>
<a href="javascript:void(0)" onclick="docs.renderSection('sdks-web')" class="px-6 py-3 bg-white text-slate-950 rounded-2xl font-bold text-sm flex items-center gap-2 hover:bg-slate-100 transition-all">
<i data-lucide="globe" class="w-4 h-4 text-amber-500"></i> Web JavaScript / TS
</a>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="glass p-10 rounded-[2.5rem] border-white/5 bg-gradient-to-b from-white/[0.02] to-transparent hover:border-blue-500/30 transition-all group">
<div class="w-16 h-16 rounded-2xl bg-blue-500/10 flex items-center justify-center text-blue-400 mb-8 font-black text-2xl group-hover:scale-110 transition-transform">01</div>
<h4 class="font-black text-2xl mb-4">${isAr ? 'مفتاح الوصول' : 'Access Key'}</h4>
<p class="text-slate-400 leading-relaxed">${isAr ? 'قم بإنشاء مفتاح API من لوحة التحكم لتفعيل طلباتك.' : 'Generate your secure API key from the dashboard to authenticate requests.'}</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="glass p-8 rounded-[2rem] border-white/5 bg-gradient-to-b from-white/[0.02] to-transparent hover:border-blue-500/30 transition-all group">
<div class="w-14 h-14 rounded-2xl bg-blue-500/10 flex items-center justify-center text-blue-400 mb-6 font-black text-xl group-hover:scale-110 transition-transform">01</div>
<h4 class="font-black text-xl mb-3 text-white">${isAr ? 'مفتاح الـ API الآمن' : 'API Key Setup'}</h4>
<p class="text-slate-400 text-sm leading-relaxed">${isAr ? 'أنشئ مفتاح وصول محمي بضوابط النطاق (Domain Whitelist) لتطبيقاتك التجارية.' : 'Generate production API keys with IP/Domain restrictions to protect your usage.'}</p>
</div>
<div class="glass p-10 rounded-[2.5rem] border-white/5 bg-gradient-to-b from-white/[0.02] to-transparent hover:border-emerald-500/30 transition-all group">
<div class="w-16 h-16 rounded-2xl bg-emerald-500/10 flex items-center justify-center text-emerald-400 mb-8 font-black text-2xl group-hover:scale-110 transition-transform">02</div>
<h4 class="font-black text-2xl mb-4">${isAr ? 'تكامل الخريطة' : 'Map Integration'}</h4>
<p class="text-slate-400 leading-relaxed">${isAr ? 'اختر النمط (Obsidian أو Light) وادمج الخريطة في تطبيقك.' : 'Select a theme and integrate the vector tiles using our GL styles.'}</p>
<div class="glass p-8 rounded-[2rem] border-white/5 bg-gradient-to-b from-white/[0.02] to-transparent hover:border-emerald-500/30 transition-all group">
<div class="w-14 h-14 rounded-2xl bg-emerald-500/10 flex items-center justify-center text-emerald-400 mb-6 font-black text-xl group-hover:scale-110 transition-transform">02</div>
<h4 class="font-black text-xl mb-3 text-white">${isAr ? 'خرائط مخصصة بهويتك' : 'Custom Map Styling'}</h4>
<p class="text-slate-400 text-sm leading-relaxed">${isAr ? 'اختر النمط الفاتح النظيف للتوصيل، أو النمط الداكن الفخم لتطبيقات النقل، مع مباني 3D.' : 'Choose between Light Delivery or Obsidian Dark ride-hailing styles with 3D buildings.'}</p>
</div>
<div class="glass p-10 rounded-[2.5rem] border-white/5 bg-gradient-to-b from-white/[0.02] to-transparent hover:border-violet-500/30 transition-all group">
<div class="w-16 h-16 rounded-2xl bg-violet-500/10 flex items-center justify-center text-violet-400 mb-8 font-black text-2xl group-hover:scale-110 transition-transform">03</div>
<h4 class="font-black text-2xl mb-4">${isAr ? 'بيانات ذكية' : 'Smart Data'}</h4>
<p class="text-slate-400 leading-relaxed">${isAr ? 'استخدم خدمات البحث والتوجيه لإضافة ذكاء مكاني لتطبيقك.' : 'Leverage Geocoding and Routing APIs for advanced spatial intelligence.'}</p>
<div class="glass p-8 rounded-[2rem] border-white/5 bg-gradient-to-b from-white/[0.02] to-transparent hover:border-violet-500/30 transition-all group">
<div class="w-14 h-14 rounded-2xl bg-violet-500/10 flex items-center justify-center text-violet-400 mb-6 font-black text-xl group-hover:scale-110 transition-transform">03</div>
<h4 class="font-black text-xl mb-3 text-white">${isAr ? 'توجيه وبحث فائق الدقة' : 'Smart Routing & Search'}</h4>
<p class="text-slate-400 text-sm leading-relaxed">${isAr ? 'احسب مسار وتكلفة الرحلة لحظياً مع مصفوفة مطابقة السائقين بأقرب الطلبات.' : 'Calculate route fares, accurate ETAs, and multi-driver dispatch matrices in milliseconds.'}</p>
</div>
</div>
<div class="space-y-8 pt-6">
<div class="space-y-6 pt-4">
<div class="flex items-center gap-4">
<div class="h-8 w-1.5 bg-blue-500 rounded-full"></div>
<h4 class="text-3xl font-black">${isAr ? 'بيانات الوصول والمصادقة' : 'Domain & Authentication'}</h4>
<div class="h-7 w-1.5 bg-blue-500 rounded-full"></div>
<h4 class="text-2xl font-black text-white">${isAr ? 'بيانات الوصول والمصادقة المباشرة' : 'Production Endpoints & Authentication'}</h4>
</div>
<div class="bg-slate-950 rounded-[3rem] p-10 border border-slate-800 relative group overflow-hidden shadow-2xl">
<div class="absolute top-0 right-0 p-6 opacity-20 group-hover:opacity-100 transition-opacity">
<span class="bg-blue-500/10 text-blue-400 px-4 py-1.5 rounded-full text-xs font-black uppercase tracking-widest">Production URL</span>
<div class="bg-slate-950 rounded-[2.5rem] p-8 border border-slate-800 relative group overflow-hidden shadow-2xl">
<div class="absolute top-0 right-0 p-6 opacity-40 group-hover:opacity-100 transition-opacity">
<span class="bg-blue-500/10 text-blue-400 px-3 py-1 rounded-full text-xs font-black uppercase tracking-widest border border-blue-500/20">Production Endpoint</span>
</div>
<code class="text-blue-400 font-mono text-2xl block mb-6 select-all">https://map-saas.intaleq.com/api</code>
<div class="flex flex-col md:flex-row gap-8 text-slate-400">
<div class="flex-1 space-y-2">
<p class="text-sm font-bold uppercase text-slate-500 tracking-widest italic">${isAr ? 'طريقة المصادقة' : 'Auth Method'}</p>
<p class="text-lg">${isAr ? 'يتم إرسال المفتاح عبر الـ HTTP Header التالي:' : 'Pass your API key in the following HTTP header:'}</p>
<code class="text-blue-300 font-mono font-black text-xl">x-api-key</code>
<code class="text-blue-400 font-mono text-xl block mb-6 select-all">https://map-saas.intaleqapp.com/api</code>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 text-slate-400 pt-4 border-t border-slate-900">
<div>
<p class="text-xs font-bold uppercase text-slate-500 tracking-widest mb-1">${isAr ? 'المصادقة عبر الـ Header (موصى به في Backend/Apps)' : 'Header Auth (Recommended)'}</p>
<code class="text-blue-300 font-mono font-bold text-sm bg-blue-500/10 px-2 py-1 rounded">x-api-key: in_9478b32836d19cff73db3063</code>
</div>
<div class="flex-1 space-y-2 border-slate-800 md:border-l md:pl-8">
<p class="text-sm font-bold uppercase text-slate-500 tracking-widest italic">${isAr ? 'نطاق الوصول' : 'Allowed Origins'}</p>
<p class="text-lg">${isAr ? 'تأكد من إضافة النطاق الخاص بك في إعدادات المفتاح.' : 'Ensure your request origin is listed in the key restrictions.'}</p>
<div>
<p class="text-xs font-bold uppercase text-slate-500 tracking-widest mb-1">${isAr ? 'المصادقة عبر الـ Query (للخرائط المباشرة)' : 'Query Parameter Auth'}</p>
<code class="text-emerald-300 font-mono font-bold text-sm bg-emerald-500/10 px-2 py-1 rounded">?key=in_9478b32836d19cff73db3063</code>
</div>
</div>
</div>
</div>
</div>
`,
'sdks': `
<div class="space-y-12 animate-in fade-in slide-in-from-bottom-6 duration-700">
<header>
<h3 class="text-5xl font-black mb-4 text-gradient">${isAr ? 'المكتبات البرمجية (SDKs)' : 'SDKs & Client Libraries'}</h3>
<p class="text-slate-400 text-xl max-w-xl">${isAr ? 'استخدم مكتباتنا الجاهزة لدمج الخرائط والخدمات في ثوانٍ.' : 'Accelerate your development with our official enterprise-grade client libraries.'}</p>
'sdks-ios': `
<div class="space-y-10 animate-in fade-in slide-in-from-bottom-6 duration-700">
<header class="flex items-center gap-5 p-8 rounded-[2.5rem] bg-gradient-to-r from-slate-900 via-slate-900/60 to-transparent border border-white/10">
<div class="w-16 h-16 rounded-2xl bg-white/10 flex items-center justify-center text-white text-3xl shadow-xl">
<i data-lucide="apple" class="w-9 h-9"></i>
</div>
<div>
<div class="flex items-center gap-3 mb-1">
<h3 class="text-3xl font-black text-white">iOS Native SDK</h3>
<span class="px-3 py-0.5 rounded-full bg-blue-500/20 text-blue-400 text-xs font-bold border border-blue-500/30">Swift 5.9+ / UIKit & SwiftUI</span>
</div>
<p class="text-slate-400 text-base">${isAr ? 'مكتبة آبل الأصلية لتطبيقات النقل والتوصيل (Ride-Hailing & Delivery Apps) بسرعة 60 إطاراً في الثانية.' : 'Native iOS SDK for ride-hailing, driver tracking, and delivery logistics apps on iPhone & iPad.'}</p>
</div>
</header>
<div class="grid grid-cols-1 md:grid-cols-2 gap-10">
<!-- Flutter SDK Card -->
<div class="glass p-10 rounded-[3rem] border-white/5 flex flex-col hover:border-blue-500/30 transition-all">
<div class="flex items-center gap-4 mb-8">
<div class="w-12 h-12 rounded-xl bg-blue-500/10 flex items-center justify-center text-blue-400">
<i data-lucide="smartphone"></i>
</div>
<h4 class="text-2xl font-black text-white">Flutter SDK</h4>
<!-- Installation -->
<div class="space-y-4">
<h4 class="text-xl font-bold text-white flex items-center gap-2">
<i data-lucide="download" class="w-5 h-5 text-blue-400"></i> ${isAr ? '1. التثبيت (Installation)' : '1. Installation'}
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-slate-950 p-6 rounded-2xl border border-slate-800">
<p class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-2">Swift Package Manager (SPM)</p>
<code class="text-xs text-blue-300 font-mono select-all block bg-white/5 p-3 rounded-xl">
https://github.com/maplibre/maplibre-native-spm
</code>
</div>
<p class="text-slate-400 mb-8 flex-1 leading-relaxed">
${isAr ? 'مكتبة متكاملة لنظام Flutter تدعم أندرويد و iOS والويب.' : 'A robust Flutter wrapper for MapLibre with native Intaleq services integrated.'}
</p>
<div class="bg-slate-950 p-6 rounded-2xl border border-slate-800 font-mono text-xs mb-8">
<span class="text-slate-500"># pubspec.yaml</span><br>
<span class="text-blue-400">intaleq_maps:</span> <span class="text-emerald-400">^1.0.0</span>
<div class="bg-slate-950 p-6 rounded-2xl border border-slate-800">
<p class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-2">CocoaPods (Podfile)</p>
<code class="text-xs text-emerald-300 font-mono select-all block bg-white/5 p-3 rounded-xl">
pod 'MapLibre', '~> 5.13.0'
</code>
</div>
<a href="https://pub.dev/packages/intaleq_maps" target="_blank" class="btn btn-primary w-full justify-center">View on pub.dev</a>
</div>
<!-- JS SDK Card -->
<div class="glass p-10 rounded-[3rem] border-white/5 flex flex-col hover:border-emerald-500/30 transition-all">
<div class="flex items-center gap-4 mb-8">
<div class="w-12 h-12 rounded-xl bg-emerald-500/10 flex items-center justify-center text-emerald-400">
<i data-lucide="globe"></i>
</div>
<h4 class="text-2xl font-black text-white">JavaScript SDK</h4>
</div>
<p class="text-slate-400 mb-8 flex-1 leading-relaxed">
${isAr ? 'مكتبة JavaScript حديثة مدعومة بـ TypeScript لتطبيقات الويب.' : 'Modern TypeScript-ready SDK for seamless web map integration and routing.'}
</p>
<div class="bg-slate-950 p-6 rounded-2xl border border-slate-800 font-mono text-xs mb-8">
<span class="text-slate-500"># Install via NPM</span><br>
<span class="text-emerald-400">npm i intaleq-maps-gl</span>
</div>
<a href="https://www.npmjs.com/package/intaleq-maps-gl" target="_blank" class="btn btn-primary w-full justify-center">View on NPM</a>
</div>
</div>
<div class="bg-indigo-900/10 p-12 rounded-[3.5rem] border border-indigo-500/20">
<h4 class="text-2xl font-black mb-6 text-indigo-400">${isAr ? 'مثال: إضافة مؤشر (JS SDK)' : 'Example: Adding a Marker (JS SDK)'}</h4>
<pre class="bg-slate-950 p-10 rounded-[2.5rem] border border-slate-800 text-xs font-mono text-slate-300 leading-loose overflow-x-auto">
import { IntaleqMap } from 'intaleq-maps-gl';
<!-- Swift Code Example -->
<div class="space-y-4">
<div class="flex items-center justify-between">
<h4 class="text-xl font-bold text-white flex items-center gap-2">
<i data-lucide="code" class="w-5 h-5 text-blue-400"></i> ${isAr ? '2. مثال كود Swift كامل لتطبيق توصيل/نقل' : '2. Swift Implementation (Delivery / Ride-Hailing)'}
</h4>
<span class="text-xs font-mono text-slate-500">DeliveryMapViewController.swift</span>
</div>
<pre class="bg-slate-950 p-8 rounded-[2rem] border border-slate-800 text-xs font-mono text-slate-300 leading-relaxed overflow-x-auto select-all">
<span class="text-purple-400">import</span> UIKit
<span class="text-purple-400">import</span> MapLibre
const map = new IntaleqMap({
container: 'map',
apiKey: 'YOUR_KEY',
styleType: 'obsidian'
});
<span class="text-purple-400">class</span> <span class="text-yellow-300">DeliveryMapViewController</span>: <span class="text-blue-300">UIViewController</span>, <span class="text-blue-300">MLNMapViewDelegate</span> {
<span class="text-purple-400">var</span> mapView: <span class="text-blue-300">MLNMapView</span>!
<span class="text-purple-400">let</span> apiKey = <span class="text-emerald-300">"YOUR_INTALEQ_API_KEY"</span>
<span class="text-purple-400">override func</span> <span class="text-blue-400">viewDidLoad</span>() {
<span class="text-purple-400">super</span>.viewDidLoad()
<span class="text-slate-500">// 1. رابط نمط الخريطة التجاري من انطلاق</span>
<span class="text-purple-400">let</span> styleURL = <span class="text-blue-300">URL</span>(string: <span class="text-emerald-300">"https://map-saas.intaleqapp.com/tactical-style.json?key=\(apiKey)"</span>)!
<span class="text-slate-500">// 2. تهيئة الخريطة وتثبيت موقع البداية على عمان</span>
mapView = <span class="text-blue-300">MLNMapView</span>(frame: view.bounds, styleURL: styleURL)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.setCenter(<span class="text-blue-300">CLLocationCoordinate2D</span>(latitude: <span class="text-cyan-300">31.9539</span>, longitude: <span class="text-cyan-300">35.9106</span>), zoomLevel: <span class="text-cyan-300">14</span>, animated: <span class="text-purple-400">false</span>)
mapView.delegate = <span class="text-purple-400">self</span>
view.addSubview(mapView)
<span class="text-slate-500">// 3. إضافة علامة موقع السائق / العميل</span>
<span class="text-purple-400">let</span> pickupPoint = <span class="text-blue-300">MLNPointAnnotation</span>()
pickupPoint.coordinate = <span class="text-blue-300">CLLocationCoordinate2D</span>(latitude: <span class="text-cyan-300">31.9539</span>, longitude: <span class="text-cyan-300">35.9106</span>)
pickupPoint.title = <span class="text-emerald-300">"نقطة استلام الطلب"</span>
pickupPoint.subtitle = <span class="text-slate-400">"شارع مكة، عمان"</span>
mapView.addAnnotation(pickupPoint)
}
}</pre>
</div>
map.addIntaleqMarker({
position: [35.91, 31.95], // [lng, lat]
color: '#0D47A1'
});</pre>
<!-- SwiftUI Example -->
<div class="space-y-4">
<h4 class="text-xl font-bold text-white flex items-center gap-2">
<i data-lucide="layout" class="w-5 h-5 text-blue-400"></i> ${isAr ? '3. تكامل SwiftUI' : '3. SwiftUI View Component'}
</h4>
<pre class="bg-slate-950 p-8 rounded-[2rem] border border-slate-800 text-xs font-mono text-slate-300 leading-relaxed overflow-x-auto select-all">
<span class="text-purple-400">import</span> SwiftUI
<span class="text-purple-400">import</span> MapLibre
<span class="text-purple-400">struct</span> <span class="text-yellow-300">IntaleqMapView</span>: <span class="text-blue-300">UIViewRepresentable</span> {
<span class="text-purple-400">func</span> makeUIView(context: Context) -> <span class="text-blue-300">MLNMapView</span> {
<span class="text-purple-400">let</span> url = <span class="text-blue-300">URL</span>(string: <span class="text-emerald-300">"https://map-saas.intaleqapp.com/tactical-style.json"</span>)!
<span class="text-purple-400">let</span> map = <span class="text-blue-300">MLNMapView</span>(frame: .zero, styleURL: url)
map.setCenter(<span class="text-blue-300">CLLocationCoordinate2D</span>(latitude: <span class="text-cyan-300">31.9539</span>, longitude: <span class="text-cyan-300">35.9106</span>), zoomLevel: <span class="text-cyan-300">13</span>, animated: <span class="text-purple-400">false</span>)
<span class="text-purple-400">return</span> map
}
<span class="text-purple-400">func</span> updateUIView(_ uiView: <span class="text-blue-300">MLNMapView</span>, context: Context) {}
}</pre>
</div>
</div>
`,
'sdks-android': `
<div class="space-y-10 animate-in fade-in slide-in-from-bottom-6 duration-700">
<header class="flex items-center gap-5 p-8 rounded-[2.5rem] bg-gradient-to-r from-emerald-950/40 via-slate-900 to-transparent border border-white/10">
<div class="w-16 h-16 rounded-2xl bg-emerald-500/10 flex items-center justify-center text-emerald-400 text-3xl shadow-xl">
<i data-lucide="smartphone" class="w-9 h-9"></i>
</div>
<div>
<div class="flex items-center gap-3 mb-1">
<h3 class="text-3xl font-black text-white">Android Native SDK</h3>
<span class="px-3 py-0.5 rounded-full bg-emerald-500/20 text-emerald-400 text-xs font-bold border border-emerald-500/30">Kotlin & Jetpack Compose</span>
</div>
<p class="text-slate-400 text-base">${isAr ? 'مكتبة أندرويد لتطبيقات الكباتن والسائقين وتتبع مسارات الشحنات بكفاءة وسرعة فائقة.' : 'High-performance Android SDK for driver apps, delivery fleets, and real-time asset tracking.'}</p>
</div>
</header>
<!-- Gradle Setup -->
<div class="space-y-4">
<h4 class="text-xl font-bold text-white flex items-center gap-2">
<i data-lucide="download" class="w-5 h-5 text-emerald-400"></i> ${isAr ? '1. إعداد Gradle (build.gradle.kts)' : '1. Gradle Dependency'}
</h4>
<pre class="bg-slate-950 p-6 rounded-2xl border border-slate-800 text-xs font-mono text-emerald-300 select-all">
dependencies {
implementation(<span class="text-amber-300">"org.maplibre.gl:android-sdk:11.5.1"</span>)
}</pre>
</div>
<!-- Kotlin Code Example -->
<div class="space-y-4">
<div class="flex items-center justify-between">
<h4 class="text-xl font-bold text-white flex items-center gap-2">
<i data-lucide="code" class="w-5 h-5 text-emerald-400"></i> ${isAr ? '2. كود Kotlin لتطبيق السائق / النقل الذكي' : '2. Kotlin Implementation (Driver App)'}
</h4>
<span class="text-xs font-mono text-slate-500">DriverMapActivity.kt</span>
</div>
<pre class="bg-slate-950 p-8 rounded-[2rem] border border-slate-800 text-xs font-mono text-slate-300 leading-relaxed overflow-x-auto select-all">
<span class="text-purple-400">package</span> com.intaleq.driver
<span class="text-purple-400">import</span> android.os.Bundle
<span class="text-purple-400">import</span> androidx.appcompat.app.AppCompatActivity
<span class="text-purple-400">import</span> org.maplibre.android.MapLibre
<span class="text-purple-400">import</span> org.maplibre.android.camera.CameraPosition
<span class="text-purple-400">import</span> org.maplibre.android.geometry.LatLng
<span class="text-purple-400">import</span> org.maplibre.android.maps.MapView
<span class="text-purple-400">class</span> <span class="text-yellow-300">DriverMapActivity</span> : <span class="text-blue-300">AppCompatActivity</span>() {
<span class="text-purple-400">private lateinit var</span> mapView: <span class="text-blue-300">MapView</span>
<span class="text-purple-400">override fun</span> <span class="text-blue-400">onCreate</span>(savedInstanceState: <span class="text-blue-300">Bundle</span>?) {
<span class="text-purple-400">super</span>.onCreate(savedInstanceState)
<span class="text-slate-500">// 1. تهيئة محرك الخريطة</span>
<span class="text-blue-300">MapLibre</span>.getInstance(<span class="text-purple-400">this</span>)
setContentView(R.layout.activity_driver_map)
mapView = findViewById(R.id.mapView)
mapView.onCreate(savedInstanceState)
<span class="text-slate-500">// 2. تحميل نمط انطلاق التجاري</span>
<span class="text-purple-400">val</span> styleUrl = <span class="text-emerald-300">"https://map-saas.intaleqapp.com/tactical-style.json"</span>
mapView.getMapAsync { map ->
map.setStyle(styleUrl) { style ->
<span class="text-slate-500">// 3. تعيين موقع السائق وزاوية الرؤية 3D</span>
map.cameraPosition = <span class="text-blue-300">CameraPosition</span>.Builder()
.target(<span class="text-blue-300">LatLng</span>(<span class="text-cyan-300">31.9539</span>, <span class="text-cyan-300">35.9106</span>))
.zoom(<span class="text-cyan-300">14.0</span>)
.tilt(<span class="text-cyan-300">45.0</span>)
.build()
}
}
}
<span class="text-purple-400">override fun</span> <span class="text-blue-400">onResume</span>() { <span class="text-purple-400">super</span>.onResume(); mapView.onResume() }
<span class="text-purple-400">override fun</span> <span class="text-blue-400">onPause</span>() { <span class="text-purple-400">super</span>.onPause(); mapView.onPause() }
<span class="text-purple-400">override fun</span> <span class="text-blue-400">onDestroy</span>() { <span class="text-purple-400">super</span>.onDestroy(); mapView.onDestroy() }
}</pre>
</div>
</div>
`,
'sdks-flutter': `
<div class="space-y-10 animate-in fade-in slide-in-from-bottom-6 duration-700">
<header class="flex items-center gap-5 p-8 rounded-[2.5rem] bg-gradient-to-r from-cyan-950/40 via-slate-900 to-transparent border border-white/10">
<div class="w-16 h-16 rounded-2xl bg-cyan-500/10 flex items-center justify-center text-cyan-400 text-3xl shadow-xl">
<i data-lucide="layers" class="w-9 h-9"></i>
</div>
<div>
<div class="flex items-center gap-3 mb-1">
<h3 class="text-3xl font-black text-white">Flutter SDK</h3>
<span class="px-3 py-0.5 rounded-full bg-cyan-500/20 text-cyan-400 text-xs font-bold border border-cyan-500/30">Dart 3.5+ / iOS & Android</span>
</div>
<p class="text-slate-400 text-base">${isAr ? 'حزمة فلاتر الرسمية الموحدة لتطوير تطبيقات النقل والتوصيل الميداني على كلا النظامين بكود واحد.' : 'Official Flutter package for cross-platform commercial dispatch and ride-hailing apps.'}</p>
</div>
</header>
<!-- Pubspec Dependency -->
<div class="space-y-4">
<h4 class="text-xl font-bold text-white flex items-center gap-2">
<i data-lucide="download" class="w-5 h-5 text-cyan-400"></i> ${isAr ? '1. التثبيت (pubspec.yaml)' : '1. Pubspec Installation'}
</h4>
<pre class="bg-slate-950 p-6 rounded-2xl border border-slate-800 text-xs font-mono text-cyan-300 select-all">
dependencies:
intaleq_maps: ^1.0.0</pre>
</div>
<!-- Flutter Dart Code -->
<div class="space-y-4">
<div class="flex items-center justify-between">
<h4 class="text-xl font-bold text-white flex items-center gap-2">
<i data-lucide="code" class="w-5 h-5 text-cyan-400"></i> ${isAr ? '2. كود Flutter (Dart) لتطبيق التوصيل' : '2. Flutter Dart Widget'}
</h4>
<span class="text-xs font-mono text-slate-500">ride_tracking_page.dart</span>
</div>
<pre class="bg-slate-950 p-8 rounded-[2rem] border border-slate-800 text-xs font-mono text-slate-300 leading-relaxed overflow-x-auto select-all">
<span class="text-purple-400">import</span> <span class="text-emerald-300">'package:flutter/material.dart'</span>;
<span class="text-purple-400">import</span> <span class="text-emerald-300">'package:intaleq_maps/intaleq_maps.dart'</span>;
<span class="text-purple-400">class</span> <span class="text-yellow-300">RideTrackingPage</span> <span class="text-purple-400">extends</span> <span class="text-blue-300">StatelessWidget</span> {
<span class="text-purple-400">const</span> <span class="text-yellow-300">RideTrackingPage</span>({<span class="text-purple-400">super</span>.key});
<span class="text-purple-400">@override</span>
<span class="text-blue-300">Widget</span> build(<span class="text-blue-300">BuildContext</span> context) {
<span class="text-purple-400">return</span> <span class="text-blue-300">Scaffold</span>(
body: <span class="text-blue-300">IntaleqMap</span>(
apiKey: <span class="text-emerald-300">'YOUR_INTALEQ_KEY'</span>,
styleString: <span class="text-emerald-300">'https://map-saas.intaleqapp.com/tactical-style.json'</span>,
initialCameraPosition: <span class="text-purple-400">const</span> <span class="text-blue-300">CameraPosition</span>(
target: <span class="text-blue-300">LatLng</span>(<span class="text-cyan-300">31.9539</span>, <span class="text-cyan-300">35.9106</span>),
zoom: <span class="text-cyan-300">14.0</span>,
tilt: <span class="text-cyan-300">40.0</span>,
),
myLocationEnabled: <span class="text-purple-400">true</span>,
myLocationTrackingMode: <span class="text-blue-300">MyLocationTrackingMode</span>.Tracking,
onMapCreated: (controller) {
<span class="text-slate-500">// الخريطة جاهزة لعرض خط المسار وحركة السائق</span>
},
),
);
}
}</pre>
</div>
</div>
`,
'sdks-web': `
<div class="space-y-10 animate-in fade-in slide-in-from-bottom-6 duration-700">
<header class="flex items-center gap-5 p-8 rounded-[2.5rem] bg-gradient-to-r from-amber-950/40 via-slate-900 to-transparent border border-white/10">
<div class="w-16 h-16 rounded-2xl bg-amber-500/10 flex items-center justify-center text-amber-400 text-3xl shadow-xl">
<i data-lucide="globe" class="w-9 h-9"></i>
</div>
<div>
<div class="flex items-center gap-3 mb-1">
<h3 class="text-3xl font-black text-white">JavaScript / TypeScript SDK</h3>
<span class="px-3 py-0.5 rounded-full bg-amber-500/20 text-amber-400 text-xs font-bold border border-amber-500/30">Web & React / Vue / Angular</span>
</div>
<p class="text-slate-400 text-base">${isAr ? 'مكتبة الويب للوحات تحكم الإدارة (Dispatch Portals) وتتبع الأساطيل المباشر.' : 'Web mapping SDK for fleet dispatch dashboards and customer web tracking.'}</p>
</div>
</header>
<!-- NPM Install -->
<div class="space-y-4">
<h4 class="text-xl font-bold text-white flex items-center gap-2">
<i data-lucide="download" class="w-5 h-5 text-amber-400"></i> ${isAr ? '1. التثبيت عبر NPM' : '1. NPM Package'}
</h4>
<pre class="bg-slate-950 p-6 rounded-2xl border border-slate-800 text-xs font-mono text-amber-300 select-all">
npm install maplibre-gl @mapbox/mapbox-gl-rtl-text</pre>
</div>
<!-- Web Code Example -->
<div class="space-y-4">
<div class="flex items-center justify-between">
<h4 class="text-xl font-bold text-white flex items-center gap-2">
<i data-lucide="code" class="w-5 h-5 text-amber-400"></i> ${isAr ? '2. كود التكامل (JavaScript / TypeScript)' : '2. JavaScript / TypeScript Implementation'}
</h4>
<span class="text-xs font-mono text-slate-500">dispatch_map.ts</span>
</div>
<pre class="bg-slate-950 p-8 rounded-[2rem] border border-slate-800 text-xs font-mono text-slate-300 leading-relaxed overflow-x-auto select-all">
<span class="text-purple-400">import</span> maplibregl <span class="text-purple-400">from</span> <span class="text-emerald-300">'maplibre-gl'</span>;
<span class="text-purple-400">import</span> <span class="text-emerald-300">'maplibre-gl/dist/maplibre-gl.css'</span>;
<span class="text-slate-500">// 1. تفعيل محرك الخطوط العربية (RTL Plugin)</span>
maplibregl.setRTLTextPlugin(
<span class="text-emerald-300">'https://map-saas.intaleqapp.com/rtl-plugin.js'</span>,
<span class="text-purple-400">null</span>,
<span class="text-purple-400">true</span>
);
<span class="text-slate-500">// 2. تهيئة خريطة لوحة التحكم</span>
<span class="text-purple-400">const</span> map = <span class="text-purple-400">new</span> maplibregl.<span class="text-blue-300">Map</span>({
container: <span class="text-emerald-300">'map'</span>,
style: <span class="text-emerald-300">'https://map-saas.intaleqapp.com/tactical-style.json'</span>,
center: [<span class="text-cyan-300">35.9106</span>, <span class="text-cyan-300">31.9539</span>],
zoom: <span class="text-cyan-300">12.5</span>,
pitch: <span class="text-cyan-300">45</span>
});
<span class="text-slate-500">// 3. إضافة سائق على الخريطة</span>
<span class="text-purple-400">new</span> maplibregl.<span class="text-blue-300">Marker</span>({ color: <span class="text-emerald-300">'#0071E3'</span> })
.setLngLat([<span class="text-cyan-300">35.9106</span>, <span class="text-cyan-300">31.9539</span>])
.setPopup(<span class="text-purple-400">new</span> maplibregl.<span class="text-blue-300">Popup</span>().setHTML(<span class="text-emerald-300">'&lt;h4&gt;كابتن سيرو: أحمد (متاح للطلب)&lt;/h4&gt;'</span>))
.addTo(map);</pre>
</div>
</div>
`,
'tiles-api': `
<div class="space-y-12 animate-in fade-in slide-in-from-bottom-6 duration-700">
<header class="flex flex-col md:flex-row md:items-end justify-between gap-6">
<div>
<h3 class="text-5xl font-black mb-4 text-gradient">${isAr ? 'خرائط الـ Vector' : 'Vector Tiles API'}</h3>
<p class="text-slate-400 text-xl max-w-xl">${isAr ? 'خرائط تفاعلية فائقة السرعة تدعم العرض ثلاثي الأبعاد والتحكم الكامل في الخصائص.' : 'High-performance interactive maps with native 3D buildings and custom GL styles.'}</p>
<h3 class="text-4xl md:text-5xl font-black mb-4 text-gradient">${isAr ? 'خدمة خرائط الـ Vector Tiles' : 'Map Vector Tiles API'}</h3>
<p class="text-slate-400 text-lg max-w-2xl">${isAr ? 'خرائط متجهة تفاعلية فائقة السرعة تدعم العرض ثلاثي الأبعاد والتحكم في طبقات الطرق والمباني وأسماء الأحياء باللغة العربية.' : 'High-performance interactive vector tiles with native 3D buildings, Arabic typography, and commercial POIs.'}</p>
</div>
</header>
<div class="endpoint-card glass rounded-[3.5rem] border-white/5 overflow-hidden shadow-2xl bg-gradient-to-b from-white/[0.03] to-transparent">
<div class="p-8 bg-white/[0.04] border-b border-white/5 flex items-center justify-between">
<div class="flex items-center gap-6">
<div class="px-4 py-2 bg-emerald-500 text-white text-sm font-black rounded-xl shadow-lg shadow-emerald-500/20 uppercase tracking-tighter">GET</div>
<code class="text-lg font-bold text-slate-100 font-mono">/v1/maps/style.json</code>
<div class="endpoint-card glass rounded-[3rem] border-white/5 overflow-hidden shadow-2xl bg-gradient-to-b from-white/[0.03] to-transparent">
<div class="p-6 bg-white/[0.04] border-b border-white/5 flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="px-3.5 py-1.5 bg-emerald-500 text-white text-xs font-black rounded-xl uppercase tracking-wider">GET</div>
<code class="text-base font-bold text-slate-100 font-mono">/v1/maps/style.json</code>
</div>
</div>
<div class="p-12">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12">
<div class="space-y-8">
<div>
<h5 class="text-[10px] font-black uppercase tracking-[0.2em] text-blue-400 mb-6">${isAr ? 'المعاملات المدعومة' : 'Query Parameters'}</h5>
<div class="space-y-4">
<div class="flex items-center justify-between p-4 bg-white/5 rounded-2xl border border-white/5">
<code class="text-blue-300 font-bold">theme</code>
<span class="text-[10px] font-mono text-slate-600 uppercase">Optional (obsidian | light)</span>
</div>
<div class="flex items-center justify-between p-4 bg-white/5 rounded-2xl border border-white/5">
<code class="text-blue-300 font-bold">key</code>
<span class="text-[10px] font-mono text-rose-500 uppercase">Required</span>
</div>
<div class="p-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="space-y-6">
<h5 class="text-xs font-black uppercase tracking-wider text-blue-400">${isAr ? 'المعاملات المدعومة (Parameters)' : 'Query Parameters'}</h5>
<div class="space-y-3">
<div class="flex items-center justify-between p-3.5 bg-white/5 rounded-xl border border-white/5">
<code class="text-blue-300 font-bold">theme</code>
<span class="text-xs font-mono text-slate-400">obsidian (داكن) | light (فاتح للتوصيل)</span>
</div>
<div class="flex items-center justify-between p-3.5 bg-white/5 rounded-xl border border-white/5">
<code class="text-blue-300 font-bold">key</code>
<span class="text-xs font-mono text-rose-400 font-bold">مفتاح API الخاص بك (مطلوب)</span>
</div>
</div>
</div>
<div class="bg-slate-950 rounded-[2.5rem] p-10 border border-slate-800 shadow-inner">
<h5 class="text-[10px] font-black uppercase tracking-[0.2em] text-slate-500 mb-6">${isAr ? 'رابط النمط المباشر' : 'Direct Style URL'}</h5>
<code class="text-xs text-blue-400 break-all select-all font-mono leading-relaxed">
https://map-saas.intaleq.com/api/v1/maps/style.json?theme=obsidian&key=YOUR_API_KEY
<div class="bg-slate-950 rounded-2xl p-6 border border-slate-800">
<h5 class="text-xs font-black uppercase tracking-wider text-slate-500 mb-4">${isAr ? 'رابط النمط المباشر (Direct Style URL)' : 'Direct Style URL'}</h5>
<code class="text-xs text-blue-400 break-all select-all font-mono leading-relaxed block bg-white/5 p-4 rounded-xl">
https://map-saas.intaleqapp.com/tactical-style.json?key=YOUR_API_KEY
</code>
</div>
</div>
@@ -208,56 +467,94 @@ map.addIntaleqMarker({
</div>
</div>
`,
'geocoding-api': `
<div class="space-y-12 animate-in fade-in slide-in-from-bottom-6 duration-700">
<header>
<h3 class="text-5xl font-black mb-4 text-gradient">${isAr ? 'البحث المكاني (Geocoding)' : 'Geocoding API'}</h3>
<p class="text-slate-400 text-xl max-w-2xl">${isAr ? 'حوّل العناوين إلى إحداثيات أو العكس بدقة غير مسبوقة في الأردن وسوريا.' : 'Transform addresses into coordinates or reverse resolve locations with extreme accuracy in the Levant region.'}</p>
<h3 class="text-4xl md:text-5xl font-black mb-4 text-gradient">${isAr ? 'خدمة البحث المكاني وعناوين التوصيل (Geocoding API)' : 'Geocoding & Places API'}</h3>
<p class="text-slate-400 text-lg max-w-2xl">${isAr ? 'محرك البحث الذكي لتحديد نقاط استلام وتوصيل الطلبات، والمطاعم، والمتاجر، والأحياء بدقة فائقة في الأردن وسوريا.' : 'Intelligent location search and reverse geocoding tailored for delivery pickups and street address resolution.'}</p>
</header>
<div class="endpoint-card glass rounded-[3.5rem] border-white/5 overflow-hidden shadow-2xl">
<div class="p-8 bg-blue-500/5 border-b border-white/5 flex items-center justify-between">
<!-- Forward Search -->
<div class="endpoint-card glass rounded-[3rem] border-white/5 overflow-hidden shadow-2xl mb-8">
<div class="p-6 bg-blue-500/5 border-b border-white/5 flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="px-4 py-2 bg-blue-600 text-white text-xs font-black rounded-xl shadow-lg shadow-blue-500/20 uppercase tracking-widest">SEARCH</div>
<div class="px-3.5 py-1.5 bg-blue-600 text-white text-xs font-black rounded-xl uppercase tracking-wider">GET</div>
<code class="text-base font-bold text-slate-200">/v1/geocoding/search</code>
</div>
<span class="text-xs text-slate-400 font-bold">${isAr ? 'البحث عن الأماكن والعناوين' : 'Forward Place Search'}</span>
</div>
<div class="p-12">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12">
<div class="space-y-8">
<table class="w-full text-sm">
<thead class="text-[10px] text-slate-500 font-black uppercase tracking-[0.2em]">
<tr class="border-b border-white/5"><th class="pb-4">Param</th><th class="pb-4 text-right">Description</th></tr>
</thead>
<tbody class="text-slate-400 divide-y divide-white/[0.02]">
<tr><td class="py-4 font-bold text-blue-300 font-mono">q</td><td class="py-4 text-right">Query (e.g. "Masjid Hashem")</td></tr>
<tr><td class="py-4 font-bold text-blue-300 font-mono">limit</td><td class="py-4 text-right">Max results (Default: 5)</td></tr>
</tbody>
</table>
<div class="p-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="space-y-4">
<h5 class="text-xs font-black uppercase tracking-wider text-blue-400">${isAr ? 'معاملات الطلب (Query Params)' : 'Request Parameters'}</h5>
<div class="space-y-2 text-xs">
<div class="flex justify-between p-2.5 bg-white/5 rounded-lg border border-white/5"><code>q</code><span>نص البحث (مثال: "مطعم القدس شارع الجامعة")</span></div>
<div class="flex justify-between p-2.5 bg-white/5 rounded-lg border border-white/5"><code>lat, lng</code><span>إحداثيات العميل لترتيب الأقرب أولاً</span></div>
<div class="flex justify-between p-2.5 bg-white/5 rounded-lg border border-white/5"><code>limit</code><span>عدد النتائج المطلوبة (افتراضي: 5)</span></div>
</div>
</div>
<div class="bg-slate-950 rounded-[3rem] p-8 border border-slate-800 shadow-inner">
<div class="flex justify-between items-center mb-6">
<span class="text-[10px] font-black uppercase text-emerald-400 tracking-widest flex items-center gap-2">
<i data-lucide="layers" class="w-3 h-3"></i> Real-World Response
</span>
</div>
<pre class="text-[10px] font-mono text-emerald-300/80 leading-relaxed overflow-y-auto h-64">
<div class="bg-slate-950 rounded-2xl p-6 border border-slate-800">
<h5 class="text-xs font-black uppercase tracking-wider text-emerald-400 mb-3">${isAr ? 'مخرجات الـ JSON التجارية' : 'Commercial JSON Response'}</h5>
<pre class="text-[11px] font-mono text-emerald-300 leading-relaxed overflow-y-auto max-h-60 select-all">
{
"status": "success",
"query": "مطعم القدس شارع الجامعة",
"results": [
{
"id": 5843,
"name": "مسجد هاشم",
"name_ar": "مسجد هاشم",
"category": "building",
"governorate": "الزرقاء",
"location": { "lat": 32.10659, "lng": 36.18301 },
"full_address": "لواء قصبة الزرقاء، الزرقاء",
"distance_km": "12.68",
"source": "user_place"
"id": "poi_98231",
"name": "مطعم القدس",
"name_en": "Al Quds Restaurant",
"category": "restaurant",
"formatted_address": "شارع الجامعة الأردنية، الجبيهة، عمان",
"location": {
"lat": 32.01542,
"lng": 35.86981
},
"neighborhood": "الجبيهة",
"city": "عمان",
"confidence": 0.98
}
],
"source": "cache_hit"
]
}</pre>
</div>
</div>
</div>
</div>
<!-- Reverse Geocoding -->
<div class="endpoint-card glass rounded-[3rem] border-white/5 overflow-hidden shadow-2xl">
<div class="p-6 bg-emerald-500/5 border-b border-white/5 flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="px-3.5 py-1.5 bg-emerald-600 text-white text-xs font-black rounded-xl uppercase tracking-wider">GET</div>
<code class="text-base font-bold text-slate-200">/v1/geocoding/reverse</code>
</div>
<span class="text-xs text-slate-400 font-bold">${isAr ? 'تحويل موقع السائق إلى عنوان وفاتورة' : 'Reverse Coordinate to Address'}</span>
</div>
<div class="p-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="space-y-4">
<h5 class="text-xs font-black uppercase tracking-wider text-emerald-400">${isAr ? 'معاملات الطلب' : 'Request Parameters'}</h5>
<div class="space-y-2 text-xs">
<div class="flex justify-between p-2.5 bg-white/5 rounded-lg border border-white/5"><code>lat</code><span>خط العرض لموقع السائق/المركبة</span></div>
<div class="flex justify-between p-2.5 bg-white/5 rounded-lg border border-white/5"><code>lng</code><span>خط الطول لموقع السائق/المركبة</span></div>
</div>
</div>
<div class="bg-slate-950 rounded-2xl p-6 border border-slate-800">
<h5 class="text-xs font-black uppercase tracking-wider text-emerald-400 mb-3">${isAr ? 'مخرجات العنوان التلقائي' : 'Resolved Address Response'}</h5>
<pre class="text-[11px] font-mono text-emerald-300 leading-relaxed overflow-y-auto max-h-60 select-all">
{
"status": "success",
"formatted_address": "شارع مكة، أم أذينة، عمان",
"street": "شارع مكة",
"neighborhood": "أم أذينة",
"city": "عمان",
"location": {
"lat": 31.9821,
"lng": 35.8574
}
}</pre>
</div>
</div>
@@ -265,60 +562,68 @@ map.addIntaleqMarker({
</div>
</div>
`,
'routing-api': `
<div class="space-y-12 animate-in fade-in slide-in-from-bottom-6 duration-700">
<header class="relative p-12 overflow-hidden rounded-[3.5rem] bg-slate-900 border border-white/5 md:flex md:items-center md:justify-between shadow-2xl">
<div class="absolute -top-24 -left-24 w-64 h-64 bg-violet-600/10 blur-[100px] rounded-full"></div>
<div class="relative z-10">
<h3 class="text-5xl font-black mb-4 text-gradient">${isAr ? 'محرك التوجيه (Routing)' : 'Routing Engine'}</h3>
<p class="text-slate-400 text-xl max-w-xl">${isAr ? 'حساب أسرع المسارات مع تحليلات لحظية لحركة المرور.' : 'Fast pathfinding with traffic-aware duration metrics.'}</p>
</div>
<header>
<h3 class="text-4xl md:text-5xl font-black mb-4 text-gradient">${isAr ? 'محرك التوجيه وحساب الأجرة والملاحة (Routing API)' : 'Routing & Navigation API'}</h3>
<p class="text-slate-400 text-lg max-w-2xl">${isAr ? 'حساب أسرع المسارات، تقدير وقت الوصول الحقيقي (ETA)، حساب مسافة الرحلة بدقة لحساب الأجرة، والتوجيه خطوة بخطوة.' : 'Turn-by-turn navigation engine with traffic-aware duration, distance metrics for fare calculation, and route polylines.'}</p>
</header>
<div class="endpoint-card glass rounded-[3.5rem] border-white/5 overflow-hidden shadow-2xl">
<div class="p-8 bg-violet-500/5 border-b border-white/5 flex items-center justify-between">
<div class="endpoint-card glass rounded-[3rem] border-white/5 overflow-hidden shadow-2xl">
<div class="p-6 bg-violet-500/5 border-b border-white/5 flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="px-4 py-2 bg-violet-600 text-white text-xs font-black rounded-xl shadow-lg shadow-violet-500/20 uppercase tracking-widest">ROUTE</div>
<div class="px-3.5 py-1.5 bg-violet-600 text-white text-xs font-black rounded-xl uppercase tracking-wider">GET / POST</div>
<code class="text-base font-bold text-slate-200">/v1/routing/route</code>
</div>
<span class="text-xs text-slate-400 font-bold">${isAr ? 'حساب المسار وتوجيه السائق' : 'Route Navigation'}</span>
</div>
<div class="p-12">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12">
<div class="space-y-8">
<div class="p-6 bg-slate-900/40 rounded-3xl border border-white/5">
<h5 class="text-[10px] font-black uppercase tracking-widest text-violet-400 mb-4">Required Parameters</h5>
<ul class="text-xs text-slate-400 space-y-4">
<li class="flex justify-between border-b border-white/5 pb-2"><span>start</span> <span class="text-violet-300 font-mono italic">"35.91,31.95"</span></li>
<li class="flex justify-between border-b border-white/5 pb-2"><span>end</span> <span class="text-violet-300 font-mono italic">"35.85,31.82"</span></li>
<li class="flex justify-between"><span>profile</span> <span class="text-slate-500">car | bike | foot</span></li>
</ul>
<div class="p-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="space-y-4">
<h5 class="text-xs font-black uppercase tracking-wider text-violet-400">${isAr ? 'معاملات طلب الرحلة (Params)' : 'Parameters'}</h5>
<div class="p-5 bg-slate-900/50 rounded-2xl border border-white/5 text-xs text-slate-300 space-y-3">
<div class="flex justify-between border-b border-white/5 pb-2"><span>start</span><code class="text-violet-300 font-mono">35.9106,31.9539</code></div>
<div class="flex justify-between border-b border-white/5 pb-2"><span>end</span><code class="text-violet-300 font-mono">36.0380,32.1351</code></div>
<div class="flex justify-between border-b border-white/5 pb-2"><span>profile</span><span class="text-slate-400">car (سيارة) | delivery (دراجة توصيل)</span></div>
<div class="flex justify-between"><span>traffic</span><span class="text-emerald-400 font-bold">true (حساب الازدحام المروري)</span></div>
</div>
</div>
<div class="bg-slate-950 rounded-[3rem] p-10 border border-slate-800 shadow-inner">
<div class="flex justify-between items-center mb-6">
<span class="text-[10px] font-black uppercase text-violet-400 tracking-widest flex items-center gap-2">
<i data-lucide="activity" class="w-3.5 h-3.5"></i> Production Response
</span>
</div>
<pre class="text-[10px] font-mono text-violet-300/80 leading-loose overflow-x-auto h-64">
<div class="bg-slate-950 rounded-2xl p-6 border border-slate-800">
<h5 class="text-xs font-black uppercase tracking-wider text-violet-400 mb-3">${isAr ? 'المخرجات التجارية لحساب الأجرة والمسار' : 'Commercial Response (Fare & ETA)'}</h5>
<pre class="text-[11px] font-mono text-violet-300 leading-relaxed overflow-y-auto max-h-72 select-all">
{
"distance": 30313.8,
"duration": 1975,
"trafficAwareDuration": 1975,
"points": "_cvbE_th{Ev@iJpHtDLEJKz@^XJn@...",
"instructions": [
{
"text": "Continue onto شارع الأمير الحسن",
"distance": 172.8,
"street_name": "شارع الأمير الحسن"
},
{
"text": "اتجه قليلاً لليمين خلال شارع الجيش",
"distance": 190.1,
"street_name": "شارع الجيش"
}
]
"status": "success",
"route": {
"distance_meters": 8450,
"distance_km": 8.45,
"duration_seconds": 780,
"duration_minutes": 13.0,
"traffic_delay_seconds": 120,
"estimated_fare_jod": 2.53,
"geometry": "_cvbE_th{Ev@iJpHtDLEJKz@^XJn@...",
"steps": [
{
"instruction": "انطلق باتجاه الشمال على شارع وصفي التل",
"instruction_en": "Head north on Wasfi Al-Tal St",
"distance_meters": 1200,
"duration_seconds": 110
},
{
"instruction": "اتجه يميناً عند دوار الواحة نحو شارع المدينة المنورة",
"instruction_en": "Turn right at Al-Waha Circle onto Al-Madina St",
"distance_meters": 3400,
"duration_seconds": 310
},
{
"instruction": "لقد وصلت إلى وجهتك على اليمين",
"instruction_en": "You have arrived at your destination on the right",
"distance_meters": 0,
"duration_seconds": 0
}
]
}
}</pre>
</div>
</div>
@@ -328,9 +633,12 @@ map.addIntaleqMarker({
`
};
container.innerHTML = content[id] || '<div class="h-64 flex items-center justify-center italic text-slate-600">Documentation section coming soon...</div>';
// Fallback for generic 'sdks'
content['sdks'] = content['sdks-flutter'];
container.innerHTML = content[id] || '<div class="h-64 flex items-center justify-center italic text-slate-500">Documentation section coming soon...</div>';
// Re-initialize icons
// Re-initialize lucide icons
if (window.lucide) lucide.createIcons();
}
};
+10 -2
View File
@@ -139,7 +139,11 @@ const i18n = {
'modal-key-label': 'Key Name',
'cancel': 'Cancel',
'guides-title': 'Guides',
'side-sdks': 'SDKs & Libraries'
'side-sdks': 'SDKs & Libraries',
'docs-overview': 'Overview',
'docs-getting-started': 'Getting Started',
'docs-sdks-title': 'Client SDKs',
'docs-rest-title': 'REST APIs'
},
ar: {
// Navbar (Landing)
@@ -272,7 +276,11 @@ const i18n = {
'modal-key-label': 'اسم المفتاح',
'cancel': 'إلغاء',
'guides-title': 'الأدلة برمجية',
'side-sdks': 'المكتبات البرمجية (SDKs)'
'side-sdks': 'المكتبات البرمجية (SDKs)',
'docs-overview': 'نظرة عامة',
'docs-getting-started': 'البدء السريع',
'docs-sdks-title': 'المكتبات وحزم الـ SDK',
'docs-rest-title': 'واجهات الـ REST API'
}
},
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+675
View File
@@ -0,0 +1,675 @@
{
"version": 8,
"name": "Intaleq Sovereign 3D Topographic & Terrain Style (خريطة التضاريس ثلاثية الأبعاد السيادية)",
"metadata": {
"brand": "Intaleq",
"version": "3.5.0-terrain-3d",
"description": "High-fidelity Sovereign 3D Topographic Style with DEM terrain displacement, dynamic hillshading, contour lines, and 3D architectural extrusions"
},
"center": [
35.4337,
29.5763
],
"zoom": 12.5,
"pitch": 65,
"bearing": -35,
"glyphs": "https://cdn.protomaps.com/fonts/pbf/{fontstack}/{range}.pbf",
"sources": {
"terrain-dem": {
"type": "raster-dem",
"tiles": [
"https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png",
"https://tiles.intaleqapp.com/raster_dem/{z}/{x}/{y}.png"
],
"encoding": "terrarium",
"tileSize": 256,
"maxzoom": 15
},
"esri-satellite": {
"type": "raster",
"tiles": [
"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
],
"tileSize": 256,
"maxzoom": 18,
"attribution": "© Esri, DigitalGlobe, GeoEye, Earthstar Geographics"
},
"opentopo-contours": {
"type": "raster",
"tiles": [
"https://a.tile.opentopomap.org/{z}/{x}/{y}.png",
"https://b.tile.opentopomap.org/{z}/{x}/{y}.png",
"https://c.tile.opentopomap.org/{z}/{x}/{y}.png"
],
"tileSize": 256,
"maxzoom": 17
},
"jordan_contours": {
"type": "vector",
"tiles": [
"https://tiles.intaleqapp.com/jordan_contours/{z}/{x}/{y}"
],
"minzoom": 8,
"maxzoom": 16
},
"local-osm-polygons": {
"type": "vector",
"tiles": [
"https://tiles.intaleqapp.com/planet_osm_polygon/{z}/{x}/{y}"
],
"maxzoom": 14,
"attribution": "© Intaleq | © OpenStreetMap contributors"
},
"local-osm-lines": {
"type": "vector",
"tiles": [
"https://tiles.intaleqapp.com/planet_osm_line/{z}/{x}/{y}"
],
"maxzoom": 14
},
"local-osm-points": {
"type": "vector",
"tiles": [
"https://tiles.intaleqapp.com/planet_osm_point/{z}/{x}/{y}"
],
"maxzoom": 14
},
"places_jordan": {
"type": "vector",
"tiles": [
"https://tiles.intaleqapp.com/places_jordan/{z}/{x}/{y}"
],
"maxzoom": 14
},
"overture_buildings": {
"type": "vector",
"tiles": [
"https://tiles.intaleqapp.com/overture_building/{z}/{x}/{y}"
],
"maxzoom": 16
},
"approved_roads": {
"type": "vector",
"tiles": [
"https://tiles.intaleqapp.com/approved_roads/{z}/{x}/{y}"
],
"minzoom": 8,
"maxzoom": 18
}
},
"layers": [
{
"id": "topo-background",
"type": "background",
"paint": {
"background-color": "#EFECE6"
}
},
{
"id": "satellite-base-layer",
"type": "raster",
"source": "esri-satellite",
"layout": {
"visibility": "none"
},
"paint": {
"raster-opacity": 0.95,
"raster-saturation": 0.1
}
},
{
"id": "terrain-3d-hillshading",
"type": "hillshade",
"source": "terrain-dem",
"layout": {
"visibility": "visible"
},
"paint": {
"hillshade-illumination-direction": 315,
"hillshade-illumination-anchor": "viewport",
"hillshade-shadow-color": "#261d15",
"hillshade-highlight-color": "#fffbf2",
"hillshade-accent-color": "#784a28",
"hillshade-exaggeration": 0.75
}
},
{
"id": "natural-water-poly",
"type": "fill",
"source": "local-osm-polygons",
"source-layer": "planet_osm_polygon",
"filter": [
"any",
["==", "natural", "water"],
["==", "water", "lake"],
["==", "waterway", "riverbank"]
],
"paint": {
"fill-color": "#0284c7",
"fill-opacity": 0.85
}
},
{
"id": "natural-wood-forest",
"type": "fill",
"source": "local-osm-polygons",
"source-layer": "planet_osm_polygon",
"filter": [
"any",
["==", "natural", "wood"],
["==", "landuse", "forest"],
["==", "leisure", "nature_reserve"]
],
"paint": {
"fill-color": "#4d7c0f",
"fill-opacity": 0.25
}
},
{
"id": "natural-sand-dune",
"type": "fill",
"source": "local-osm-polygons",
"source-layer": "planet_osm_polygon",
"filter": [
"any",
["==", "natural", "sand"],
["==", "natural", "desert"],
["==", "natural", "scree"]
],
"paint": {
"fill-color": "#ea580c",
"fill-opacity": 0.12
}
},
{
"id": "wadis-waterways",
"type": "line",
"source": "local-osm-lines",
"source-layer": "planet_osm_line",
"filter": [
"in",
"waterway",
"river",
"stream",
"canal",
"wadi"
],
"paint": {
"line-color": "#0284c7",
"line-width": [
"interpolate",
["linear"],
["zoom"],
8, 1,
14, 3.5
],
"line-opacity": 0.7
}
},
{
"id": "contours-raster-overlay",
"type": "raster",
"source": "opentopo-contours",
"minzoom": 8,
"maxzoom": 18,
"layout": {
"visibility": "visible"
},
"paint": {
"raster-opacity": 0.55,
"raster-contrast": 0.2
}
},
{
"id": "contour-minor-lines",
"type": "line",
"source": "jordan_contours",
"source-layer": "jordan_contours",
"minzoom": 11,
"layout": {
"visibility": "visible",
"line-join": "round",
"line-cap": "round"
},
"paint": {
"line-color": "#a88358",
"line-width": 0.75,
"line-opacity": 0.5
}
},
{
"id": "contour-major-lines",
"type": "line",
"source": "jordan_contours",
"source-layer": "jordan_contours",
"minzoom": 9,
"filter": [
"==",
"type",
"index"
],
"layout": {
"visibility": "visible",
"line-join": "round",
"line-cap": "round"
},
"paint": {
"line-color": "#784a28",
"line-width": 1.4,
"line-opacity": 0.75
}
},
{
"id": "roads-casing",
"type": "line",
"source": "local-osm-lines",
"source-layer": "planet_osm_line",
"filter": [
"in",
"highway",
"motorway",
"trunk",
"primary",
"secondary"
],
"paint": {
"line-color": "#ffffff",
"line-width": [
"interpolate",
["linear"],
["zoom"],
8, 2,
14, 6
],
"line-opacity": 0.9
}
},
{
"id": "roads-core-highways",
"type": "line",
"source": "local-osm-lines",
"source-layer": "planet_osm_line",
"filter": [
"in",
"highway",
"motorway",
"trunk"
],
"paint": {
"line-color": "#d97706",
"line-width": [
"interpolate",
["linear"],
["zoom"],
8, 1.2,
14, 4.5
]
}
},
{
"id": "roads-core-primary",
"type": "line",
"source": "local-osm-lines",
"source-layer": "planet_osm_line",
"filter": [
"in",
"highway",
"primary",
"secondary"
],
"paint": {
"line-color": "#f59e0b",
"line-width": [
"interpolate",
["linear"],
["zoom"],
9, 1,
14, 3.2
]
}
},
{
"id": "roads-core-minor",
"type": "line",
"source": "local-osm-lines",
"source-layer": "planet_osm_line",
"filter": [
"in",
"highway",
"residential",
"service",
"unclassified",
"living_street"
],
"minzoom": 13.5,
"paint": {
"line-color": "#ffffff",
"line-width": [
"interpolate",
["linear"],
["zoom"],
13.5, 0.9,
16, 2.5
],
"line-opacity": 0.8
}
},
{
"id": "road-labels-highways",
"type": "symbol",
"source": "local-osm-lines",
"source-layer": "planet_osm_line",
"filter": [
"in",
"highway",
"motorway",
"trunk",
"primary"
],
"minzoom": 12,
"layout": {
"text-field": [
"coalesce",
["get", "name:ar"],
["get", "name"],
""
],
"text-font": [
"Noto Sans Arabic Bold",
"Open Sans Bold"
],
"text-size": [
"interpolate",
["linear"],
["zoom"],
12, 10,
15, 12,
18, 14
],
"symbol-placement": "line",
"text-rotation-alignment": "map",
"text-pitch-alignment": "map",
"text-keep-upright": true,
"text-letter-spacing": 0.05,
"text-padding": 24,
"symbol-spacing": 700,
"text-max-angle": 20,
"text-allow-overlap": false,
"text-ignore-placement": false
},
"paint": {
"text-color": "#1e293b",
"text-halo-color": "rgba(255, 255, 255, 0.95)",
"text-halo-width": 2.2
}
},
{
"id": "road-labels-major",
"type": "symbol",
"source": "local-osm-lines",
"source-layer": "planet_osm_line",
"filter": [
"in",
"highway",
"secondary",
"tertiary"
],
"minzoom": 13.5,
"layout": {
"text-field": [
"coalesce",
["get", "name:ar"],
["get", "name"],
""
],
"text-font": [
"Noto Sans Arabic Bold",
"Open Sans Bold"
],
"text-size": [
"interpolate",
["linear"],
["zoom"],
13.5, 9.5,
16, 11,
18, 13
],
"symbol-placement": "line",
"text-rotation-alignment": "map",
"text-pitch-alignment": "map",
"text-keep-upright": true,
"text-letter-spacing": 0.04,
"text-padding": 20,
"symbol-spacing": 600,
"text-max-angle": 20,
"text-allow-overlap": false,
"text-ignore-placement": false
},
"paint": {
"text-color": "#334155",
"text-halo-color": "rgba(255, 255, 255, 0.9)",
"text-halo-width": 2.0
}
},
{
"id": "road-labels-minor",
"type": "symbol",
"source": "local-osm-lines",
"source-layer": "planet_osm_line",
"filter": [
"in",
"highway",
"residential",
"unclassified"
],
"minzoom": 16,
"layout": {
"text-field": [
"coalesce",
["get", "name:ar"],
["get", "name"],
""
],
"text-font": [
"Noto Sans Arabic Regular",
"Open Sans Regular"
],
"text-size": [
"interpolate",
["linear"],
["zoom"],
16, 9.5,
18, 11.5
],
"symbol-placement": "line",
"text-rotation-alignment": "map",
"text-pitch-alignment": "map",
"text-keep-upright": true,
"text-letter-spacing": 0.04,
"text-padding": 20,
"symbol-spacing": 600,
"text-max-angle": 18,
"text-allow-overlap": false,
"text-ignore-placement": false
},
"paint": {
"text-color": "#475569",
"text-halo-color": "rgba(255, 255, 255, 0.9)",
"text-halo-width": 1.8
}
},
{
"id": "approved-road-labels",
"type": "symbol",
"source": "approved_roads",
"source-layer": "approved_roads",
"minzoom": 16.5,
"layout": {
"text-field": [
"coalesce",
["get", "name:ar"],
["get", "name"],
""
],
"text-font": [
"Noto Sans Arabic Bold",
"Open Sans Bold"
],
"text-size": [
"interpolate",
["linear"],
["zoom"],
16.5, 9,
18, 11.5
],
"symbol-placement": "line",
"text-rotation-alignment": "map",
"text-pitch-alignment": "map",
"text-keep-upright": true,
"text-letter-spacing": 0.05,
"text-padding": 20,
"symbol-spacing": 650,
"text-max-angle": 20,
"text-allow-overlap": false,
"text-ignore-placement": false
},
"paint": {
"text-color": "#1e293b",
"text-halo-color": "rgba(255, 255, 255, 0.95)",
"text-halo-width": 2.2
}
},
{
"id": "3d-buildings-ground-shadows",
"type": "fill",
"source": "overture_buildings",
"source-layer": "overture_building",
"minzoom": 13.5,
"layout": {
"visibility": "visible"
},
"paint": {
"fill-color": "#0f172a",
"fill-opacity": 0.45,
"fill-translate": [12, 4],
"fill-translate-anchor": "map"
}
},
{
"id": "3d-buildings-extrusion",
"type": "fill-extrusion",
"source": "overture_buildings",
"source-layer": "overture_building",
"minzoom": 13.5,
"layout": {
"visibility": "visible"
},
"paint": {
"fill-extrusion-color": [
"interpolate",
["linear"],
["coalesce", ["get", "height"], 12],
0, "#e7e5e4",
30, "#d6d3d1",
70, "#a8a29e",
150, "#78716c"
],
"fill-extrusion-height": [
"coalesce",
["get", "height"],
["*", ["coalesce", ["get", "num_floors"], 3], 3.5]
],
"fill-extrusion-base": 0,
"fill-extrusion-opacity": 0.88
}
},
{
"id": "mountain-peaks-points",
"type": "circle",
"source": "local-osm-points",
"source-layer": "planet_osm_point",
"filter": [
"==",
"natural",
"peak"
],
"minzoom": 10,
"paint": {
"circle-radius": 4.5,
"circle-color": "#991b1b",
"circle-stroke-width": 1.5,
"circle-stroke-color": "#ffffff"
}
},
{
"id": "mountain-peaks-labels",
"type": "symbol",
"source": "local-osm-points",
"source-layer": "planet_osm_point",
"filter": [
"==",
"natural",
"peak"
],
"minzoom": 10,
"layout": {
"text-field": [
"format",
["coalesce", ["get", "name:ar"], ["get", "name"], "قمة جبلية"],
{},
"\n▲ ",
{ "font-scale": 0.8 },
["concat", ["coalesce", ["get", "ele"], ""], " م"],
{ "font-scale": 0.75 }
],
"text-font": ["Noto Sans Arabic Regular", "Open Sans Regular"],
"text-size": 11.5,
"text-offset": [0, 1.2],
"text-anchor": "top",
"text-max-width": 10
},
"paint": {
"text-color": "#451a03",
"text-halo-color": "#ffffff",
"text-halo-width": 2
}
},
{
"id": "jordan-city-labels",
"type": "symbol",
"source": "places_jordan",
"source-layer": "places_jordan",
"filter": [
"any",
["==", "category", "city"],
["==", "category", "town"],
["==", "category", "governorate"],
["==", "category", "capital"]
],
"minzoom": 6,
"maxzoom": 11,
"layout": {
"text-field": ["coalesce", ["get", "name_ar"], ["get", "name"], ""],
"text-font": ["Noto Sans Arabic Bold", "Open Sans Bold"],
"text-size": [
"interpolate",
["linear"],
["zoom"],
6, 10,
9, 12,
11, 14
],
"text-padding": 24,
"text-allow-overlap": false,
"text-ignore-placement": false,
"text-transform": "uppercase",
"text-letter-spacing": 0.05
},
"paint": {
"text-color": "#1c1917",
"text-halo-color": "#fafaf9",
"text-halo-width": 2.2
}
}
]
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+457
View File
@@ -0,0 +1,457 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>خطة العمل التنفيذية والهندسية | سيرو ماب ونظام المرشدين المحليين</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;500;700;800;900&family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-base: #0a0f1d;
--bg-surface: #111827;
--bg-card: #1e293b;
--border-subtle: rgba(255, 255, 255, 0.08);
--border-accent: rgba(56, 189, 248, 0.3);
--text-primary: #f8fafc;
--text-secondary: #94a3b8;
--accent-cyan: #38bdf8;
--accent-emerald: #10b981;
--accent-gold: #f59e0b;
--accent-purple: #a855f7;
--accent-rose: #f43f5e;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-base);
color: var(--text-primary);
font-family: 'Tajawal', sans-serif;
line-height: 1.8;
padding: 2.5rem 1.25rem;
}
.container {
max-width: 1180px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 3rem;
padding-bottom: 2rem;
border-bottom: 1px solid var(--border-subtle);
}
.badge {
display: inline-block;
padding: 0.35rem 1.2rem;
background: rgba(56, 189, 248, 0.1);
color: var(--accent-cyan);
border: 1px solid rgba(56, 189, 248, 0.3);
border-radius: 9999px;
font-size: 0.85rem;
font-weight: 800;
margin-bottom: 1rem;
letter-spacing: 0.5px;
}
h1 {
font-size: 2.3rem;
font-weight: 900;
color: #ffffff;
margin-bottom: 0.75rem;
line-height: 1.3;
}
.subtitle {
color: var(--text-secondary);
font-size: 1.1rem;
max-width: 850px;
margin: 0 auto;
}
.pillar-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.5rem;
margin-bottom: 3rem;
}
.pillar-card {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: 14px;
padding: 1.75rem;
position: relative;
}
.pillar-card.highlight {
border-color: var(--accent-cyan);
box-shadow: 0 10px 30px -10px rgba(56, 189, 248, 0.15);
}
.pillar-icon {
width: 44px;
height: 44px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1rem;
font-weight: 900;
font-size: 1.2rem;
}
.pillar-title {
font-size: 1.2rem;
font-weight: 800;
color: #ffffff;
margin-bottom: 0.5rem;
}
.pillar-desc {
color: var(--text-secondary);
font-size: 0.95rem;
}
.section-title {
font-size: 1.6rem;
font-weight: 900;
color: #ffffff;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
border-right: 4px solid var(--accent-cyan);
padding-right: 0.75rem;
}
.phase-box {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: 14px;
padding: 2rem;
margin-bottom: 2rem;
}
.phase-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.25rem;
flex-wrap: wrap;
gap: 0.75rem;
}
.phase-num {
background: rgba(16, 185, 129, 0.15);
color: var(--accent-emerald);
border: 1px solid rgba(16, 185, 129, 0.3);
padding: 0.25rem 0.75rem;
border-radius: 6px;
font-weight: 800;
font-size: 0.85rem;
font-family: 'JetBrains Mono', monospace;
}
.phase-name {
font-size: 1.35rem;
font-weight: 800;
color: #ffffff;
}
.code-block {
direction: ltr;
text-align: left;
background: #070b14;
border: 1px solid var(--border-subtle);
border-radius: 8px;
padding: 1.25rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
color: #38bdf8;
overflow-x: auto;
margin: 1.25rem 0;
line-height: 1.6;
}
table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
font-size: 0.95rem;
}
th, td {
padding: 0.9rem 1rem;
text-align: right;
border-bottom: 1px solid var(--border-subtle);
}
th {
background: rgba(255, 255, 255, 0.03);
color: var(--accent-cyan);
font-weight: 800;
}
td {
color: var(--text-secondary);
}
td strong {
color: #ffffff;
}
.badge-point {
display: inline-block;
padding: 0.2rem 0.6rem;
background: rgba(245, 158, 11, 0.15);
color: var(--accent-gold);
border-radius: 6px;
font-weight: 800;
font-family: 'JetBrains Mono', monospace;
direction: ltr;
}
.actions {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 3rem;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.85rem 1.75rem;
border-radius: 10px;
font-weight: 800;
text-decoration: none;
font-size: 1rem;
transition: all 0.2s ease;
}
.btn-primary {
background: #0284c7;
color: #ffffff;
}
.btn-primary:hover {
background: #0369a1;
}
.btn-secondary {
background: var(--bg-card);
color: var(--text-primary);
border: 1px solid var(--border-subtle);
}
.btn-secondary:hover {
background: #334155;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="badge">SIRO MAPS MASTER ROADMAP & ARCHITECTURE</div>
<h1>خطة العمل التنفيذية والهندسية: منظومة سيرو ماب</h1>
<p class="subtitle">
بناء المسارات التراثية ونظام المرشدين المحليين (Local Guides) مع التدقيق اليدوي ومحرك التلعيب، مع ضمان العزل التام للخدمات التجارية القائمة.
</p>
</header>
<!-- Operating Principles -->
<div class="pillar-grid">
<div class="pillar-card highlight">
<div class="pillar-icon" style="background: rgba(56, 189, 248, 0.15); color: var(--accent-cyan);">1</div>
<div class="pillar-title">الكتابة على الماك والبناء على السيرفر</div>
<div class="pillar-desc">
تطوير الكود واختباره محلياً في بيئة العمل الصارمة، ثم المزامنة التكتيكية مع خادم الإنتاج عبر سكريبت المزامنة الجاهز.
</div>
</div>
<div class="pillar-card highlight">
<div class="pillar-icon" style="background: rgba(16, 185, 129, 0.15); color: var(--accent-emerald);">2</div>
<div class="pillar-title">عزل تام لخدمات الإنتاج (Zero Risk)</div>
<div class="pillar-desc">
محرك الملاحة GraphHopper، والبحث الجغرافي B2B، وتتبع السائقين، والفوترة تبقى معزولة تماماً بنسبة 100% دون أي تعديل.
</div>
</div>
<div class="pillar-card highlight">
<div class="pillar-icon" style="background: rgba(245, 158, 11, 0.15); color: var(--accent-gold);">3</div>
<div class="pillar-title">التلعيب والتدقيق اليدوي الصارم</div>
<div class="pillar-desc">
نظام مرشدين وطني يمنح نقاطاً وأوسمة للمساهمات، ولكن لا يُنشر أي تعديل للخريطة إلا بعد موافقة المشرف من لوحة التحكم.
</div>
</div>
</div>
<!-- Phase 1 -->
<div class="phase-box">
<div class="phase-header">
<div class="phase-name">المرحلة 1: البنية التحتية والمخططات المعزولة في PostGIS</div>
<div class="phase-num">PHASE 01 : DATABASE</div>
</div>
<p style="color: var(--text-secondary); margin-bottom: 1rem;">
إنشاء مخططي <code>heritage</code> و <code>community</code> داخل نفس قاعدة البيانات لتجنب استهلاك الذاكرة، مع ضمان سرعة الربط المكاني اللحظي عبر خادم مارتن للبلاطات الشعاعية.
</p>
<div class="code-block">
CREATE SCHEMA IF NOT EXISTS heritage;
CREATE SCHEMA IF NOT EXISTS community;
-- جدول المعالم والبوابات الدقيقة
CREATE TABLE heritage.landmarks (
id SERIAL PRIMARY KEY,
name_ar VARCHAR(255) NOT NULL,
category VARCHAR(100) NOT NULL,
centroid_geom geometry(Point, 4326) NOT NULL,
access_gate_geom geometry(Point, 4326),
parking_geom geometry(Point, 4326),
narrative_ar TEXT,
audio_url VARCHAR(500)
);
-- جدول مساهمات المرشدين الخاضعة للمراجعة
CREATE TABLE community.contributions (
id SERIAL PRIMARY KEY,
guide_id INT,
contribution_type VARCHAR(50), -- ADD_PLACE, CORRECT_STREET, CONFIRM_GATE
suggested_data JSONB,
suggested_geom geometry(Geometry, 4326),
status VARCHAR(30) DEFAULT 'PENDING'
);
</div>
</div>
<!-- Phase 2 -->
<div class="phase-box">
<div class="phase-header">
<div class="phase-name">المرحلة 2: الوحدات المستقلة في خادم التطبيقات (NestJS Modules)</div>
<div class="phase-num">PHASE 02 : BACKEND API</div>
</div>
<p style="color: var(--text-secondary); margin-bottom: 1rem;">
بناء موديولين معزولين تماماً داخل <code>apps/api</code>:
</p>
<ul style="color: var(--text-secondary); padding-right: 1.5rem; margin-bottom: 1.25rem;">
<li><code>HeritageModule</code>: لخدمة المعالم، المسارات الستة، والملفات الصوتية.</li>
<li><code>CommunityModule</code>: لإدارة نقاط المرشدين، لوحة المتصدرين، وتلقي المساهمات.</li>
</ul>
<h4 style="color: var(--accent-gold); margin-top: 1.5rem; margin-bottom: 0.5rem;">جدول نقاط التلعيب المعتمد للمرشدين (Gamification Scorecard):</h4>
<table>
<thead>
<tr>
<th>نوع المساهمة الميدانية</th>
<th>النقاط الممنوحة</th>
<th>طريقة التدقيق</th>
<th>الأثر الميداني</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>تأكيد بوابة أو موقف سيارات</strong></td>
<td><span class="badge-point">+50 PTS</span></td>
<td>فحص جوي سريع بالأقمار الصناعية</td>
<td>توجيه السيارات مباشرة للمدخل الحقيقي</td>
</tr>
<tr>
<td><strong>إضافة معلم محلي أو تراثي جديد</strong></td>
<td><span class="badge-point">+100 PTS</span></td>
<td>تدقيق يدوي عبر لوحة المشرف</td>
<td>إثراء الخريطة السيادية بمعالم غير متوفرة تجارياً</td>
</tr>
<tr>
<td><strong>تصحيح اسم شارع أو اتجاه</strong></td>
<td><span class="badge-point">+75 PTS</span></td>
<td>فحص الاتصال بشبكة الطرق</td>
<td>رفع جودة التوجيه الملاحي في القرى والبوادي</td>
</tr>
<tr>
<td><strong>توثيق حكاية أو تسجيل صوتي تراثي</strong></td>
<td><span class="badge-point">+150 PTS</span></td>
<td>مراجعة محتوى وتوافق تاريخي</td>
<td>ربط المكان بالسردية الوطنية الأردنية</td>
</tr>
</tbody>
</table>
</div>
<!-- Phase 3 -->
<div class="phase-box">
<div class="phase-header">
<div class="phase-name">المرحلة 3: لوحة التدقيق الإدارية الخفيفة (Moderation Console)</div>
<div class="phase-num">PHASE 03 : ADMIN CONSOLE</div>
</div>
<p style="color: var(--text-secondary); margin-bottom: 1rem;">
شاشة خفيفة وسريعة جداً مبنية داخل لوحة التحكم الحالية (Vanilla JS بدون مكتبات ثقيلة)، تمكن المشرف من:
</p>
<ul style="color: var(--text-secondary); padding-right: 1.5rem;">
<li>استعراض المساهمات المعلقة ومقارنتها فوراً مع صور الأقمار الصناعية.</li>
<li>الموافقة بضغطة زر واحدة (Approve) فتنزل النقطة فوراً في خريطة الإنتاج وتُصرف النقاط للمرشد.</li>
<li>الرفض مع كتابة سبب تنبيهي للمرشد (Reject with Feedback) لحماية نقاوة البيانات.</li>
</ul>
</div>
<!-- Phase 4 -->
<div class="phase-box">
<div class="phase-header">
<div class="phase-name">المرحلة 4: تكامل تطبيق الموبايل (Flutter - Siro Maps)</div>
<div class="phase-num">PHASE 04 : MOBILE APP</div>
</div>
<p style="color: var(--text-secondary); margin-bottom: 1rem;">
تفعيل الميزات التالية داخل كود <code>apps/siro_maps</code>:
</p>
<ul style="color: var(--text-secondary); padding-right: 1.5rem;">
<li>مفتاح تفعيل طبقة المعالم والمسارات التراثية التكتيكية.</li>
<li>واجهة "اقترح تعديلاً أو أضف مكاناً" مع التقاط الإحداثية الحالية للمستخدم.</li>
<li>شاشة ملف المرشد الشخصي (رصيد النقاط، الرتبة، والأوسمة المكتسبة).</li>
<li>دعم التخزين المحلي Offline Cache لضمان العمل في المناطق النائية.</li>
</ul>
</div>
<!-- Phase 5 -->
<div class="phase-box">
<div class="phase-header">
<div class="phase-name">المرحلة 5: المزامنة، النشر والتحقق (Deployment & Testing)</div>
<div class="phase-num">PHASE 05 : VERIFICATION</div>
</div>
<p style="color: var(--text-secondary); margin-bottom: 1rem;">
دورة النشر الآمنة:
</p>
<div class="code-block">
1. Run local integration tests on Mac
2. bash sync_to_server.sh
3. Execute database migration on map-db container
4. Rebuild API container safely: docker compose up -d --build api
5. Smoke test commercial B2B endpoints (Route, Geocode) -> Must return 200 OK
6. Verify new Heritage & Community endpoints -> Must return 200 OK
</div>
</div>
<div class="actions">
<a href="/architecture-diagram.html" class="btn btn-secondary">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
معاينة المخطط المعماري
</a>
<a href="/siro-maps.html" class="btn btn-primary">
بوابة سيرو ماب التفاعلية
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
</div>
</body>
</html>
+457
View File
@@ -0,0 +1,457 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>خطة العمل التنفيذية والهندسية | سيرو ماب ونظام المرشدين المحليين</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;500;700;800;900&family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-base: #0a0f1d;
--bg-surface: #111827;
--bg-card: #1e293b;
--border-subtle: rgba(255, 255, 255, 0.08);
--border-accent: rgba(56, 189, 248, 0.3);
--text-primary: #f8fafc;
--text-secondary: #94a3b8;
--accent-cyan: #38bdf8;
--accent-emerald: #10b981;
--accent-gold: #f59e0b;
--accent-purple: #a855f7;
--accent-rose: #f43f5e;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-base);
color: var(--text-primary);
font-family: 'Tajawal', sans-serif;
line-height: 1.8;
padding: 2.5rem 1.25rem;
}
.container {
max-width: 1180px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 3rem;
padding-bottom: 2rem;
border-bottom: 1px solid var(--border-subtle);
}
.badge {
display: inline-block;
padding: 0.35rem 1.2rem;
background: rgba(56, 189, 248, 0.1);
color: var(--accent-cyan);
border: 1px solid rgba(56, 189, 248, 0.3);
border-radius: 9999px;
font-size: 0.85rem;
font-weight: 800;
margin-bottom: 1rem;
letter-spacing: 0.5px;
}
h1 {
font-size: 2.3rem;
font-weight: 900;
color: #ffffff;
margin-bottom: 0.75rem;
line-height: 1.3;
}
.subtitle {
color: var(--text-secondary);
font-size: 1.1rem;
max-width: 850px;
margin: 0 auto;
}
.pillar-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.5rem;
margin-bottom: 3rem;
}
.pillar-card {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: 14px;
padding: 1.75rem;
position: relative;
}
.pillar-card.highlight {
border-color: var(--accent-cyan);
box-shadow: 0 10px 30px -10px rgba(56, 189, 248, 0.15);
}
.pillar-icon {
width: 44px;
height: 44px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1rem;
font-weight: 900;
font-size: 1.2rem;
}
.pillar-title {
font-size: 1.2rem;
font-weight: 800;
color: #ffffff;
margin-bottom: 0.5rem;
}
.pillar-desc {
color: var(--text-secondary);
font-size: 0.95rem;
}
.section-title {
font-size: 1.6rem;
font-weight: 900;
color: #ffffff;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
border-right: 4px solid var(--accent-cyan);
padding-right: 0.75rem;
}
.phase-box {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: 14px;
padding: 2rem;
margin-bottom: 2rem;
}
.phase-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.25rem;
flex-wrap: wrap;
gap: 0.75rem;
}
.phase-num {
background: rgba(16, 185, 129, 0.15);
color: var(--accent-emerald);
border: 1px solid rgba(16, 185, 129, 0.3);
padding: 0.25rem 0.75rem;
border-radius: 6px;
font-weight: 800;
font-size: 0.85rem;
font-family: 'JetBrains Mono', monospace;
}
.phase-name {
font-size: 1.35rem;
font-weight: 800;
color: #ffffff;
}
.code-block {
direction: ltr;
text-align: left;
background: #070b14;
border: 1px solid var(--border-subtle);
border-radius: 8px;
padding: 1.25rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
color: #38bdf8;
overflow-x: auto;
margin: 1.25rem 0;
line-height: 1.6;
}
table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
font-size: 0.95rem;
}
th, td {
padding: 0.9rem 1rem;
text-align: right;
border-bottom: 1px solid var(--border-subtle);
}
th {
background: rgba(255, 255, 255, 0.03);
color: var(--accent-cyan);
font-weight: 800;
}
td {
color: var(--text-secondary);
}
td strong {
color: #ffffff;
}
.badge-point {
display: inline-block;
padding: 0.2rem 0.6rem;
background: rgba(245, 158, 11, 0.15);
color: var(--accent-gold);
border-radius: 6px;
font-weight: 800;
font-family: 'JetBrains Mono', monospace;
direction: ltr;
}
.actions {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 3rem;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.85rem 1.75rem;
border-radius: 10px;
font-weight: 800;
text-decoration: none;
font-size: 1rem;
transition: all 0.2s ease;
}
.btn-primary {
background: #0284c7;
color: #ffffff;
}
.btn-primary:hover {
background: #0369a1;
}
.btn-secondary {
background: var(--bg-card);
color: var(--text-primary);
border: 1px solid var(--border-subtle);
}
.btn-secondary:hover {
background: #334155;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="badge">SIRO MAPS MASTER ROADMAP & ARCHITECTURE</div>
<h1>خطة العمل التنفيذية والهندسية: منظومة سيرو ماب</h1>
<p class="subtitle">
بناء المسارات التراثية ونظام المرشدين المحليين (Local Guides) مع التدقيق اليدوي ومحرك التلعيب، مع ضمان العزل التام للخدمات التجارية القائمة.
</p>
</header>
<!-- Operating Principles -->
<div class="pillar-grid">
<div class="pillar-card highlight">
<div class="pillar-icon" style="background: rgba(56, 189, 248, 0.15); color: var(--accent-cyan);">1</div>
<div class="pillar-title">الكتابة على الماك والبناء على السيرفر</div>
<div class="pillar-desc">
تطوير الكود واختباره محلياً في بيئة العمل الصارمة، ثم المزامنة التكتيكية مع خادم الإنتاج عبر سكريبت المزامنة الجاهز.
</div>
</div>
<div class="pillar-card highlight">
<div class="pillar-icon" style="background: rgba(16, 185, 129, 0.15); color: var(--accent-emerald);">2</div>
<div class="pillar-title">عزل تام لخدمات الإنتاج (Zero Risk)</div>
<div class="pillar-desc">
محرك الملاحة GraphHopper، والبحث الجغرافي B2B، وتتبع السائقين، والفوترة تبقى معزولة تماماً بنسبة 100% دون أي تعديل.
</div>
</div>
<div class="pillar-card highlight">
<div class="pillar-icon" style="background: rgba(245, 158, 11, 0.15); color: var(--accent-gold);">3</div>
<div class="pillar-title">التلعيب والتدقيق اليدوي الصارم</div>
<div class="pillar-desc">
نظام مرشدين وطني يمنح نقاطاً وأوسمة للمساهمات، ولكن لا يُنشر أي تعديل للخريطة إلا بعد موافقة المشرف من لوحة التحكم.
</div>
</div>
</div>
<!-- Phase 1 -->
<div class="phase-box">
<div class="phase-header">
<div class="phase-name">المرحلة 1: البنية التحتية والمخططات المعزولة في PostGIS</div>
<div class="phase-num">PHASE 01 : DATABASE</div>
</div>
<p style="color: var(--text-secondary); margin-bottom: 1rem;">
إنشاء مخططي <code>heritage</code> و <code>community</code> داخل نفس قاعدة البيانات لتجنب استهلاك الذاكرة، مع ضمان سرعة الربط المكاني اللحظي عبر خادم مارتن للبلاطات الشعاعية.
</p>
<div class="code-block">
CREATE SCHEMA IF NOT EXISTS heritage;
CREATE SCHEMA IF NOT EXISTS community;
-- جدول المعالم والبوابات الدقيقة
CREATE TABLE heritage.landmarks (
id SERIAL PRIMARY KEY,
name_ar VARCHAR(255) NOT NULL,
category VARCHAR(100) NOT NULL,
centroid_geom geometry(Point, 4326) NOT NULL,
access_gate_geom geometry(Point, 4326),
parking_geom geometry(Point, 4326),
narrative_ar TEXT,
audio_url VARCHAR(500)
);
-- جدول مساهمات المرشدين الخاضعة للمراجعة
CREATE TABLE community.contributions (
id SERIAL PRIMARY KEY,
guide_id INT,
contribution_type VARCHAR(50), -- ADD_PLACE, CORRECT_STREET, CONFIRM_GATE
suggested_data JSONB,
suggested_geom geometry(Geometry, 4326),
status VARCHAR(30) DEFAULT 'PENDING'
);
</div>
</div>
<!-- Phase 2 -->
<div class="phase-box">
<div class="phase-header">
<div class="phase-name">المرحلة 2: الوحدات المستقلة في خادم التطبيقات (NestJS Modules)</div>
<div class="phase-num">PHASE 02 : BACKEND API</div>
</div>
<p style="color: var(--text-secondary); margin-bottom: 1rem;">
بناء موديولين معزولين تماماً داخل <code>apps/api</code>:
</p>
<ul style="color: var(--text-secondary); padding-right: 1.5rem; margin-bottom: 1.25rem;">
<li><code>HeritageModule</code>: لخدمة المعالم، المسارات الستة، والملفات الصوتية.</li>
<li><code>CommunityModule</code>: لإدارة نقاط المرشدين، لوحة المتصدرين، وتلقي المساهمات.</li>
</ul>
<h4 style="color: var(--accent-gold); margin-top: 1.5rem; margin-bottom: 0.5rem;">جدول نقاط التلعيب المعتمد للمرشدين (Gamification Scorecard):</h4>
<table>
<thead>
<tr>
<th>نوع المساهمة الميدانية</th>
<th>النقاط الممنوحة</th>
<th>طريقة التدقيق</th>
<th>الأثر الميداني</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>تأكيد بوابة أو موقف سيارات</strong></td>
<td><span class="badge-point">+50 PTS</span></td>
<td>فحص جوي سريع بالأقمار الصناعية</td>
<td>توجيه السيارات مباشرة للمدخل الحقيقي</td>
</tr>
<tr>
<td><strong>إضافة معلم محلي أو تراثي جديد</strong></td>
<td><span class="badge-point">+100 PTS</span></td>
<td>تدقيق يدوي عبر لوحة المشرف</td>
<td>إثراء الخريطة السيادية بمعالم غير متوفرة تجارياً</td>
</tr>
<tr>
<td><strong>تصحيح اسم شارع أو اتجاه</strong></td>
<td><span class="badge-point">+75 PTS</span></td>
<td>فحص الاتصال بشبكة الطرق</td>
<td>رفع جودة التوجيه الملاحي في القرى والبوادي</td>
</tr>
<tr>
<td><strong>توثيق حكاية أو تسجيل صوتي تراثي</strong></td>
<td><span class="badge-point">+150 PTS</span></td>
<td>مراجعة محتوى وتوافق تاريخي</td>
<td>ربط المكان بالسردية الوطنية الأردنية</td>
</tr>
</tbody>
</table>
</div>
<!-- Phase 3 -->
<div class="phase-box">
<div class="phase-header">
<div class="phase-name">المرحلة 3: لوحة التدقيق الإدارية الخفيفة (Moderation Console)</div>
<div class="phase-num">PHASE 03 : ADMIN CONSOLE</div>
</div>
<p style="color: var(--text-secondary); margin-bottom: 1rem;">
شاشة خفيفة وسريعة جداً مبنية داخل لوحة التحكم الحالية (Vanilla JS بدون مكتبات ثقيلة)، تمكن المشرف من:
</p>
<ul style="color: var(--text-secondary); padding-right: 1.5rem;">
<li>استعراض المساهمات المعلقة ومقارنتها فوراً مع صور الأقمار الصناعية.</li>
<li>الموافقة بضغطة زر واحدة (Approve) فتنزل النقطة فوراً في خريطة الإنتاج وتُصرف النقاط للمرشد.</li>
<li>الرفض مع كتابة سبب تنبيهي للمرشد (Reject with Feedback) لحماية نقاوة البيانات.</li>
</ul>
</div>
<!-- Phase 4 -->
<div class="phase-box">
<div class="phase-header">
<div class="phase-name">المرحلة 4: تكامل تطبيق الموبايل (Flutter - Siro Maps)</div>
<div class="phase-num">PHASE 04 : MOBILE APP</div>
</div>
<p style="color: var(--text-secondary); margin-bottom: 1rem;">
تفعيل الميزات التالية داخل كود <code>apps/siro_maps</code>:
</p>
<ul style="color: var(--text-secondary); padding-right: 1.5rem;">
<li>مفتاح تفعيل طبقة المعالم والمسارات التراثية التكتيكية.</li>
<li>واجهة "اقترح تعديلاً أو أضف مكاناً" مع التقاط الإحداثية الحالية للمستخدم.</li>
<li>شاشة ملف المرشد الشخصي (رصيد النقاط، الرتبة، والأوسمة المكتسبة).</li>
<li>دعم التخزين المحلي Offline Cache لضمان العمل في المناطق النائية.</li>
</ul>
</div>
<!-- Phase 5 -->
<div class="phase-box">
<div class="phase-header">
<div class="phase-name">المرحلة 5: المزامنة، النشر والتحقق (Deployment & Testing)</div>
<div class="phase-num">PHASE 05 : VERIFICATION</div>
</div>
<p style="color: var(--text-secondary); margin-bottom: 1rem;">
دورة النشر الآمنة:
</p>
<div class="code-block">
1. Run local integration tests on Mac
2. bash sync_to_server.sh
3. Execute database migration on map-db container
4. Rebuild API container safely: docker compose up -d --build api
5. Smoke test commercial B2B endpoints (Route, Geocode) -> Must return 200 OK
6. Verify new Heritage & Community endpoints -> Must return 200 OK
</div>
</div>
<div class="actions">
<a href="/architecture-diagram.html" class="btn btn-secondary">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
معاينة المخطط المعماري
</a>
<a href="/siro-maps.html" class="btn btn-primary">
بوابة سيرو ماب التفاعلية
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
</div>
</body>
</html>
+80
View File
@@ -0,0 +1,80 @@
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#061d42">
<meta name="description" content="سيرو ماب: خرائط سهلة بلا تسجيل للعراق والأردن وسوريا ومصر، ورؤية لدليل سياحي يبدأ من معالم العراق.">
<title>سيرو ماب — كل طريق، بداية حكاية</title>
<link rel="icon" type="image/png" href="assets/siro-maps.png">
<style>
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(assets/tajawal-400.ttf) format('truetype');
}
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(assets/tajawal-500.ttf) format('truetype');
}
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(assets/tajawal-700.ttf) format('truetype');
}
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(assets/tajawal-800.ttf) format('truetype');
}
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(assets/tajawal-900.ttf) format('truetype');
}
:root{--navy:#061d42;--blue:#124f9f;--gold:#b59247;--pale:#edf3f9;--ink:#132844;--muted:#5c6c80;--line:#dce3ec;--white:#fff;--font:'Tajawal','Geeza Pro',Tahoma,sans-serif}
*{box-sizing:border-box}html{scroll-behavior:smooth;scroll-padding-top:100px}body{margin:0;background:#fff;color:var(--ink);font-family:var(--font);font-size:18px;line-height:1.85}a{color:inherit;text-decoration:none}button{font:inherit}button,a{-webkit-tap-highlight-color:transparent}button:focus-visible,a:focus-visible{outline:3px solid #c6a057;outline-offset:6px}button{cursor:pointer}img{max-width:100%;height:auto;display:block}h1,h2,h3,p{margin:0}h1,h2,h3{line-height:1.35}h2{font-size:clamp(32px,3.5vw,49px);font-weight:800;letter-spacing:-.6px}h3{font-size:25px}::selection{background:#d6bb7b;color:var(--navy)}.wrap{width:min(1190px,calc(100% - 88px));margin:auto}.eyebrow{font-size:14px;letter-spacing:1.8px;font-weight:700;color:var(--gold);display:flex;align-items:center;gap:12px;margin-bottom:22px}.eyebrow:before{content:'';width:28px;height:2px;background:currentColor}.latin{font-family:Arial,sans-serif;letter-spacing:2.5px;font-size:12px;direction:ltr;display:inline-block}.muted{color:var(--muted)}
header{height:100px;background:rgba(255,255,255,.97);border-bottom:1px solid var(--line);position:relative;z-index:5}.nav{height:100%;display:flex;align-items:center;justify-content:space-between;gap:24px}.brand{display:flex;gap:13px;align-items:center}.brand img{width:62px;height:62px;border-radius:15px}.brand strong{font-size:23px;display:block;line-height:1.3}.brand small{color:var(--muted);font-size:11px;letter-spacing:2.2px;display:block;direction:ltr}.links{display:flex;align-items:center;gap:30px;font-size:16px;font-weight:600}.links a{transition:color .2s}.links a:hover{color:var(--blue)}.nav-tag{font-size:14px;color:var(--blue);border:1px solid #c9d8ed;padding:7px 17px;border-radius:30px;white-space:nowrap}
.hero{position:relative;overflow:hidden;background:var(--navy);color:white;padding:75px 0 0}.hero:before{content:'';position:absolute;width:740px;height:740px;left:-140px;top:-220px;background:radial-gradient(circle,rgba(39,105,180,.38),transparent 67%);pointer-events:none}.hero-grid{display:grid;grid-template-columns:1.12fr 1fr;align-items:center;gap:65px;position:relative;min-height:470px;padding-bottom:65px}.hero h1{font-size:clamp(44px,5.7vw,76px);font-weight:800;line-height:1.24;letter-spacing:-1px}.hero h1 span{color:#dbbc77}.hero p{font-size:20px;color:#c0ccdc;max-width:530px;margin-top:27px;line-height:1.9}.hero .eyebrow{color:#ddbd78}.hero-actions{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-top:33px}.btn{display:inline-flex;align-items:center;justify-content:center;gap:20px;padding:11px 23px;border-radius:7px;font-size:17px;font-weight:700;border:1px solid transparent;min-height:50px}.btn-gold{background:#dbbc77;color:#0b2547}.btn-gold:hover{background:#efd49b}.btn-outline{border-color:#526580;color:#e3eaf3}.btn-outline:hover{background:#ffffff0c}.arrow{font-family:Arial;font-size:23px;font-weight:400}.hero-status{display:block;font-size:14px;color:#acbbcf;margin-top:17px}.brand-stage{position:relative;display:flex;justify-content:center;align-items:center;min-height:420px;padding:15px}.brand-stage:before,.brand-stage:after{content:'';position:absolute;border:1px solid #759ed12b;border-radius:50%;width:410px;height:410px;pointer-events:none}.brand-stage:after{width:480px;height:480px;border-style:dashed;opacity:.6}.hero-icon{width:330px;border-radius:70px;transform:rotate(-6deg);box-shadow:0 30px 60px #0004;z-index:1}.float-label{position:absolute;z-index:2;border:1px solid #6986af6b;background:#0b2b55eb;box-shadow:0 10px 35px #0002;backdrop-filter:blur(15px);padding:10px 17px;border-radius:10px;font-size:16px;display:flex;align-items:center;gap:12px}.float-label svg{width:22px;height:22px;stroke:#e0bf7d;fill:none;stroke-width:1.6}.label-one{left:-5px;top:60px}.label-two{right:4px;bottom:43px}.hero-facts{position:relative;border-top:1px solid #ffffff20;display:grid;grid-template-columns:repeat(3,1fr);padding:24px 0;gap:30px}.fact{display:flex;gap:17px;align-items:center}.fact:not(:last-child){border-left:1px solid #ffffff20}.fact b{font-size:35px;color:#dfc28a;font-weight:500;line-height:1}.fact span{display:block;font-size:17px}.fact small{display:block;color:#a8b8cd;font-size:14px}
.story{padding:88px 0;display:grid;grid-template-columns:.9fr 1.1fr;gap:95px;align-items:center}.story p{color:var(--muted);font-size:20px}.story .accent{color:var(--ink);font-weight:700}.story .eyebrow{margin-bottom:16px}.section-intro{display:flex;align-items:flex-end;justify-content:space-between;gap:40px;margin-bottom:37px}.section-intro p{max-width:430px;color:var(--muted)}
.coverage{background:var(--pale);padding:72px 0 78px}.country-tabs{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:24px}.country-tab{display:flex;align-items:center;justify-content:space-between;padding:17px 22px;background:transparent;border:1px solid #cdd8e6;color:var(--ink);border-radius:8px;transition:background .2s;min-height:83px}.country-tab strong{font-size:23px}.country-tab small{display:block;font-size:12px;letter-spacing:2px;line-height:1.2;color:var(--muted);direction:ltr}.country-tab .number{font-family:Arial;font-size:14px;color:#8b9db3}.country-tab[aria-selected=true]{background:var(--navy);border-color:var(--navy);color:white}.country-tab[aria-selected=true] small,.country-tab[aria-selected=true] .number{color:#d8bd81}.country-panel{background:#fff;border-radius:12px;padding:27px 32px;display:grid;grid-template-columns:1fr 1.4fr;gap:35px;align-items:center;border:1px solid #e2e9f2}.country-panel h3{font-size:28px;margin-bottom:5px}.country-panel p{font-size:17px;color:var(--muted)}.panel-note{border-right:2px solid #c4a266;padding-right:24px}.pill{display:inline-block;font-size:13px;border:1px solid #d9e1ec;background:#f7f9fc;padding:2px 10px;border-radius:30px;color:#49617f;margin-bottom:9px}
.tourism{padding:90px 0}.tourism-feature{display:grid;grid-template-columns:1.15fr 1fr;background:var(--navy);color:white;border-radius:18px;overflow:hidden;min-height:415px}.tourism-photo{position:relative;background:#15395e;min-height:380px;overflow:hidden}.tourism-photo img{width:100%;height:100%;object-fit:cover;position:absolute}.photo-caption{position:absolute;bottom:0;right:0;left:0;color:white;padding:45px 28px 22px;background:linear-gradient(transparent,#041a35e6);font-size:16px}.photo-caption small{display:block;opacity:.8;font-size:12px;direction:ltr;text-align:right}.tourism-copy{padding:42px 40px;align-self:center}.tourism-copy h3{font-size:35px;margin:12px 0 15px}.tourism-copy p{color:#beccdd}.tourism-copy .pill{background:#ffffff0c;border-color:#597087;color:#e2c388}.tourism-list{padding:0;list-style:none;display:grid;grid-template-columns:1fr 1fr;gap:13px;margin:25px 0 0;font-size:16px;color:#e7edf6}.tourism-list li:before{content:'+';color:#d8b975;padding-left:10px}.tourism-foot{display:flex;justify-content:space-between;gap:25px;margin-top:20px;font-size:15px;color:var(--muted)}.tourism-foot a{color:var(--blue);text-decoration:underline;text-underline-offset:4px}
.ideas{background:#f7f9fc;padding:75px 0 82px}.ideas-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:0;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}.idea{padding:32px 29px 30px}.idea:not(:last-child){border-left:1px solid var(--line)}.idea-index{font-size:13px;direction:ltr;display:block;text-align:right;color:#977837;letter-spacing:2px;margin-bottom:22px}.idea svg{width:34px;height:34px;fill:none;stroke:var(--blue);stroke-width:1.5;margin-bottom:15px}.idea h3{margin-bottom:13px;font-size:25px}.idea p{font-size:17px;color:var(--muted)}.benefit{margin-top:24px;padding-top:17px;border-top:1px solid var(--line);font-size:15px;color:var(--blue)}.ideas-note{font-size:14px;color:var(--muted);margin-top:20px}
.roadmap{padding:90px 0}.steps{display:grid;grid-template-columns:repeat(3,1fr);gap:32px;margin-top:42px}.step{border-top:2px solid #dce3ec;padding-top:22px}.step.current{border-top-color:var(--gold)}.step-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}.step-top span{font-size:14px;color:#728096}.step-top b{font-size:28px;font-family:Arial;font-weight:400;color:#a7b4c4}.step.current .step-top span{color:#907132}.step h3{font-size:25px;margin-bottom:12px}.step p{color:var(--muted);font-size:17px}
.partnership{border:1px solid var(--line);border-radius:15px;display:grid;grid-template-columns:160px 1fr;gap:40px;padding:32px 40px;align-items:center;margin-bottom:70px}.prize-logo{width:150px}.partnership .eyebrow{font-size:13px;margin-bottom:11px}.partnership h3{font-size:29px;margin-bottom:9px}.partnership p{font-size:18px;color:var(--muted);max-width:820px}.closing{background:var(--navy);color:white;text-align:center;padding:65px 24px}.closing .latin{color:#dbbc77;margin-bottom:15px}.closing h2{font-size:clamp(29px,4vw,46px)}.closing p{color:#acbed4;margin-top:15px;font-size:18px}.footer{padding:23px 0;display:flex;justify-content:space-between;align-items:center;gap:20px;font-size:14px;color:var(--muted)}.footer a{color:var(--blue)}.footer .latin{font-size:11px;letter-spacing:1px}
@media(min-width:1500px){.hero-grid{min-height:530px}}
@media(max-width:1050px){.wrap{width:calc(100% - 48px)}.links{gap:18px}.nav-tag{display:none}.hero-grid{gap:25px}.hero-icon{width:280px;border-radius:58px}.brand-stage:before{width:340px;height:340px}.brand-stage:after{width:390px;height:390px}.label-one{left:0;top:45px}.label-two{right:0;bottom:45px}.story{gap:45px}.tourism-copy{padding:30px}.idea{padding:28px 20px}.country-tab{padding:15px}.hero h1{font-size:58px}}
@media(max-width:760px){body{font-size:17px}.wrap{width:calc(100% - 36px)}header{height:auto;min-height:86px}.nav{padding:13px 0;flex-wrap:wrap;gap:10px}.brand img{width:48px;height:48px}.brand strong{font-size:20px}.brand small{font-size:10px}.links{gap:19px;font-size:14px}.links a:last-child{display:none}.hero{padding-top:44px}.hero-grid{grid-template-columns:1fr;gap:23px;padding-bottom:35px}.hero h1{font-size:clamp(44px,10vw,62px);line-height:1.3}.hero p{font-size:18px;margin-top:20px}.hero-actions{margin-top:25px}.btn{font-size:16px;padding:10px 19px}.hero-status{font-size:13px}.brand-stage{min-height:325px;width:min(100%,390px);margin:auto}.hero-icon{width:240px;border-radius:52px}.brand-stage:before{width:285px;height:285px}.brand-stage:after{width:320px;height:320px}.float-label{font-size:14px;padding:8px 12px}.label-one{top:39px;left:0}.label-two{bottom:23px;right:0}.hero-facts{gap:15px;padding:20px 0}.fact{gap:8px;display:block}.fact b{font-size:27px}.fact span{font-size:14px}.fact small{font-size:12px}.story{grid-template-columns:1fr;gap:22px;padding:53px 0}.story p{font-size:18px}.eyebrow{font-size:13px;margin-bottom:15px}.section-intro{display:block;margin-bottom:27px}.section-intro p{margin-top:17px;font-size:17px}.coverage{padding:48px 0}.country-tabs{grid-template-columns:repeat(2,1fr);gap:9px}.country-tab{min-height:78px}.country-tab strong{font-size:22px}.country-panel{grid-template-columns:1fr;gap:18px;padding:24px}.panel-note{padding-right:16px}.tourism{padding:55px 0}.tourism-feature{grid-template-columns:1fr}.tourism-photo{min-height:280px}.tourism-copy{padding:29px 25px}.tourism-copy h3{font-size:30px}.tourism-foot{display:block;font-size:14px}.tourism-foot a{display:inline-block;margin-top:7px}.ideas{padding:50px 0}.ideas-grid{grid-template-columns:1fr}.idea{padding:28px 4px}.idea:not(:last-child){border-left:0;border-bottom:1px solid var(--line)}.idea-index{margin-bottom:14px}.idea svg{float:left}.benefit{margin-top:17px}.roadmap{padding:53px 0}.steps{grid-template-columns:1fr;gap:28px;margin-top:25px}.step{padding-top:14px}.step-top{margin-bottom:8px}.partnership{grid-template-columns:1fr;gap:18px;padding:26px;margin-bottom:45px}.prize-logo{width:120px}.partnership h3{font-size:25px}.partnership p{font-size:17px}.closing{padding:45px 22px}.footer{flex-direction:column;align-items:flex-start;gap:8px}.tourism-list{font-size:15px}}
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*{transition:none!important}}
@media print{header,.hero-actions,.hero-status,.country-tabs,.footer a{display:none}body{font-size:12pt}.hero,.closing,.tourism-feature{print-color-adjust:exact;-webkit-print-color-adjust:exact}.wrap{width:92%}.hero{padding-top:30px}.hero-grid{min-height:0;gap:25px}.hero h1{font-size:38px}.hero p{font-size:16px}.brand-stage{min-height:250px}.hero-icon{width:200px}.brand-stage:before,.brand-stage:after,.float-label{display:none}.story,.tourism,.ideas,.roadmap,.coverage{padding:25px 0}.section-intro h2,h2{font-size:28px}.story p{font-size:16px}.tourism-feature{break-inside:avoid}.idea,.step,.partnership{break-inside:avoid}.partnership{margin-bottom:25px}.closing{padding:25px}}
</style>
</head>
<body id="top">
<header><nav class="wrap nav" aria-label="التنقل الرئيسي"><a class="brand" href="#top" aria-label="سيرو ماب، بداية الصفحة"><img src="assets/siro-maps.png" alt="شعار سيرو ماب" width="62" height="62"><span><strong>سيرو ماب</strong><small>SIRO MAP</small></span></a><div class="links"><a href="#coverage">الانطلاقة</a><a href="#tourism">اكتشف العراق</a><a href="#ideas">آفاق سيرو ماب</a><a href="#journey">رحلتنا</a></div><span class="nav-tag">من سيرو ماب، إلى كل وجهة</span></nav></header>
<main>
<section class="hero" aria-labelledby="hero-title"><div class="wrap"><div class="hero-grid"><div><div class="eyebrow">الانطلاقة الأولى · سيرو ماب</div><h1 id="hero-title">كل طريق،<br>بداية <span>حكاية.</span></h1><p>خرائط تقرّب المسافات، وحكايات تقرّبنا من المكان.<br>نبدأ بأربع دول، ونفتح من العراق بابًا لاكتشاف الثقافة والمعالم والوجهات.</p><div class="hero-actions"><a class="btn btn-gold" href="#coverage">اكتشف الانطلاقة <span class="arrow" aria-hidden="true">←</span></a><a class="btn btn-outline" href="#ideas">ما الذي نبنيه معًا؟</a></div><small class="hero-status">نسخة التطبيق قيد المراجعة على Google Play</small></div><div class="brand-stage"><img class="hero-icon" src="assets/siro-maps.png" alt="شعار سيرو ماب الذكي للملاحة والتنقل" width="512" height="512"><span class="float-label label-one"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 10c0 6-8 11-8 11S4 16 4 10a8 8 0 1 1 16 0Z"/><circle cx="12" cy="10" r="2.5"/></svg>وجهتك أقرب</span><span class="float-label label-two"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m3 5 6-2 6 2 6-2v16l-6 2-6-2-6 2Z M9 3v16 M15 5v16"/></svg>اكتشف حكاية المكان</span></div></div><div class="hero-facts"><div class="fact"><b>04</b><div><span>دول في الانطلاقة</span><small>العراق · الأردن · سوريا · مصر</small></div></div><div class="fact"><b>01</b><div><span>بداية للدليل السياحي</span><small>معالم العراق أولًا</small></div></div><div class="fact"><b>ببساطة</b><div><span>خرائط بلا تسجيل</span><small>افتح التطبيق، وابدأ رحلتك</small></div></div></div></div></section>
<section class="wrap story" aria-labelledby="story-title"><div><div class="eyebrow">فكرة تجمعنا</div><h2 id="story-title">من اسم يحمل حضارة،<br>إلى خدمة ترافق الناس.</h2></div><p>تعتمد سيرو ماب على الابتكار والسيادة الرقمية وتوفير التكاليف. واليوم، نمنح هذا المشروع حضورًا قويًا في الحياة اليومية: خرائط سهلة ومستقلة، ودليل يكشف ما حولنا من وجهات ومعالم.<br><span class="accent">خطوة أولى لشراكتنا، وأساس ننمو عليه معًا.</span></p></section>
<section class="coverage" id="coverage" aria-labelledby="coverage-title"><div class="wrap"><div class="section-intro"><div><div class="eyebrow">أربع دول · تجربة واحدة</div><h2 id="coverage-title">بداية قريبة منّا.</h2></div><p>خرائط للاستخدام دون إنشاء حساب. ننطلق بهذا النطاق، ونواصل تحسين البيانات والتجربة قبل التوسع إلى مزيد من الدول العربية.</p></div><div class="country-tabs" role="tablist" aria-label="دول الانطلاقة"><button id="tab-iq" class="country-tab" role="tab" aria-selected="true" aria-controls="country-panel" data-country="iq"><span><strong>العراق</strong><small>IRAQ</small></span><span class="number">01</span></button><button id="tab-jo" class="country-tab" role="tab" aria-selected="false" aria-controls="country-panel" tabindex="-1" data-country="jo"><span><strong>الأردن</strong><small>JORDAN</small></span><span class="number">02</span></button><button id="tab-sy" class="country-tab" role="tab" aria-selected="false" aria-controls="country-panel" tabindex="-1" data-country="sy"><span><strong>سوريا</strong><small>SYRIA</small></span><span class="number">03</span></button><button id="tab-eg" class="country-tab" role="tab" aria-selected="false" aria-controls="country-panel" tabindex="-1" data-country="eg"><span><strong>مصر</strong><small>EGYPT</small></span><span class="number">04</span></button></div><div id="country-panel" class="country-panel" role="tabpanel" tabindex="0" aria-labelledby="tab-iq"><div><span class="pill">ضمن نطاق الانطلاقة</span><h3 id="country-title">العراق، هنا تبدأ الحكاية.</h3><p id="country-description">الخرائط أولًا، ومنها نبدأ بناء الدليل السياحي.</p></div><p id="country-note" class="panel-note">نبدأ بإثراء المعالم السياحية والأثرية العراقية: قصة المكان، موقعه، ومعلومات تساعد على زيارته.</p></div></div></section>
<section class="wrap tourism" id="tourism" aria-labelledby="tourism-title"><div class="section-intro"><div><div class="eyebrow">الخطوة التالية · الدليل السياحي</div><h2 id="tourism-title">للمكان أكثر من عنوان.</h2></div><p>من معالم العراق نبدأ. نريد أن تتحول النقطة على الخريطة إلى حكاية تعرفها، ووجهة تستطيع التخطيط لزيارتها.</p></div><div class="tourism-feature"><div class="tourism-photo" id="tourism-photo"><img src="assets/erbil-citadel.jpg" alt="قلعة أربيل التاريخية والأسواق المحيطة بها" width="4032" height="3024" loading="lazy"><div class="photo-caption">قلعة أربيل · العراق<small>الصورة: <a href="https://commons.wikimedia.org/wiki/File:Citadel_of_Erbil.jpg" target="_blank" rel="noopener">Sarbast.T.Hameed / Wikimedia Commons</a> · <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener">CC BY-SA 4.0</a> · عرض مقتصّ</small></div></div><div class="tourism-copy"><span class="pill">تصور قادم داخل التطبيق</span><h3>اكتشف ما حولك،<br>واعرف ما وراءه.</h3><p>دليل قريب منك للمعالم والمواقع الأثرية، بمعلومات موثقة تُراجع وتُحدّث مع الشركاء المحليين.</p><ul class="tourism-list"><li>قصة موجزة لكل معلم</li><li>صور وتعريف بالمكان</li><li>المداخل ومعلومات الزيارة</li><li>وجهات قريبة منك</li></ul></div></div><div class="tourism-foot"><span>أمثلة للإثراء: قلعة أربيل، بابل، وآثار ومدن العراق. تُراجع إتاحة الزيارة لكل موقع قبل إدراجه.</span><a href="https://whc.unesco.org/en/statesparties/iq" target="_blank" rel="noopener">تراث العراق لدى اليونسكو ↗</a></div></section>
<section class="ideas" id="ideas" aria-labelledby="ideas-title"><div class="wrap"><div class="section-intro"><div><div class="eyebrow">آفاق سيرو ماب</div><h2 id="ideas-title">كل إضافة، قيمة جديدة.</h2></div><p>أفكار نطوّرها تدريجيًا؛ تخدم المستخدم، وتمنح شركاء سيرو ماب حضورًا يصل إلى الناس.</p></div><div class="ideas-grid"><article class="idea"><span class="idea-index">01 / الشركاء</span><svg viewBox="0 0 32 32" aria-hidden="true"><rect x="3" y="7" width="26" height="19" rx="3"/><path d="M3 13h26 M8 20h6 M21 18v6 M18 21h6"/></svg><h3>مزايا أقرب إليك</h3><p>دليل للمستشفيات والفنادق ومراكز الدورات المتعاقدة مع شبكة سيرو، يعرض موقع كل جهة ومزايا البطاقة وشروطها المعتمدة.</p><div class="benefit">الفائدة: يجد حامل البطاقة أين يستفيد منها.</div></article><article class="idea"><span class="idea-index">02 / الفعاليات</span><svg viewBox="0 0 32 32" aria-hidden="true"><rect x="5" y="6" width="22" height="23" rx="3"/><path d="M10 3v7 M22 3v7 M5 14h22 M10 20h4 M18 20h4 M10 24h4"/></svg><h3>من الدعوة إلى الوجهة</h3><p>خريطة للمؤتمرات والفعاليات الكبرى: موقع القاعة، المداخل، المواقف والوجهات المحيطة، تُشارك برابط أو رمز QR.</p><div class="benefit">الفائدة: كل مناسبة فرصة حقيقية لتجربة التطبيق.</div></article><article class="idea"><span class="idea-index">03 / جودة البيانات</span><svg viewBox="0 0 32 32" aria-hidden="true"><path d="M25 14c0 7-9 14-9 14S7 21 7 14a9 9 0 1 1 18 0Z"/><path d="m12 14 3 3 6-6"/></svg><h3>معًا، نصحّح الخريطة</h3><p>اقتراح مكان، تصحيح عنوان أو تحديد مدخل. مساهمات تُراجع، وبيانات استخدام اختيارية تساعد على تحسين التجربة.</p><div class="benefit">الفائدة: عناوين أدق، وأساس أفضل للنقل لاحقًا.</div></article></div><p class="ideas-note">هذه إضافات مقترحة للتطوير؛ ظهور الجهات والمزايا مرتبط بتأكيد بياناتها واتفاقاتها.</p></div></section>
<section class="wrap roadmap" id="journey" aria-labelledby="journey-title"><div class="eyebrow">رحلة تنمو بخطوات واضحة</div><h2 id="journey-title">نبدأ بما ينفع اليوم.<br>ونبني ما نحتاجه غدًا.</h2><div class="steps"><article class="step current"><div class="step-top"><span>الانطلاقة الأولى</span><b>01</b></div><h3>خرائط بين يدي الناس</h3><p>أربع دول، بلا تسجيل. تعريف بسيرو ماب، وتجارب فعلية تُحسّن المنتج وتبني حضور الاسم.</p></article><article class="step"><div class="step-top"><span>الإثراء التدريجي</span><b>02</b></div><h3>دليل يستحق العودة</h3><p>معالم العراق أولًا، ثم الشركاء والفعاليات؛ أسباب متجددة لفتح التطبيق والاستفادة منه.</p></article><article class="step"><div class="step-top"><span>الأفق القادم</span><b>03</b></div><h3>نحو سيرو للنقل الذكي</h3><p>ربط اكتشاف الوجهة بالوصول إليها عند إطلاق خدمة النقل، والتوسع إلى دول عربية أخرى تدريجيًا.</p></article></div></section>
<section class="closing"><span class="latin">SIRO MAP · THE FIRST JOURNEY</span><h2>من هنا تبدأ حكايتنا على الخريطة.</h2><p>العراق · الأردن · سوريا · مصر</p></section>
</main><footer class="wrap footer"><span>سيرو ماب © 2026 <span aria-hidden="true"> — </span> الانطلاقة الأولى ورؤية التطوير</span><a href="#top">العودة إلى البداية ↑</a></footer>
<script>
const countries={iq:{title:'العراق، هنا تبدأ الحكاية.',description:'الخرائط أولًا، ومنها نبدأ بناء الدليل السياحي.',note:'نبدأ بإثراء المعالم السياحية والأثرية العراقية: قصة المكان، موقعه، ومعلومات تساعد على زيارته.'},jo:{title:'الأردن، ضمن أولى وجهاتنا.',description:'استخدام الخرائط دون الحاجة إلى إنشاء حساب.',note:'الأردن ضمن نطاق الخرائط في الانطلاقة الأولى. يبدأ إثراء المحتوى السياحي من العراق، ثم يتوسع تدريجيًا.'},sy:{title:'سوريا، أقرب على الخريطة.',description:'تجربة خرائط سهلة ضمن الدول الأربع الأولى.',note:'نطوّر جودة البيانات بالاختبار والمراجعة ومساهمات المستخدمين؛ لتصبح معلومات الأماكن أكثر دقة وفائدة.'},eg:{title:'مصر، امتداد لانطلاقتنا.',description:'خرائط بلا تسجيل ضمن تجربة سيرو ماب.',note:'مصر ضمن نطاق الانطلاقة. نبدأ بخدمة الخرائط، ونضيف الدليل والوجهات والشراكات على مراحل واضحة.'}};
const tabs=[...document.querySelectorAll('[role=tab]')];function choose(tab){tabs.forEach(t=>{const selected=t===tab;t.setAttribute('aria-selected',String(selected));t.tabIndex=selected?0:-1});const item=countries[tab.dataset.country];document.getElementById('country-title').textContent=item.title;document.getElementById('country-description').textContent=item.description;document.getElementById('country-note').textContent=item.note;document.getElementById('country-panel').setAttribute('aria-labelledby',tab.id)}tabs.forEach((tab,index)=>{tab.addEventListener('click',()=>choose(tab));tab.addEventListener('keydown',event=>{let next;if(event.key==='ArrowLeft')next=(index+1)%tabs.length;if(event.key==='ArrowRight')next=(index-1+tabs.length)%tabs.length;if(event.key==='Home')next=0;if(event.key==='End')next=tabs.length-1;if(next!==undefined){event.preventDefault();choose(tabs[next]);tabs[next].focus()}})});
</script>
</body></html>
+161
View File
@@ -0,0 +1,161 @@
{
"info": {
"name": "Siro Maps - Sovereign Heritage & Community Guides API",
"description": "Production Endpoints for National Heritage Landmarks, Precision Gate Navigation, and Moderated Local Guides Gamification.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"variable": [
{
"key": "baseUrl",
"value": "https://map-saas.intaleqapp.com/api",
"type": "string"
},
{
"key": "apiKey",
"value": "zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX",
"type": "string"
}
],
"item": [
{
"name": "1. Heritage - List All Landmarks",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/v1/heritage/landmarks",
"host": ["{{baseUrl}}"],
"path": ["v1", "heritage", "landmarks"]
}
}
},
{
"name": "2. Heritage - Nearby Landmarks (Spatial Query)",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/v1/heritage/landmarks?lat=31.9539&lng=35.9350&radius=10000",
"host": ["{{baseUrl}}"],
"path": ["v1", "heritage", "landmarks"],
"query": [
{ "key": "lat", "value": "31.9539" },
{ "key": "lng", "value": "35.9350" },
{ "key": "radius", "value": "10000" }
]
}
}
},
{
"name": "3. Heritage - Single Landmark by Slug",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/v1/heritage/landmarks/amman-citadel",
"host": ["{{baseUrl}}"],
"path": ["v1", "heritage", "landmarks", "amman-citadel"]
}
}
},
{
"name": "4. Heritage - Offline Pack for Mobile",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/v1/heritage/offline-pack",
"host": ["{{baseUrl}}"],
"path": ["v1", "heritage", "offline-pack"]
}
}
},
{
"name": "5. Community - List Badges",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/v1/community/badges",
"host": ["{{baseUrl}}"],
"path": ["v1", "community", "badges"]
}
}
},
{
"name": "6. Community - Submit Contribution (Suggest Gate)",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" }
],
"body": {
"mode": "raw",
"raw": "{\n \"userId\": \"guide_hamza_001\",\n \"displayName\": \"حمزة عايد\",\n \"contributionType\": \"CONFIRM_GATE\",\n \"targetTable\": \"heritage.landmarks\",\n \"targetId\": 3,\n \"placeName\": \"جرش - البوابة الجنوبية السياحية\",\n \"lat\": 32.2735,\n \"lng\": 35.8928,\n \"suggestedData\": { \"gate_name\": \"South Main Gate\" },\n \"notes\": \"تم التحقق من موقع بوابة التذاكر الميدانية\"\n}"
},
"url": {
"raw": "{{baseUrl}}/v1/community/contribute",
"host": ["{{baseUrl}}"],
"path": ["v1", "community", "contribute"]
}
}
},
{
"name": "7. Community - Guide Profile & Points",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/v1/community/profile/guide_hamza_001",
"host": ["{{baseUrl}}"],
"path": ["v1", "community", "profile", "guide_hamza_001"]
}
}
},
{
"name": "8. Community - Leaderboard",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/v1/community/leaderboard",
"host": ["{{baseUrl}}"],
"path": ["v1", "community", "leaderboard"]
}
}
},
{
"name": "9. Admin - Moderation Pending Queue",
"request": {
"method": "GET",
"header": [
{ "key": "x-api-key", "value": "{{apiKey}}" }
],
"url": {
"raw": "{{baseUrl}}/v1/admin/moderation/pending",
"host": ["{{baseUrl}}"],
"path": ["v1", "admin", "moderation", "pending"]
}
}
},
{
"name": "10. Admin - Review Contribution (Approve)",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "x-api-key", "value": "{{apiKey}}" }
],
"body": {
"mode": "raw",
"raw": "{\n \"contributionId\": 2,\n \"action\": \"APPROVE\",\n \"reviewerName\": \"Hamza Ayed (Admin)\",\n \"reviewerNotes\": \"معتمدة ومطابقة لصور الأقمار الصناعية\"\n}"
},
"url": {
"raw": "{{baseUrl}}/v1/admin/moderation/review",
"host": ["{{baseUrl}}"],
"path": ["v1", "admin", "moderation", "review"]
}
}
}
]
}
+82
View File
@@ -0,0 +1,82 @@
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#061d42">
<meta name="description" content="سيرو ماب: خرائط سهلة بلا تسجيل للعراق والأردن وسوريا ومصر، ورؤية لدليل سياحي يبدأ من معالم العراق.">
<title>سيرو ماب — كل طريق، بداية حكاية</title>
<link rel="icon" type="image/png" href="assets/siro-maps.png">
<style>
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(assets/tajawal-400.ttf) format('truetype');
}
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(assets/tajawal-500.ttf) format('truetype');
}
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(assets/tajawal-700.ttf) format('truetype');
}
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(assets/tajawal-800.ttf) format('truetype');
}
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(assets/tajawal-900.ttf) format('truetype');
}
:root{--navy:#061d42;--blue:#124f9f;--gold:#b59247;--pale:#edf3f9;--ink:#132844;--muted:#5c6c80;--line:#dce3ec;--white:#fff;--font:'Tajawal','Geeza Pro',Tahoma,sans-serif}
*{box-sizing:border-box}html{scroll-behavior:smooth;scroll-padding-top:100px}body{margin:0;background:#fff;color:var(--ink);font-family:var(--font);font-size:18px;line-height:1.85}a{color:inherit;text-decoration:none}button{font:inherit}button,a{-webkit-tap-highlight-color:transparent}button:focus-visible,a:focus-visible{outline:3px solid #c6a057;outline-offset:6px}button{cursor:pointer}img{max-width:100%;height:auto;display:block}h1,h2,h3,p{margin:0}h1,h2,h3{line-height:1.35}h2{font-size:clamp(32px,3.5vw,49px);font-weight:800;letter-spacing:-.6px}h3{font-size:25px}::selection{background:#d6bb7b;color:var(--navy)}.wrap{width:min(1190px,calc(100% - 88px));margin:auto}.eyebrow{font-size:14px;letter-spacing:1.8px;font-weight:700;color:var(--gold);display:flex;align-items:center;gap:12px;margin-bottom:22px}.eyebrow:before{content:'';width:28px;height:2px;background:currentColor}.latin{font-family:Arial,sans-serif;letter-spacing:2.5px;font-size:12px;direction:ltr;display:inline-block}.muted{color:var(--muted)}
header{height:100px;background:rgba(255,255,255,.97);border-bottom:1px solid var(--line);position:relative;z-index:5}.nav{height:100%;display:flex;align-items:center;justify-content:space-between;gap:24px}.brand{display:flex;gap:13px;align-items:center}.brand img{width:62px;height:62px;border-radius:15px}.brand strong{font-size:23px;display:block;line-height:1.3}.brand small{color:var(--muted);font-size:11px;letter-spacing:2.2px;display:block;direction:ltr}.links{display:flex;align-items:center;gap:30px;font-size:16px;font-weight:600}.links a{transition:color .2s}.links a:hover{color:var(--blue)}.nav-tag{font-size:14px;color:var(--blue);border:1px solid #c9d8ed;padding:7px 17px;border-radius:30px;white-space:nowrap}
.hero{position:relative;overflow:hidden;background:var(--navy);color:white;padding:75px 0 0}.hero:before{content:'';position:absolute;width:740px;height:740px;left:-140px;top:-220px;background:radial-gradient(circle,rgba(39,105,180,.38),transparent 67%);pointer-events:none}.hero-grid{display:grid;grid-template-columns:1.12fr 1fr;align-items:center;gap:65px;position:relative;min-height:470px;padding-bottom:65px}.hero h1{font-size:clamp(44px,5.7vw,76px);font-weight:800;line-height:1.24;letter-spacing:-1px}.hero h1 span{color:#dbbc77}.hero p{font-size:20px;color:#c0ccdc;max-width:530px;margin-top:27px;line-height:1.9}.hero .eyebrow{color:#ddbd78}.hero-actions{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-top:33px}.btn{display:inline-flex;align-items:center;justify-content:center;gap:20px;padding:11px 23px;border-radius:7px;font-size:17px;font-weight:700;border:1px solid transparent;min-height:50px}.btn-gold{background:#dbbc77;color:#0b2547}.btn-gold:hover{background:#efd49b}.btn-outline{border-color:#526580;color:#e3eaf3}.btn-outline:hover{background:#ffffff0c}.arrow{font-family:Arial;font-size:23px;font-weight:400}.hero-status{display:block;font-size:14px;color:#acbbcf;margin-top:17px}.brand-stage{position:relative;display:flex;justify-content:center;align-items:center;min-height:420px;padding:15px}.brand-stage:before,.brand-stage:after{content:'';position:absolute;border:1px solid #759ed12b;border-radius:50%;width:410px;height:410px;pointer-events:none}.brand-stage:after{width:480px;height:480px;border-style:dashed;opacity:.6}.hero-icon{width:330px;border-radius:70px;transform:rotate(-6deg);box-shadow:0 30px 60px #0004;z-index:1}.float-label{position:absolute;z-index:2;border:1px solid #6986af6b;background:#0b2b55eb;box-shadow:0 10px 35px #0002;backdrop-filter:blur(15px);padding:10px 17px;border-radius:10px;font-size:16px;display:flex;align-items:center;gap:12px}.float-label svg{width:22px;height:22px;stroke:#e0bf7d;fill:none;stroke-width:1.6}.label-one{left:-5px;top:60px}.label-two{right:4px;bottom:43px}.hero-facts{position:relative;border-top:1px solid #ffffff20;display:grid;grid-template-columns:repeat(3,1fr);padding:24px 0;gap:30px}.fact{display:flex;gap:17px;align-items:center}.fact:not(:last-child){border-left:1px solid #ffffff20}.fact b{font-size:35px;color:#dfc28a;font-weight:500;line-height:1}.fact span{display:block;font-size:17px}.fact small{display:block;color:#a8b8cd;font-size:14px}
.story{padding:88px 0;display:grid;grid-template-columns:.9fr 1.1fr;gap:95px;align-items:center}.story p{color:var(--muted);font-size:20px}.story .accent{color:var(--ink);font-weight:700}.story .eyebrow{margin-bottom:16px}.section-intro{display:flex;align-items:flex-end;justify-content:space-between;gap:40px;margin-bottom:37px}.section-intro p{max-width:430px;color:var(--muted)}
.coverage{background:var(--pale);padding:72px 0 78px}.country-tabs{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:24px}.country-tab{display:flex;align-items:center;justify-content:space-between;padding:17px 22px;background:transparent;border:1px solid #cdd8e6;color:var(--ink);border-radius:8px;transition:background .2s;min-height:83px}.country-tab strong{font-size:23px}.country-tab small{display:block;font-size:12px;letter-spacing:2px;line-height:1.2;color:var(--muted);direction:ltr}.country-tab .number{font-family:Arial;font-size:14px;color:#8b9db3}.country-tab[aria-selected=true]{background:var(--navy);border-color:var(--navy);color:white}.country-tab[aria-selected=true] small,.country-tab[aria-selected=true] .number{color:#d8bd81}.country-panel{background:#fff;border-radius:12px;padding:27px 32px;display:grid;grid-template-columns:1fr 1.4fr;gap:35px;align-items:center;border:1px solid #e2e9f2}.country-panel h3{font-size:28px;margin-bottom:5px}.country-panel p{font-size:17px;color:var(--muted)}.panel-note{border-right:2px solid #c4a266;padding-right:24px}.pill{display:inline-block;font-size:13px;border:1px solid #d9e1ec;background:#f7f9fc;padding:2px 10px;border-radius:30px;color:#49617f;margin-bottom:9px}
.tourism{padding:90px 0}.tourism-feature{display:grid;grid-template-columns:1.15fr 1fr;background:var(--navy);color:white;border-radius:18px;overflow:hidden;min-height:415px}.tourism-photo{position:relative;background:#15395e;min-height:380px;overflow:hidden}.tourism-photo img{width:100%;height:100%;object-fit:cover;position:absolute}.photo-caption{position:absolute;bottom:0;right:0;left:0;color:white;padding:45px 28px 22px;background:linear-gradient(transparent,#041a35e6);font-size:16px}.photo-caption small{display:block;opacity:.8;font-size:12px;direction:ltr;text-align:right}.tourism-copy{padding:42px 40px;align-self:center}.tourism-copy h3{font-size:35px;margin:12px 0 15px}.tourism-copy p{color:#beccdd}.tourism-copy .pill{background:#ffffff0c;border-color:#597087;color:#e2c388}.tourism-list{padding:0;list-style:none;display:grid;grid-template-columns:1fr 1fr;gap:13px;margin:25px 0 0;font-size:16px;color:#e7edf6}.tourism-list li:before{content:'+';color:#d8b975;padding-left:10px}.tourism-foot{display:flex;justify-content:space-between;gap:25px;margin-top:20px;font-size:15px;color:var(--muted)}.tourism-foot a{color:var(--blue);text-decoration:underline;text-underline-offset:4px}
.pillars-section{padding:80px 0}.pillars-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:24px;margin-top:35px}.pillar-card{background:var(--pale);border:1px solid #dce5f0;border-radius:16px;padding:32px;display:flex;flex-direction:column;justify-content:space-between;transition:transform .2s,box-shadow .2s}.pillar-card:hover{transform:translateY(-4px);box-shadow:0 12px 30px rgba(6,29,66,.08);border-color:#b59247}.pillar-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:18px}.pillar-num{font-size:32px;font-weight:900;color:var(--gold);line-height:1}.pillar-tag{font-size:13px;font-weight:700;color:var(--blue);background:rgba(18,79,159,.08);padding:4px 12px;border-radius:20px}.pillar-card h3{font-size:24px;margin-bottom:12px;color:var(--navy)}.pillar-card p{color:var(--muted);font-size:16px;line-height:1.7;margin-bottom:20px}.pillar-badge{font-size:13px;font-weight:600;color:var(--gold);border-top:1px solid #dce5f0;padding-top:14px}
.ideas{background:#f7f9fc;padding:75px 0 82px}.ideas-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:0;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}.idea{padding:32px 29px 30px}.idea:not(:last-child){border-left:1px solid var(--line)}.idea-index{font-size:13px;direction:ltr;display:block;text-align:right;color:#977837;letter-spacing:2px;margin-bottom:22px}.idea svg{width:34px;height:34px;fill:none;stroke:var(--blue);stroke-width:1.5;margin-bottom:15px}.idea h3{margin-bottom:13px;font-size:25px}.idea p{font-size:17px;color:var(--muted)}.benefit{margin-top:24px;padding-top:17px;border-top:1px solid var(--line);font-size:15px;color:var(--blue)}.ideas-note{font-size:14px;color:var(--muted);margin-top:20px}
.roadmap{padding:90px 0}.steps{display:grid;grid-template-columns:repeat(3,1fr);gap:32px;margin-top:42px}.step{border-top:2px solid #dce3ec;padding-top:22px}.step.current{border-top-color:var(--gold)}.step-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}.step-top span{font-size:14px;color:#728096}.step-top b{font-size:28px;font-family:Arial;font-weight:400;color:#a7b4c4}.step.current .step-top span{color:#907132}.step h3{font-size:25px;margin-bottom:12px}.step p{color:var(--muted);font-size:17px}
.partnership{border:1px solid var(--line);border-radius:15px;display:grid;grid-template-columns:160px 1fr;gap:40px;padding:32px 40px;align-items:center;margin-bottom:70px}.prize-logo{width:150px}.partnership .eyebrow{font-size:13px;margin-bottom:11px}.partnership h3{font-size:29px;margin-bottom:9px}.partnership p{font-size:18px;color:var(--muted);max-width:820px}.closing{background:var(--navy);color:white;text-align:center;padding:65px 24px}.closing .latin{color:#dbbc77;margin-bottom:15px}.closing h2{font-size:clamp(29px,4vw,46px)}.closing p{color:#acbed4;margin-top:15px;font-size:18px}.footer{padding:23px 0;display:flex;justify-content:space-between;align-items:center;gap:20px;font-size:14px;color:var(--muted)}.footer a{color:var(--blue)}.footer .latin{font-size:11px;letter-spacing:1px}
@media(min-width:1500px){.hero-grid{min-height:530px}}
@media(max-width:1050px){.wrap{width:calc(100% - 48px)}.links{gap:18px}.nav-tag{display:none}.hero-grid{gap:25px}.hero-icon{width:280px;border-radius:58px}.brand-stage:before{width:340px;height:340px}.brand-stage:after{width:390px;height:390px}.label-one{left:0;top:45px}.label-two{right:0;bottom:45px}.story{gap:45px}.tourism-copy{padding:30px}.idea{padding:28px 20px}.country-tab{padding:15px}.hero h1{font-size:58px}}
@media(max-width:760px){body{font-size:17px}.wrap{width:calc(100% - 36px)}header{height:auto;min-height:86px}.nav{padding:13px 0;flex-wrap:wrap;gap:10px}.brand img{width:48px;height:48px}.brand strong{font-size:20px}.brand small{font-size:10px}.links{gap:19px;font-size:14px}.links a:last-child{display:none}.hero{padding-top:44px}.hero-grid{grid-template-columns:1fr;gap:23px;padding-bottom:35px}.hero h1{font-size:clamp(44px,10vw,62px);line-height:1.3}.hero p{font-size:18px;margin-top:20px}.hero-actions{margin-top:25px}.btn{font-size:16px;padding:10px 19px}.hero-status{font-size:13px}.brand-stage{min-height:325px;width:min(100%,390px);margin:auto}.hero-icon{width:240px;border-radius:52px}.brand-stage:before{width:285px;height:285px}.brand-stage:after{width:320px;height:320px}.float-label{font-size:14px;padding:8px 12px}.label-one{top:39px;left:0}.label-two{bottom:23px;right:0}.hero-facts{gap:15px;padding:20px 0}.fact{gap:8px;display:block}.fact b{font-size:27px}.fact span{font-size:14px}.fact small{font-size:12px}.story{grid-template-columns:1fr;gap:22px;padding:53px 0}.story p{font-size:18px}.eyebrow{font-size:13px;margin-bottom:15px}.section-intro{display:block;margin-bottom:27px}.section-intro p{margin-top:17px;font-size:17px}.coverage{padding:48px 0}.country-tabs{grid-template-columns:repeat(2,1fr);gap:9px}.country-tab{min-height:78px}.country-tab strong{font-size:22px}.country-panel{grid-template-columns:1fr;gap:18px;padding:24px}.panel-note{padding-right:16px}.tourism{padding:55px 0}.tourism-feature{grid-template-columns:1fr}.tourism-photo{min-height:280px}.tourism-copy{padding:29px 25px}.tourism-copy h3{font-size:30px}.tourism-foot{display:block;font-size:14px}.tourism-foot a{display:inline-block;margin-top:7px}.ideas{padding:50px 0}.ideas-grid{grid-template-columns:1fr}.idea{padding:28px 4px}.idea:not(:last-child){border-left:0;border-bottom:1px solid var(--line)}.idea-index{margin-bottom:14px}.idea svg{float:left}.benefit{margin-top:17px}.roadmap{padding:53px 0}.steps{grid-template-columns:1fr;gap:28px;margin-top:25px}.step{padding-top:14px}.step-top{margin-bottom:8px}.partnership{grid-template-columns:1fr;gap:18px;padding:26px;margin-bottom:45px}.prize-logo{width:120px}.partnership h3{font-size:25px}.partnership p{font-size:17px}.closing{padding:45px 22px}.footer{flex-direction:column;align-items:flex-start;gap:8px}.tourism-list{font-size:15px}}
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*{transition:none!important}}
@media print{header,.hero-actions,.hero-status,.country-tabs,.footer a{display:none}body{font-size:12pt}.hero,.closing,.tourism-feature{print-color-adjust:exact;-webkit-print-color-adjust:exact}.wrap{width:92%}.hero{padding-top:30px}.hero-grid{min-height:0;gap:25px}.hero h1{font-size:38px}.hero p{font-size:16px}.brand-stage{min-height:250px}.hero-icon{width:200px}.brand-stage:before,.brand-stage:after,.float-label{display:none}.story,.tourism,.ideas,.roadmap,.coverage{padding:25px 0}.section-intro h2,h2{font-size:28px}.story p{font-size:16px}.tourism-feature{break-inside:avoid}.idea,.step,.partnership{break-inside:avoid}.partnership{margin-bottom:25px}.closing{padding:25px}}
</style>
</head>
<body id="top">
<header><nav class="wrap nav" aria-label="التنقل الرئيسي"><a class="brand" href="#top" aria-label="سيرو ماب، بداية الصفحة"><img src="assets/siro-maps.png" alt="شعار سيرو ماب" width="62" height="62"><span><strong>سيرو ماب</strong><small>SIRO MAP</small></span></a><div class="links"><a href="#coverage">الانطلاقة</a><a href="#tourism">اكتشف العراق</a><a href="#pillars">ركائز الدليل</a><a href="#ideas">آفاق سيرو ماب</a><a href="#journey">رحلتنا</a></div><span class="nav-tag">من سيرو ماب، إلى كل وجهة</span></nav></header>
<main>
<section class="hero" aria-labelledby="hero-title"><div class="wrap"><div class="hero-grid"><div><div class="eyebrow">الانطلاقة الأولى · سيرو ماب</div><h1 id="hero-title">كل طريق،<br>بداية <span>حكاية.</span></h1><p>خرائط تقرّب المسافات، وحكايات تقرّبنا من المكان.<br>نبدأ بأربع دول، ونفتح من العراق بابًا لاكتشاف الثقافة والمعالم والوجهات.</p><div class="hero-actions"><a class="btn btn-gold" href="#coverage">اكتشف الانطلاقة <span class="arrow" aria-hidden="true">←</span></a><a class="btn btn-outline" href="#ideas">ما الذي نبنيه معًا؟</a></div><small class="hero-status">نسخة التطبيق قيد المراجعة على Google Play</small></div><div class="brand-stage"><img class="hero-icon" src="assets/siro-maps.png" alt="شعار سيرو ماب الذكي للملاحة والتنقل" width="512" height="512"><span class="float-label label-one"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 10c0 6-8 11-8 11S4 16 4 10a8 8 0 1 1 16 0Z"/><circle cx="12" cy="10" r="2.5"/></svg>وجهتك أقرب</span><span class="float-label label-two"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m3 5 6-2 6 2 6-2v16l-6 2-6-2-6 2Z M9 3v16 M15 5v16"/></svg>اكتشف حكاية المكان</span></div></div><div class="hero-facts"><div class="fact"><b>04</b><div><span>دول في الانطلاقة</span><small>العراق · الأردن · سوريا · مصر</small></div></div><div class="fact"><b>01</b><div><span>بداية للدليل السياحي</span><small>معالم العراق أولًا</small></div></div><div class="fact"><b>ببساطة</b><div><span>خرائط بلا تسجيل</span><small>افتح التطبيق، وابدأ رحلتك</small></div></div></div></div></section>
<section class="wrap story" aria-labelledby="story-title"><div><div class="eyebrow">فكرة تجمعنا</div><h2 id="story-title">من اسم يحمل حضارة،<br>إلى خدمة ترافق الناس.</h2></div><p>تعتمد سيرو ماب على الابتكار والسيادة الرقمية وتوفير التكاليف. واليوم، نمنح هذا المشروع حضورًا قويًا في الحياة اليومية: خرائط سهلة ومستقلة، ودليل يكشف ما حولنا من وجهات ومعالم.<br><span class="accent">خطوة أولى لشراكتنا، وأساس ننمو عليه معًا.</span></p></section>
<section class="coverage" id="coverage" aria-labelledby="coverage-title"><div class="wrap"><div class="section-intro"><div><div class="eyebrow">أربع دول · تجربة واحدة</div><h2 id="coverage-title">بداية قريبة منّا.</h2></div><p>خرائط للاستخدام دون إنشاء حساب. ننطلق بهذا النطاق، ونواصل تحسين البيانات والتجربة قبل التوسع إلى مزيد من الدول العربية.</p></div><div class="country-tabs" role="tablist" aria-label="دول الانطلاقة"><button id="tab-iq" class="country-tab" role="tab" aria-selected="true" aria-controls="country-panel" data-country="iq"><span><strong>العراق</strong><small>IRAQ</small></span><span class="number">01</span></button><button id="tab-jo" class="country-tab" role="tab" aria-selected="false" aria-controls="country-panel" tabindex="-1" data-country="jo"><span><strong>الأردن</strong><small>JORDAN</small></span><span class="number">02</span></button><button id="tab-sy" class="country-tab" role="tab" aria-selected="false" aria-controls="country-panel" tabindex="-1" data-country="sy"><span><strong>سوريا</strong><small>SYRIA</small></span><span class="number">03</span></button><button id="tab-eg" class="country-tab" role="tab" aria-selected="false" aria-controls="country-panel" tabindex="-1" data-country="eg"><span><strong>مصر</strong><small>EGYPT</small></span><span class="number">04</span></button></div><div id="country-panel" class="country-panel" role="tabpanel" tabindex="0" aria-labelledby="tab-iq"><div><span class="pill">ضمن نطاق الانطلاقة</span><h3 id="country-title">العراق، هنا تبدأ الحكاية.</h3><p id="country-description">الخرائط أولًا، ومنها نبدأ بناء الدليل السياحي.</p></div><p id="country-note" class="panel-note">نبدأ بإثراء المعالم السياحية والأثرية العراقية: قصة المكان، موقعه، ومعلومات تساعد على زيارته.</p></div></div></section>
<section class="wrap tourism" id="tourism" aria-labelledby="tourism-title"><div class="section-intro"><div><div class="eyebrow">الخطوة التالية · الدليل السياحي</div><h2 id="tourism-title">للمكان أكثر من عنوان.</h2></div><p>من معالم العراق نبدأ. نريد أن تتحول النقطة على الخريطة إلى حكاية تعرفها، ووجهة تستطيع التخطيط لزيارتها.</p></div><div class="tourism-feature"><div class="tourism-photo" id="tourism-photo"><img src="assets/erbil-citadel.jpg" alt="قلعة أربيل التاريخية والأسواق المحيطة بها" width="4032" height="3024" loading="lazy"><div class="photo-caption">قلعة أربيل · العراق<small>الصورة: <a href="https://commons.wikimedia.org/wiki/File:Citadel_of_Erbil.jpg" target="_blank" rel="noopener">Sarbast.T.Hameed / Wikimedia Commons</a> · <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener">CC BY-SA 4.0</a> · عرض مقتصّ</small></div></div><div class="tourism-copy"><span class="pill">تصور قادم داخل التطبيق</span><h3>اكتشف ما حولك،<br>واعرف ما وراءه.</h3><p>دليل قريب منك للمعالم والمواقع الأثرية، بمعلومات موثقة تُراجع وتُحدّث مع الشركاء المحليين.</p><ul class="tourism-list"><li>قصة موجزة لكل معلم</li><li>صور وتعريف بالمكان</li><li>المداخل ومعلومات الزيارة</li><li>وجهات قريبة منك</li></ul></div></div><div class="tourism-foot"><span>أمثلة للإثراء: قلعة أربيل، بابل، وآثار ومدن العراق. تُراجع إتاحة الزيارة لكل موقع قبل إدراجه.</span><a href="https://whc.unesco.org/en/statesparties/iq" target="_blank" rel="noopener">تراث العراق لدى اليونسكو ↗</a></div></section>
<section class="wrap pillars-section" id="pillars" aria-labelledby="pillars-title"><div class="section-intro"><div><div class="eyebrow">منهجية العمل · الركائز الأربع للدليل</div><h2 id="pillars-title">كيف نُعيد صياغة تجربة المعالم؟</h2></div><p>لا نكتفي بوضع نقطة صامتة على الخارطة؛ بل نمزج دقة الملاحة الميدانية بسردية التاريخ والسيادة التقنية.</p></div><div class="pillars-grid"><div class="pillar-card"><div class="pillar-header"><span class="pillar-num">01</span><span class="pillar-tag">الملاحة الواقعية</span></div><h3>التوجيه للبوابات الفعلية</h3><p>توجيه السائقين والزوار مباشرة إلى نقاط الدخول الرسمية ومواقف السيارات بدلاً من التوجيه الخاطئ إلى منتصف المواقع الشاسعة أو الأسوار المغلقة.</p><div class="pillar-badge">دقة الوصول الميداني</div></div><div class="pillar-card"><div class="pillar-header"><span class="pillar-num">02</span><span class="pillar-tag">التوثيق الحضاري</span></div><h3>سردية المكان في 60 ثانية</h3><p>بطاقة ذكية ومكثفة لكل معلم: الحقبة التاريخية، من بنى المكان، وأهميته التراثية العالمية، بصور موثقة بدعم المبادرات الثقافية الوطنية.</p><div class="pillar-badge">معرفة سريعة وموثوقة</div></div><div class="pillar-card"><div class="pillar-header"><span class="pillar-num">03</span><span class="pillar-tag">السيادة التقنية</span></div><h3>استكشاف كامل بدون إنترنت</h3><p>حزم محلية خفيفة تحفظ قصص المعالم ومساراتها على الهاتف، لتعمل بكفاءة وسرعة فائقة حتى في المناطق الصحراوية والنائية.</p><div class="pillar-badge">استمرارية في الميدان</div></div><div class="pillar-card"><div class="pillar-header"><span class="pillar-num">04</span><span class="pillar-tag">منظومة النقل</span></div><h3>جسر مباشر بين الاستكشاف والوصول</h3><p>ربط قرار الزيارة فوراً بطلب وسيلة مواصلات ذكية أو بدء مسار الملاحة، لتحويل الاكتشاف الثقافي إلى حركة يومية واقتصاد سياحي نابض.</p><div class="pillar-badge">تكامل ذكي مع الحركة</div></div></div></section>
<section class="ideas" id="ideas" aria-labelledby="ideas-title"><div class="wrap"><div class="section-intro"><div><div class="eyebrow">آفاق سيرو ماب</div><h2 id="ideas-title">كل إضافة، قيمة جديدة.</h2></div><p>أفكار نطوّرها تدريجيًا؛ تخدم المستخدم، وتمنح شركاء سيرو ماب حضورًا يصل إلى الناس.</p></div><div class="ideas-grid"><article class="idea"><span class="idea-index">01 / الشركاء</span><svg viewBox="0 0 32 32" aria-hidden="true"><rect x="3" y="7" width="26" height="19" rx="3"/><path d="M3 13h26 M8 20h6 M21 18v6 M18 21h6"/></svg><h3>مزايا أقرب إليك</h3><p>دليل للمستشفيات والفنادق ومراكز الدورات المتعاقدة مع شبكة سيرو، يعرض موقع كل جهة ومزايا البطاقة وشروطها المعتمدة.</p><div class="benefit">الفائدة: يجد حامل البطاقة أين يستفيد منها.</div></article><article class="idea"><span class="idea-index">02 / الفعاليات</span><svg viewBox="0 0 32 32" aria-hidden="true"><rect x="5" y="6" width="22" height="23" rx="3"/><path d="M10 3v7 M22 3v7 M5 14h22 M10 20h4 M18 20h4 M10 24h4"/></svg><h3>من الدعوة إلى الوجهة</h3><p>خريطة للمؤتمرات والفعاليات الكبرى: موقع القاعة، المداخل، المواقف والوجهات المحيطة، تُشارك برابط أو رمز QR.</p><div class="benefit">الفائدة: كل مناسبة فرصة حقيقية لتجربة التطبيق.</div></article><article class="idea"><span class="idea-index">03 / جودة البيانات</span><svg viewBox="0 0 32 32" aria-hidden="true"><path d="M25 14c0 7-9 14-9 14S7 21 7 14a9 9 0 1 1 18 0Z"/><path d="m12 14 3 3 6-6"/></svg><h3>معًا، نصحّح الخريطة</h3><p>اقتراح مكان، تصحيح عنوان أو تحديد مدخل. مساهمات تُراجع، وبيانات استخدام اختيارية تساعد على تحسين التجربة.</p><div class="benefit">الفائدة: عناوين أدق، وأساس أفضل للنقل لاحقًا.</div></article></div><p class="ideas-note">هذه إضافات مقترحة للتطوير؛ ظهور الجهات والمزايا مرتبط بتأكيد بياناتها واتفاقاتها.</p></div></section>
<section class="wrap roadmap" id="journey" aria-labelledby="journey-title"><div class="eyebrow">رحلة تنمو بخطوات واضحة</div><h2 id="journey-title">نبدأ بما ينفع اليوم.<br>ونبني ما نحتاجه غدًا.</h2><div class="steps"><article class="step current"><div class="step-top"><span>الانطلاقة الأولى</span><b>01</b></div><h3>خرائط بين يدي الناس</h3><p>أربع دول، بلا تسجيل. تعريف بسيرو ماب، وتجارب فعلية تُحسّن المنتج وتبني حضور الاسم.</p></article><article class="step"><div class="step-top"><span>الإثراء التدريجي</span><b>02</b></div><h3>دليل يستحق العودة</h3><p>معالم العراق أولًا، ثم الشركاء والفعاليات؛ أسباب متجددة لفتح التطبيق والاستفادة منه.</p></article><article class="step"><div class="step-top"><span>الأفق القادم</span><b>03</b></div><h3>نحو سيرو للنقل الذكي</h3><p>ربط اكتشاف الوجهة بالوصول إليها عند إطلاق خدمة النقل، والتوسع إلى دول عربية أخرى تدريجيًا.</p></article></div></section>
<section class="closing"><span class="latin">SIRO MAP · THE FIRST JOURNEY</span><h2>من هنا تبدأ حكايتنا على الخريطة.</h2><p>العراق · الأردن · سوريا · مصر</p></section>
</main><footer class="wrap footer"><span>سيرو ماب © 2026 <span aria-hidden="true"> — </span> الانطلاقة الأولى ورؤية التطوير</span><a href="#top">العودة إلى البداية ↑</a></footer>
<script>
const countries={iq:{title:'العراق، هنا تبدأ الحكاية.',description:'الخرائط أولًا، ومنها نبدأ بناء الدليل السياحي.',note:'نبدأ بإثراء المعالم السياحية والأثرية العراقية: قصة المكان، موقعه، ومعلومات تساعد على زيارته.'},jo:{title:'الأردن، ضمن أولى وجهاتنا.',description:'استخدام الخرائط دون الحاجة إلى إنشاء حساب.',note:'الأردن ضمن نطاق الخرائط في الانطلاقة الأولى. يبدأ إثراء المحتوى السياحي من العراق، ثم يتوسع تدريجيًا.'},sy:{title:'سوريا، أقرب على الخريطة.',description:'تجربة خرائط سهلة ضمن الدول الأربع الأولى.',note:'نطوّر جودة البيانات بالاختبار والمراجعة ومساهمات المستخدمين؛ لتصبح معلومات الأماكن أكثر دقة وفائدة.'},eg:{title:'مصر، امتداد لانطلاقتنا.',description:'خرائط بلا تسجيل ضمن تجربة سيرو ماب.',note:'مصر ضمن نطاق الانطلاقة. نبدأ بخدمة الخرائط، ونضيف الدليل والوجهات والشراكات على مراحل واضحة.'}};
const tabs=[...document.querySelectorAll('[role=tab]')];function choose(tab){tabs.forEach(t=>{const selected=t===tab;t.setAttribute('aria-selected',String(selected));t.tabIndex=selected?0:-1});const item=countries[tab.dataset.country];document.getElementById('country-title').textContent=item.title;document.getElementById('country-description').textContent=item.description;document.getElementById('country-note').textContent=item.note;document.getElementById('country-panel').setAttribute('aria-labelledby',tab.id)}tabs.forEach((tab,index)=>{tab.addEventListener('click',()=>choose(tab));tab.addEventListener('keydown',event=>{let next;if(event.key==='ArrowLeft')next=(index+1)%tabs.length;if(event.key==='ArrowRight')next=(index-1+tabs.length)%tabs.length;if(event.key==='Home')next=0;if(event.key==='End')next=tabs.length-1;if(next!==undefined){event.preventDefault();choose(tabs[next]);tabs[next].focus()}})});
</script>
</body></html>
@@ -0,0 +1,93 @@
Copyright 2018 Boutros International. (http://www.boutrosfonts.com)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

+82
View File
@@ -0,0 +1,82 @@
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#061d42">
<meta name="description" content="سيرو ماب: خرائط سهلة بلا تسجيل للعراق والأردن وسوريا ومصر، ورؤية لدليل سياحي يبدأ من معالم العراق.">
<title>سيرو ماب — كل طريق، بداية حكاية</title>
<link rel="icon" type="image/png" href="assets/siro-maps.png">
<style>
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(assets/tajawal-400.ttf) format('truetype');
}
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(assets/tajawal-500.ttf) format('truetype');
}
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(assets/tajawal-700.ttf) format('truetype');
}
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(assets/tajawal-800.ttf) format('truetype');
}
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(assets/tajawal-900.ttf) format('truetype');
}
:root{--navy:#061d42;--blue:#124f9f;--gold:#b59247;--pale:#edf3f9;--ink:#132844;--muted:#5c6c80;--line:#dce3ec;--white:#fff;--font:'Tajawal','Geeza Pro',Tahoma,sans-serif}
*{box-sizing:border-box}html{scroll-behavior:smooth;scroll-padding-top:100px}body{margin:0;background:#fff;color:var(--ink);font-family:var(--font);font-size:18px;line-height:1.85}a{color:inherit;text-decoration:none}button{font:inherit}button,a{-webkit-tap-highlight-color:transparent}button:focus-visible,a:focus-visible{outline:3px solid #c6a057;outline-offset:6px}button{cursor:pointer}img{max-width:100%;height:auto;display:block}h1,h2,h3,p{margin:0}h1,h2,h3{line-height:1.35}h2{font-size:clamp(32px,3.5vw,49px);font-weight:800;letter-spacing:-.6px}h3{font-size:25px}::selection{background:#d6bb7b;color:var(--navy)}.wrap{width:min(1190px,calc(100% - 88px));margin:auto}.eyebrow{font-size:14px;letter-spacing:1.8px;font-weight:700;color:var(--gold);display:flex;align-items:center;gap:12px;margin-bottom:22px}.eyebrow:before{content:'';width:28px;height:2px;background:currentColor}.latin{font-family:Arial,sans-serif;letter-spacing:2.5px;font-size:12px;direction:ltr;display:inline-block}.muted{color:var(--muted)}
header{height:100px;background:rgba(255,255,255,.97);border-bottom:1px solid var(--line);position:relative;z-index:5}.nav{height:100%;display:flex;align-items:center;justify-content:space-between;gap:24px}.brand{display:flex;gap:13px;align-items:center}.brand img{width:62px;height:62px;border-radius:15px}.brand strong{font-size:23px;display:block;line-height:1.3}.brand small{color:var(--muted);font-size:11px;letter-spacing:2.2px;display:block;direction:ltr}.links{display:flex;align-items:center;gap:30px;font-size:16px;font-weight:600}.links a{transition:color .2s}.links a:hover{color:var(--blue)}.nav-tag{font-size:14px;color:var(--blue);border:1px solid #c9d8ed;padding:7px 17px;border-radius:30px;white-space:nowrap}
.hero{position:relative;overflow:hidden;background:var(--navy);color:white;padding:75px 0 0}.hero:before{content:'';position:absolute;width:740px;height:740px;left:-140px;top:-220px;background:radial-gradient(circle,rgba(39,105,180,.38),transparent 67%);pointer-events:none}.hero-grid{display:grid;grid-template-columns:1.12fr 1fr;align-items:center;gap:65px;position:relative;min-height:470px;padding-bottom:65px}.hero h1{font-size:clamp(44px,5.7vw,76px);font-weight:800;line-height:1.24;letter-spacing:-1px}.hero h1 span{color:#dbbc77}.hero p{font-size:20px;color:#c0ccdc;max-width:530px;margin-top:27px;line-height:1.9}.hero .eyebrow{color:#ddbd78}.hero-actions{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-top:33px}.btn{display:inline-flex;align-items:center;justify-content:center;gap:20px;padding:11px 23px;border-radius:7px;font-size:17px;font-weight:700;border:1px solid transparent;min-height:50px}.btn-gold{background:#dbbc77;color:#0b2547}.btn-gold:hover{background:#efd49b}.btn-outline{border-color:#526580;color:#e3eaf3}.btn-outline:hover{background:#ffffff0c}.arrow{font-family:Arial;font-size:23px;font-weight:400}.hero-status{display:block;font-size:14px;color:#acbbcf;margin-top:17px}.brand-stage{position:relative;display:flex;justify-content:center;align-items:center;min-height:420px;padding:15px}.brand-stage:before,.brand-stage:after{content:'';position:absolute;border:1px solid #759ed12b;border-radius:50%;width:410px;height:410px;pointer-events:none}.brand-stage:after{width:480px;height:480px;border-style:dashed;opacity:.6}.hero-icon{width:330px;border-radius:70px;transform:rotate(-6deg);box-shadow:0 30px 60px #0004;z-index:1}.float-label{position:absolute;z-index:2;border:1px solid #6986af6b;background:#0b2b55eb;box-shadow:0 10px 35px #0002;backdrop-filter:blur(15px);padding:10px 17px;border-radius:10px;font-size:16px;display:flex;align-items:center;gap:12px}.float-label svg{width:22px;height:22px;stroke:#e0bf7d;fill:none;stroke-width:1.6}.label-one{left:-5px;top:60px}.label-two{right:4px;bottom:43px}.hero-facts{position:relative;border-top:1px solid #ffffff20;display:grid;grid-template-columns:repeat(3,1fr);padding:24px 0;gap:30px}.fact{display:flex;gap:17px;align-items:center}.fact:not(:last-child){border-left:1px solid #ffffff20}.fact b{font-size:35px;color:#dfc28a;font-weight:500;line-height:1}.fact span{display:block;font-size:17px}.fact small{display:block;color:#a8b8cd;font-size:14px}
.story{padding:88px 0;display:grid;grid-template-columns:.9fr 1.1fr;gap:95px;align-items:center}.story p{color:var(--muted);font-size:20px}.story .accent{color:var(--ink);font-weight:700}.story .eyebrow{margin-bottom:16px}.section-intro{display:flex;align-items:flex-end;justify-content:space-between;gap:40px;margin-bottom:37px}.section-intro p{max-width:430px;color:var(--muted)}
.coverage{background:var(--pale);padding:72px 0 78px}.country-tabs{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:24px}.country-tab{display:flex;align-items:center;justify-content:space-between;padding:17px 22px;background:transparent;border:1px solid #cdd8e6;color:var(--ink);border-radius:8px;transition:background .2s;min-height:83px}.country-tab strong{font-size:23px}.country-tab small{display:block;font-size:12px;letter-spacing:2px;line-height:1.2;color:var(--muted);direction:ltr}.country-tab .number{font-family:Arial;font-size:14px;color:#8b9db3}.country-tab[aria-selected=true]{background:var(--navy);border-color:var(--navy);color:white}.country-tab[aria-selected=true] small,.country-tab[aria-selected=true] .number{color:#d8bd81}.country-panel{background:#fff;border-radius:12px;padding:27px 32px;display:grid;grid-template-columns:1fr 1.4fr;gap:35px;align-items:center;border:1px solid #e2e9f2}.country-panel h3{font-size:28px;margin-bottom:5px}.country-panel p{font-size:17px;color:var(--muted)}.panel-note{border-right:2px solid #c4a266;padding-right:24px}.pill{display:inline-block;font-size:13px;border:1px solid #d9e1ec;background:#f7f9fc;padding:2px 10px;border-radius:30px;color:#49617f;margin-bottom:9px}
.tourism{padding:90px 0}.tourism-feature{display:grid;grid-template-columns:1.15fr 1fr;background:var(--navy);color:white;border-radius:18px;overflow:hidden;min-height:415px}.tourism-photo{position:relative;background:#15395e;min-height:380px;overflow:hidden}.tourism-photo img{width:100%;height:100%;object-fit:cover;position:absolute}.photo-caption{position:absolute;bottom:0;right:0;left:0;color:white;padding:45px 28px 22px;background:linear-gradient(transparent,#041a35e6);font-size:16px}.photo-caption small{display:block;opacity:.8;font-size:12px;direction:ltr;text-align:right}.tourism-copy{padding:42px 40px;align-self:center}.tourism-copy h3{font-size:35px;margin:12px 0 15px}.tourism-copy p{color:#beccdd}.tourism-copy .pill{background:#ffffff0c;border-color:#597087;color:#e2c388}.tourism-list{padding:0;list-style:none;display:grid;grid-template-columns:1fr 1fr;gap:13px;margin:25px 0 0;font-size:16px;color:#e7edf6}.tourism-list li:before{content:'+';color:#d8b975;padding-left:10px}.tourism-foot{display:flex;justify-content:space-between;gap:25px;margin-top:20px;font-size:15px;color:var(--muted)}.tourism-foot a{color:var(--blue);text-decoration:underline;text-underline-offset:4px}
.pillars-section{padding:80px 0}.pillars-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:24px;margin-top:35px}.pillar-card{background:var(--pale);border:1px solid #dce5f0;border-radius:16px;padding:32px;display:flex;flex-direction:column;justify-content:space-between;transition:transform .2s,box-shadow .2s}.pillar-card:hover{transform:translateY(-4px);box-shadow:0 12px 30px rgba(6,29,66,.08);border-color:#b59247}.pillar-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:18px}.pillar-num{font-size:32px;font-weight:900;color:var(--gold);line-height:1}.pillar-tag{font-size:13px;font-weight:700;color:var(--blue);background:rgba(18,79,159,.08);padding:4px 12px;border-radius:20px}.pillar-card h3{font-size:24px;margin-bottom:12px;color:var(--navy)}.pillar-card p{color:var(--muted);font-size:16px;line-height:1.7;margin-bottom:20px}.pillar-badge{font-size:13px;font-weight:600;color:var(--gold);border-top:1px solid #dce5f0;padding-top:14px}
.ideas{background:#f7f9fc;padding:75px 0 82px}.ideas-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:0;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}.idea{padding:32px 29px 30px}.idea:not(:last-child){border-left:1px solid var(--line)}.idea-index{font-size:13px;direction:ltr;display:block;text-align:right;color:#977837;letter-spacing:2px;margin-bottom:22px}.idea svg{width:34px;height:34px;fill:none;stroke:var(--blue);stroke-width:1.5;margin-bottom:15px}.idea h3{margin-bottom:13px;font-size:25px}.idea p{font-size:17px;color:var(--muted)}.benefit{margin-top:24px;padding-top:17px;border-top:1px solid var(--line);font-size:15px;color:var(--blue)}.ideas-note{font-size:14px;color:var(--muted);margin-top:20px}
.roadmap{padding:90px 0}.steps{display:grid;grid-template-columns:repeat(3,1fr);gap:32px;margin-top:42px}.step{border-top:2px solid #dce3ec;padding-top:22px}.step.current{border-top-color:var(--gold)}.step-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}.step-top span{font-size:14px;color:#728096}.step-top b{font-size:28px;font-family:Arial;font-weight:400;color:#a7b4c4}.step.current .step-top span{color:#907132}.step h3{font-size:25px;margin-bottom:12px}.step p{color:var(--muted);font-size:17px}
.partnership{border:1px solid var(--line);border-radius:15px;display:grid;grid-template-columns:160px 1fr;gap:40px;padding:32px 40px;align-items:center;margin-bottom:70px}.prize-logo{width:150px}.partnership .eyebrow{font-size:13px;margin-bottom:11px}.partnership h3{font-size:29px;margin-bottom:9px}.partnership p{font-size:18px;color:var(--muted);max-width:820px}.closing{background:var(--navy);color:white;text-align:center;padding:65px 24px}.closing .latin{color:#dbbc77;margin-bottom:15px}.closing h2{font-size:clamp(29px,4vw,46px)}.closing p{color:#acbed4;margin-top:15px;font-size:18px}.footer{padding:23px 0;display:flex;justify-content:space-between;align-items:center;gap:20px;font-size:14px;color:var(--muted)}.footer a{color:var(--blue)}.footer .latin{font-size:11px;letter-spacing:1px}
@media(min-width:1500px){.hero-grid{min-height:530px}}
@media(max-width:1050px){.wrap{width:calc(100% - 48px)}.links{gap:18px}.nav-tag{display:none}.hero-grid{gap:25px}.hero-icon{width:280px;border-radius:58px}.brand-stage:before{width:340px;height:340px}.brand-stage:after{width:390px;height:390px}.label-one{left:0;top:45px}.label-two{right:0;bottom:45px}.story{gap:45px}.tourism-copy{padding:30px}.idea{padding:28px 20px}.country-tab{padding:15px}.hero h1{font-size:58px}}
@media(max-width:760px){body{font-size:17px}.wrap{width:calc(100% - 36px)}header{height:auto;min-height:86px}.nav{padding:13px 0;flex-wrap:wrap;gap:10px}.brand img{width:48px;height:48px}.brand strong{font-size:20px}.brand small{font-size:10px}.links{gap:19px;font-size:14px}.links a:last-child{display:none}.hero{padding-top:44px}.hero-grid{grid-template-columns:1fr;gap:23px;padding-bottom:35px}.hero h1{font-size:clamp(44px,10vw,62px);line-height:1.3}.hero p{font-size:18px;margin-top:20px}.hero-actions{margin-top:25px}.btn{font-size:16px;padding:10px 19px}.hero-status{font-size:13px}.brand-stage{min-height:325px;width:min(100%,390px);margin:auto}.hero-icon{width:240px;border-radius:52px}.brand-stage:before{width:285px;height:285px}.brand-stage:after{width:320px;height:320px}.float-label{font-size:14px;padding:8px 12px}.label-one{top:39px;left:0}.label-two{bottom:23px;right:0}.hero-facts{gap:15px;padding:20px 0}.fact{gap:8px;display:block}.fact b{font-size:27px}.fact span{font-size:14px}.fact small{font-size:12px}.story{grid-template-columns:1fr;gap:22px;padding:53px 0}.story p{font-size:18px}.eyebrow{font-size:13px;margin-bottom:15px}.section-intro{display:block;margin-bottom:27px}.section-intro p{margin-top:17px;font-size:17px}.coverage{padding:48px 0}.country-tabs{grid-template-columns:repeat(2,1fr);gap:9px}.country-tab{min-height:78px}.country-tab strong{font-size:22px}.country-panel{grid-template-columns:1fr;gap:18px;padding:24px}.panel-note{padding-right:16px}.tourism{padding:55px 0}.tourism-feature{grid-template-columns:1fr}.tourism-photo{min-height:280px}.tourism-copy{padding:29px 25px}.tourism-copy h3{font-size:30px}.tourism-foot{display:block;font-size:14px}.tourism-foot a{display:inline-block;margin-top:7px}.ideas{padding:50px 0}.ideas-grid{grid-template-columns:1fr}.idea{padding:28px 4px}.idea:not(:last-child){border-left:0;border-bottom:1px solid var(--line)}.idea-index{margin-bottom:14px}.idea svg{float:left}.benefit{margin-top:17px}.roadmap{padding:53px 0}.steps{grid-template-columns:1fr;gap:28px;margin-top:25px}.step{padding-top:14px}.step-top{margin-bottom:8px}.partnership{grid-template-columns:1fr;gap:18px;padding:26px;margin-bottom:45px}.prize-logo{width:120px}.partnership h3{font-size:25px}.partnership p{font-size:17px}.closing{padding:45px 22px}.footer{flex-direction:column;align-items:flex-start;gap:8px}.tourism-list{font-size:15px}}
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*{transition:none!important}}
@media print{header,.hero-actions,.hero-status,.country-tabs,.footer a{display:none}body{font-size:12pt}.hero,.closing,.tourism-feature{print-color-adjust:exact;-webkit-print-color-adjust:exact}.wrap{width:92%}.hero{padding-top:30px}.hero-grid{min-height:0;gap:25px}.hero h1{font-size:38px}.hero p{font-size:16px}.brand-stage{min-height:250px}.hero-icon{width:200px}.brand-stage:before,.brand-stage:after,.float-label{display:none}.story,.tourism,.ideas,.roadmap,.coverage{padding:25px 0}.section-intro h2,h2{font-size:28px}.story p{font-size:16px}.tourism-feature{break-inside:avoid}.idea,.step,.partnership{break-inside:avoid}.partnership{margin-bottom:25px}.closing{padding:25px}}
</style>
</head>
<body id="top">
<header><nav class="wrap nav" aria-label="التنقل الرئيسي"><a class="brand" href="#top" aria-label="سيرو ماب، بداية الصفحة"><img src="assets/siro-maps.png" alt="شعار سيرو ماب" width="62" height="62"><span><strong>سيرو ماب</strong><small>SIRO MAP</small></span></a><div class="links"><a href="#coverage">الانطلاقة</a><a href="#tourism">اكتشف العراق</a><a href="#pillars">ركائز الدليل</a><a href="#ideas">آفاق سيرو ماب</a><a href="#journey">رحلتنا</a></div><span class="nav-tag">من سيرو ماب، إلى كل وجهة</span></nav></header>
<main>
<section class="hero" aria-labelledby="hero-title"><div class="wrap"><div class="hero-grid"><div><div class="eyebrow">الانطلاقة الأولى · سيرو ماب</div><h1 id="hero-title">كل طريق،<br>بداية <span>حكاية.</span></h1><p>خرائط تقرّب المسافات، وحكايات تقرّبنا من المكان.<br>نبدأ بأربع دول، ونفتح من العراق بابًا لاكتشاف الثقافة والمعالم والوجهات.</p><div class="hero-actions"><a class="btn btn-gold" href="#coverage">اكتشف الانطلاقة <span class="arrow" aria-hidden="true">←</span></a><a class="btn btn-outline" href="#ideas">ما الذي نبنيه معًا؟</a></div><small class="hero-status">نسخة التطبيق قيد المراجعة على Google Play</small></div><div class="brand-stage"><img class="hero-icon" src="assets/siro-maps.png" alt="شعار سيرو ماب الذكي للملاحة والتنقل" width="512" height="512"><span class="float-label label-one"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 10c0 6-8 11-8 11S4 16 4 10a8 8 0 1 1 16 0Z"/><circle cx="12" cy="10" r="2.5"/></svg>وجهتك أقرب</span><span class="float-label label-two"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m3 5 6-2 6 2 6-2v16l-6 2-6-2-6 2Z M9 3v16 M15 5v16"/></svg>اكتشف حكاية المكان</span></div></div><div class="hero-facts"><div class="fact"><b>04</b><div><span>دول في الانطلاقة</span><small>العراق · الأردن · سوريا · مصر</small></div></div><div class="fact"><b>01</b><div><span>بداية للدليل السياحي</span><small>معالم العراق أولًا</small></div></div><div class="fact"><b>ببساطة</b><div><span>خرائط بلا تسجيل</span><small>افتح التطبيق، وابدأ رحلتك</small></div></div></div></div></section>
<section class="wrap story" aria-labelledby="story-title"><div><div class="eyebrow">فكرة تجمعنا</div><h2 id="story-title">من اسم يحمل حضارة،<br>إلى خدمة ترافق الناس.</h2></div><p>تعتمد سيرو ماب على الابتكار والسيادة الرقمية وتوفير التكاليف. واليوم، نمنح هذا المشروع حضورًا قويًا في الحياة اليومية: خرائط سهلة ومستقلة، ودليل يكشف ما حولنا من وجهات ومعالم.<br><span class="accent">خطوة أولى لشراكتنا، وأساس ننمو عليه معًا.</span></p></section>
<section class="coverage" id="coverage" aria-labelledby="coverage-title"><div class="wrap"><div class="section-intro"><div><div class="eyebrow">أربع دول · تجربة واحدة</div><h2 id="coverage-title">بداية قريبة منّا.</h2></div><p>خرائط للاستخدام دون إنشاء حساب. ننطلق بهذا النطاق، ونواصل تحسين البيانات والتجربة قبل التوسع إلى مزيد من الدول العربية.</p></div><div class="country-tabs" role="tablist" aria-label="دول الانطلاقة"><button id="tab-iq" class="country-tab" role="tab" aria-selected="true" aria-controls="country-panel" data-country="iq"><span><strong>العراق</strong><small>IRAQ</small></span><span class="number">01</span></button><button id="tab-jo" class="country-tab" role="tab" aria-selected="false" aria-controls="country-panel" tabindex="-1" data-country="jo"><span><strong>الأردن</strong><small>JORDAN</small></span><span class="number">02</span></button><button id="tab-sy" class="country-tab" role="tab" aria-selected="false" aria-controls="country-panel" tabindex="-1" data-country="sy"><span><strong>سوريا</strong><small>SYRIA</small></span><span class="number">03</span></button><button id="tab-eg" class="country-tab" role="tab" aria-selected="false" aria-controls="country-panel" tabindex="-1" data-country="eg"><span><strong>مصر</strong><small>EGYPT</small></span><span class="number">04</span></button></div><div id="country-panel" class="country-panel" role="tabpanel" tabindex="0" aria-labelledby="tab-iq"><div><span class="pill">ضمن نطاق الانطلاقة</span><h3 id="country-title">العراق، هنا تبدأ الحكاية.</h3><p id="country-description">الخرائط أولًا، ومنها نبدأ بناء الدليل السياحي.</p></div><p id="country-note" class="panel-note">نبدأ بإثراء المعالم السياحية والأثرية العراقية: قصة المكان، موقعه، ومعلومات تساعد على زيارته.</p></div></div></section>
<section class="wrap tourism" id="tourism" aria-labelledby="tourism-title"><div class="section-intro"><div><div class="eyebrow">الخطوة التالية · الدليل السياحي</div><h2 id="tourism-title">للمكان أكثر من عنوان.</h2></div><p>من معالم العراق نبدأ. نريد أن تتحول النقطة على الخريطة إلى حكاية تعرفها، ووجهة تستطيع التخطيط لزيارتها.</p></div><div class="tourism-feature"><div class="tourism-photo" id="tourism-photo"><img src="assets/erbil-citadel.jpg" alt="قلعة أربيل التاريخية والأسواق المحيطة بها" width="4032" height="3024" loading="lazy"><div class="photo-caption">قلعة أربيل · العراق<small>الصورة: <a href="https://commons.wikimedia.org/wiki/File:Citadel_of_Erbil.jpg" target="_blank" rel="noopener">Sarbast.T.Hameed / Wikimedia Commons</a> · <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener">CC BY-SA 4.0</a> · عرض مقتصّ</small></div></div><div class="tourism-copy"><span class="pill">تصور قادم داخل التطبيق</span><h3>اكتشف ما حولك،<br>واعرف ما وراءه.</h3><p>دليل قريب منك للمعالم والمواقع الأثرية، بمعلومات موثقة تُراجع وتُحدّث مع الشركاء المحليين.</p><ul class="tourism-list"><li>قصة موجزة لكل معلم</li><li>صور وتعريف بالمكان</li><li>المداخل ومعلومات الزيارة</li><li>وجهات قريبة منك</li></ul></div></div><div class="tourism-foot"><span>أمثلة للإثراء: قلعة أربيل، بابل، وآثار ومدن العراق. تُراجع إتاحة الزيارة لكل موقع قبل إدراجه.</span><a href="https://whc.unesco.org/en/statesparties/iq" target="_blank" rel="noopener">تراث العراق لدى اليونسكو ↗</a></div></section>
<section class="wrap pillars-section" id="pillars" aria-labelledby="pillars-title"><div class="section-intro"><div><div class="eyebrow">منهجية العمل · الركائز الأربع للدليل</div><h2 id="pillars-title">كيف نُعيد صياغة تجربة المعالم؟</h2></div><p>لا نكتفي بوضع نقطة صامتة على الخارطة؛ بل نمزج دقة الملاحة الميدانية بسردية التاريخ والسيادة التقنية.</p></div><div class="pillars-grid"><div class="pillar-card"><div class="pillar-header"><span class="pillar-num">01</span><span class="pillar-tag">الملاحة الواقعية</span></div><h3>التوجيه للبوابات الفعلية</h3><p>توجيه السائقين والزوار مباشرة إلى نقاط الدخول الرسمية ومواقف السيارات بدلاً من التوجيه الخاطئ إلى منتصف المواقع الشاسعة أو الأسوار المغلقة.</p><div class="pillar-badge">دقة الوصول الميداني</div></div><div class="pillar-card"><div class="pillar-header"><span class="pillar-num">02</span><span class="pillar-tag">التوثيق الحضاري</span></div><h3>سردية المكان في 60 ثانية</h3><p>بطاقة ذكية ومكثفة لكل معلم: الحقبة التاريخية، من بنى المكان، وأهميته التراثية العالمية، بصور موثقة بدعم المبادرات الثقافية الوطنية.</p><div class="pillar-badge">معرفة سريعة وموثوقة</div></div><div class="pillar-card"><div class="pillar-header"><span class="pillar-num">03</span><span class="pillar-tag">السيادة التقنية</span></div><h3>استكشاف كامل بدون إنترنت</h3><p>حزم محلية خفيفة تحفظ قصص المعالم ومساراتها على الهاتف، لتعمل بكفاءة وسرعة فائقة حتى في المناطق الصحراوية والنائية.</p><div class="pillar-badge">استمرارية في الميدان</div></div><div class="pillar-card"><div class="pillar-header"><span class="pillar-num">04</span><span class="pillar-tag">منظومة النقل</span></div><h3>جسر مباشر بين الاستكشاف والوصول</h3><p>ربط قرار الزيارة فوراً بطلب وسيلة مواصلات ذكية أو بدء مسار الملاحة، لتحويل الاكتشاف الثقافي إلى حركة يومية واقتصاد سياحي نابض.</p><div class="pillar-badge">تكامل ذكي مع الحركة</div></div></div></section>
<section class="ideas" id="ideas" aria-labelledby="ideas-title"><div class="wrap"><div class="section-intro"><div><div class="eyebrow">آفاق سيرو ماب</div><h2 id="ideas-title">كل إضافة، قيمة جديدة.</h2></div><p>أفكار نطوّرها تدريجيًا؛ تخدم المستخدم، وتمنح شركاء سيرو ماب حضورًا يصل إلى الناس.</p></div><div class="ideas-grid"><article class="idea"><span class="idea-index">01 / الشركاء</span><svg viewBox="0 0 32 32" aria-hidden="true"><rect x="3" y="7" width="26" height="19" rx="3"/><path d="M3 13h26 M8 20h6 M21 18v6 M18 21h6"/></svg><h3>مزايا أقرب إليك</h3><p>دليل للمستشفيات والفنادق ومراكز الدورات المتعاقدة مع شبكة سيرو، يعرض موقع كل جهة ومزايا البطاقة وشروطها المعتمدة.</p><div class="benefit">الفائدة: يجد حامل البطاقة أين يستفيد منها.</div></article><article class="idea"><span class="idea-index">02 / الفعاليات</span><svg viewBox="0 0 32 32" aria-hidden="true"><rect x="5" y="6" width="22" height="23" rx="3"/><path d="M10 3v7 M22 3v7 M5 14h22 M10 20h4 M18 20h4 M10 24h4"/></svg><h3>من الدعوة إلى الوجهة</h3><p>خريطة للمؤتمرات والفعاليات الكبرى: موقع القاعة، المداخل، المواقف والوجهات المحيطة، تُشارك برابط أو رمز QR.</p><div class="benefit">الفائدة: كل مناسبة فرصة حقيقية لتجربة التطبيق.</div></article><article class="idea"><span class="idea-index">03 / جودة البيانات</span><svg viewBox="0 0 32 32" aria-hidden="true"><path d="M25 14c0 7-9 14-9 14S7 21 7 14a9 9 0 1 1 18 0Z"/><path d="m12 14 3 3 6-6"/></svg><h3>معًا، نصحّح الخريطة</h3><p>اقتراح مكان، تصحيح عنوان أو تحديد مدخل. مساهمات تُراجع، وبيانات استخدام اختيارية تساعد على تحسين التجربة.</p><div class="benefit">الفائدة: عناوين أدق، وأساس أفضل للنقل لاحقًا.</div></article></div><p class="ideas-note">هذه إضافات مقترحة للتطوير؛ ظهور الجهات والمزايا مرتبط بتأكيد بياناتها واتفاقاتها.</p></div></section>
<section class="wrap roadmap" id="journey" aria-labelledby="journey-title"><div class="eyebrow">رحلة تنمو بخطوات واضحة</div><h2 id="journey-title">نبدأ بما ينفع اليوم.<br>ونبني ما نحتاجه غدًا.</h2><div class="steps"><article class="step current"><div class="step-top"><span>الانطلاقة الأولى</span><b>01</b></div><h3>خرائط بين يدي الناس</h3><p>أربع دول، بلا تسجيل. تعريف بسيرو ماب، وتجارب فعلية تُحسّن المنتج وتبني حضور الاسم.</p></article><article class="step"><div class="step-top"><span>الإثراء التدريجي</span><b>02</b></div><h3>دليل يستحق العودة</h3><p>معالم العراق أولًا، ثم الشركاء والفعاليات؛ أسباب متجددة لفتح التطبيق والاستفادة منه.</p></article><article class="step"><div class="step-top"><span>الأفق القادم</span><b>03</b></div><h3>نحو سيرو للنقل الذكي</h3><p>ربط اكتشاف الوجهة بالوصول إليها عند إطلاق خدمة النقل، والتوسع إلى دول عربية أخرى تدريجيًا.</p></article></div></section>
<section class="closing"><span class="latin">SIRO MAP · THE FIRST JOURNEY</span><h2>من هنا تبدأ حكايتنا على الخريطة.</h2><p>العراق · الأردن · سوريا · مصر</p></section>
</main><footer class="wrap footer"><span>سيرو ماب © 2026 <span aria-hidden="true"> — </span> الانطلاقة الأولى ورؤية التطوير</span><a href="#top">العودة إلى البداية ↑</a></footer>
<script>
const countries={iq:{title:'العراق، هنا تبدأ الحكاية.',description:'الخرائط أولًا، ومنها نبدأ بناء الدليل السياحي.',note:'نبدأ بإثراء المعالم السياحية والأثرية العراقية: قصة المكان، موقعه، ومعلومات تساعد على زيارته.'},jo:{title:'الأردن، ضمن أولى وجهاتنا.',description:'استخدام الخرائط دون الحاجة إلى إنشاء حساب.',note:'الأردن ضمن نطاق الخرائط في الانطلاقة الأولى. يبدأ إثراء المحتوى السياحي من العراق، ثم يتوسع تدريجيًا.'},sy:{title:'سوريا، أقرب على الخريطة.',description:'تجربة خرائط سهلة ضمن الدول الأربع الأولى.',note:'نطوّر جودة البيانات بالاختبار والمراجعة ومساهمات المستخدمين؛ لتصبح معلومات الأماكن أكثر دقة وفائدة.'},eg:{title:'مصر، امتداد لانطلاقتنا.',description:'خرائط بلا تسجيل ضمن تجربة سيرو ماب.',note:'مصر ضمن نطاق الانطلاقة. نبدأ بخدمة الخرائط، ونضيف الدليل والوجهات والشراكات على مراحل واضحة.'}};
const tabs=[...document.querySelectorAll('[role=tab]')];function choose(tab){tabs.forEach(t=>{const selected=t===tab;t.setAttribute('aria-selected',String(selected));t.tabIndex=selected?0:-1});const item=countries[tab.dataset.country];document.getElementById('country-title').textContent=item.title;document.getElementById('country-description').textContent=item.description;document.getElementById('country-note').textContent=item.note;document.getElementById('country-panel').setAttribute('aria-labelledby',tab.id)}tabs.forEach((tab,index)=>{tab.addEventListener('click',()=>choose(tab));tab.addEventListener('keydown',event=>{let next;if(event.key==='ArrowLeft')next=(index+1)%tabs.length;if(event.key==='ArrowRight')next=(index-1+tabs.length)%tabs.length;if(event.key==='Home')next=0;if(event.key==='End')next=tabs.length-1;if(next!==undefined){event.preventDefault();choose(tabs[next]);tabs[next].focus()}})});
</script>
</body></html>
+3
View File
@@ -0,0 +1,3 @@
# MapSaaS Sovereign API Keys Template
MAP_SAAS_API_KEY=in_xxxxxxxxxxxxxxxxxxxxxxxx
GOOGLE_MAP_API_KEY=AIzaSyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+50
View File
@@ -0,0 +1,50 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# Environment variables & secrets
.env
.env.*
!.env.example
+45
View File
@@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "ee80f08bbf97172ec030b8751ceab557177a34a6"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
- platform: android
create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
- platform: ios
create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
- platform: linux
create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
- platform: macos
create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
- platform: web
create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
- platform: windows
create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+80
View File
@@ -0,0 +1,80 @@
# تعليمات فاحصي متجر التطبيقات (App Review & Testing Instructions)
## لمتجر جوجل بلاي (Google Play Console) ومتجر آبل (App Store Connect)
---
### أولاً: النص المخصص للنسخ المباشر إلى Google Play Console (باللغة الإنجليزية - المعتمدة للمراجعين)
> **مكان اللصق في Google Play Console**:
> انتقل إلى: **سياسة التطبيق (App content)** ➔ **الوصول إلى التطبيق (App access)** ➔ اختر **"جميع الوظائف أو بعضها خاضع لقيود" (All or some functionality is restricted)** ➔ أضف تعليمات جديدة:
```text
IMPORTANT NOTICE FOR APP REVIEWERS:
1. Operational Geographic Coverage:
Siro Map is engineered on an independent, self-hosted regional spatial vector engine. Our active vector map tiles and routing servers are currently provisioned specifically for three operational countries in the MENA region:
- Jordan (الأردن)
- Iraq (العراق)
- Egypt (مصر)
If you test the app on a physical device or emulator located outside these three countries (e.g., from the United States or Europe) with local GPS enabled, the camera will center on your local coordinates where our regional map tiles are not rendered, resulting in an empty tile canvas. This is expected behavior due to localized regional hosting.
2. How to Test and Review the App Successfully:
OPTION A (Recommended - Instant In-App Search):
1. Launch the app and grant or dismiss location permissions.
2. In the top search bar, type any landmark or city within our coverage area:
- For Jordan: Type "Amman" or "عمان" or "City Mall"
- For Iraq: Type "Baghdad" or "بغداد" or "Tahrir Square"
- For Egypt: Type "Cairo" or "القاهرة" or "Nasr City"
3. Select any result. The map will instantly fly to the location, load rich vector tiles, 3D buildings, and gates.
4. Tap "Start Navigation" or "ابدأ الملاحة" to experience turn-by-turn routing, Arabic voice guidance, and vehicle markers.
OPTION B (Simulated GPS Location):
In your emulator or testing device, set a mock GPS location to any of our operational hubs:
- Amman (Jordan): Latitude 31.9539, Longitude 35.9106
- Baghdad (Iraq): Latitude 33.3152, Longitude 44.3661
- Cairo (Egypt): Latitude 30.0444, Longitude 31.2357
The app will immediately render full street networks and navigation HUD.
3. Authentication & Credentials:
No username or password is required. The application automatically registers via secure hardware device fingerprinting and provisions a consumer API key seamlessly on initial launch.
For any technical inquiries during review, please contact: support@intaleqapp.com
```
---
### ثانياً: النص باللغة العربية (للتوثيق الداخلي والمراجعين الناطقين بالعربية)
```text
ملاحظة هامة لفاحصي ومراجعي التطبيق:
1. نطاق التغطية الجغرافية التشغيلية:
تعتمد «خرائط سيرو» على منظومة خرائط وملاحة فيكتورية مستقلة ومستضافة ذاتياً، وتغطي خوادم الخرائط والبيانات المكانية الحالية ثلاث دول تشغيلية رئيسية في منطقة الشرق الأوسط وشمال أفريقيا:
- الأردن
- العراق
- مصر
في حال تم فتح التطبيق واختباره من جهاز يقع خارج هذه الدول الثلاث (مثل الولايات المتحدة أو أوروبا) مع تفعيل نظام تحديد المواقع المحلي (GPS)، فإن الكاميرا ستتوجه لموقع الجهاز الحالي حيث لا تتوفر بلاطات خرائط محلية خارج نطاق التغطية، مما يؤدي لعدم ظهور تفاصيل الطرق. هذا سلوك طبيعي ناتج عن التخصيص الجغرافي للمنظومة.
2. كيفية فحص وتجربة التطبيق بنجاح:
الخيار الأول (الموصى به - عبر البحث المباشر داخل التطبيق):
1. افتح التطبيق، ثم في شريط البحث العلوي ابحث عن أي مدينة أو معلم في الدول المدعومة:
- الأردن: ابحث عن "عمان" أو "سيتي مول" أو "الدوار السابع"
- العراق: ابحث عن "بغداد" أو "ساحة التحرير" أو "أربيل"
- مصر: ابحث عن "القاهرة" أو "ميدان التحرير" أو "مدينة نصر"
2. اختر أي نتيجة من القائمة، وستنتقل الخريطة فوراً للموقع وتظهر البلاطات الفيكتورية وتفاصيل الطرق والبوابات.
3. اضغط على زر "ابدأ الملاحة" لتجربة مسارات السير والتوجيه الصوتي ونمط القيادة.
الخيار الثاني (عبر محاكاة الموقع GPS Mock Location):
في المحاكي أو جهاز الفحص، قم بتعيين إحداثيات موقع وهمي ضمن إحدى المدن التالية:
- عمان (الأردن): خط العرض 31.9539 ، خط الطول 35.9106
- بغداد (العراق): خط العرض 33.3152 ، خط الطول 44.3661
- القاهرة (مصر): خط العرض 30.0444 ، خط الطول 31.2357
وستعمل الخريطة والملاحة اللحظية على الفور.
3. بيانات تسجيل الدخول والوصول:
لا يتطلب التطبيق أي اسم مستخدم أو كلمة مرور. التطبيق يعمل فوراً بمجرد الفتح ويقوم بتوليد مفتاح وصول مشفر عبر بصمة الجهاز تلقائياً.
```
+221
View File
@@ -0,0 +1,221 @@
# ملف بيانات ونصوص النشر على Google Play Console | خرائط سيرو (Siro Map)
================================================================================
هذا الملف يحتوي على كافة النصوص والبيانات المطلوبة لرفع وتعبئة تطبيق **Siro Map** على **Google Play Console** باللغتين العربية والإنجليزية، وفق معايير خوارزميات تحسين متجر التطبيقات (ASO) وتوافقاً مع الحدود الرسمية لعدد الحروف، مع الحفاظ على معرف الحزمة التقني المعتمد على المتجر: `com.urukmap.app`.
---
## 1. البيانات الأساسية (Basic Details)
### باللغة العربية (Arabic - Default)
* **اسم التطبيق (App name - بحد أقصى 30 حرفاً)**:
```text
خرائط سيرو: ملاحة وتوجيه ذكي
```
*(بديل مقترح: `خرائط سيرو - Siro Map`)*
* **الوصف القصير (Short description - بحد أقصى 80 حرفاً)**:
```text
ملاحة ذكية وتوجيه صوتي عربي بدقة متناهية مع خريطة وتوجيه دون اتصال بالإنترنت.
```
* **الوصف الكامل (Full description - بحد أقصى 4000 حرف)**:
```text
«خرائط سيرو - Siro Map» هي المنظومة المتقدمة للملاحة والخرائط الذكية المصممة خصيصاً لتلبية احتياجات العالم العربي ومنطقة الشرق الأوسط وشمال أفريقيا، بموثوقية فائقة وتقنيات تشغيل مستقلة.
سواء كنت تتنقل داخل المدن المزدحمة، أو تقود عبر الطرق السريعة في المناطق التي تعاني من ضعف شبكات التغطية أو قيود خدمات الخرائط العالمية (في العراق، السودان، اليمن، سوريا، والمنطقة)، تمنحك خرائط سيرو تجربة تنقل دقيقة، سريعة، واقتصادية في استهلاك البيانات.
لماذا تختار خرائط سيرو؟
🧠 1. كاش ذكي واستدعاء فوري من الذاكرة
نظام متقدم لحفظ بلاطات الخريطة في ذاكرة الجهاز (Memory Cache) لاستدعائها فوراً دون تأخير، مما يوفر سرعة استجابة فائقة ويقلل استهلاك باقة الإنترنت.
🔐 2. مفتاح جهاز مشفر وحماية مخصصة (Device Key)
تسجيل آمن عبر بصمة الجهاز الفريدة وتخصيص مفتاح وصول مشفر يضمن أقصى معايير الاستقرار وحماية خصوصية بيانات المستخدمين.
⚡ 3. توجيه لحظي Turn-by-Turn بدقة عالية
محرك ملاحة ذكي متطور يوجهك خطوة بخطوة مع مسارات بديلة لتفادي الازدحام المروري، وزمن استجابة فائق السرعة.
🎙️ 4. توجيه صوتي عربي نقي وواضح
إرشادات صوتية واضحة باللغة العربية تنبهك مسبقاً قبل كل منعطف، دوار، أو تقاطع لتظل عيناك على الطريق دائماً.
📡 5. ملاحة فعالة دون اتصال بالإنترنت (Offline Mode)
واصل رحلتك وتوجيهك حتى عند انقطاع الإنترنت أو أثناء السفر بين المحافظات والمناطق النائية بفضل نظام التخزين المؤقت الذكي.
📺 6. نافذة مصغرة عائمة (Picture-in-Picture)
تابع مسار الخريطة والسرعة اللحظية في نافذة عائمة مدمجة أثناء استخدام تطبيقاتك الأخرى أو الرد على المكالمات.
⚠️ 7. شبكة بلاغات تشاركية للحوادث ومخاطر الطريق
شارك واطلع لحظياً على بلاغات الطرق: الحوادث، الإغلاقات، الحفر، وأعمال الصيانة لحماية مركبتك وتوفير وقتك.
🚗 8. تخصيص أيقونة ونمط المركبة
اختر شكل ولون مركبتك على الخريطة (سيارة حديثة، دفع رباعي، سهم توجيه) لتجربة مخصصة وفاخرة.
🛡️ 9. حماية الخصوصية وتوفير البطارية
تصميم برمجي حديث ومحسّن لا يستهلك طاقة البطارية ولا يجمع بياناتك الخاصة بدون إذن.
حمّل «خرائط سيرو» اليوم واستمتع بتجربة ملاحة حرة، ذكية، ومستقلة 100%!
```
---
### باللغة الإنجليزية (English - United States)
* **App Name (Max 30 characters)**:
```text
Siro Map: GPS Navigation
```
*(Alternative: `Siro Map - Smart Navigation`)*
* **Short Description (Max 80 characters)**:
```text
Smart GPS navigation, memory-cached offline maps, and turn-by-turn voice alerts.
```
* **Full Description (Max 4000 characters)**:
```text
Siro Map is an advanced GPS navigation and digital mapping platform engineered specifically for the Middle East, North Africa, and regional markets requiring dependable offline capabilities.
Whether commuting through bustling metropolitan hubs or navigating transit routes across Iraq, Sudan, Yemen, Syria, and the broader MENA region, Siro Map delivers unmatched precision, high-speed memory caching, and independent offline reliability.
KEY FEATURES:
🧠 1. Smart Memory Caching & Instant Tile Retrieval
Proactive in-memory tile caching (Vector Tiles) that loads maps instantly, delivering smooth interactions while significantly reducing cellular data consumption.
🔐 2. Encrypted Hardware Fingerprint & Dedicated Device Keys
Secure, transparent device registration that issues an individual encrypted API key per hardware fingerprint, guaranteeing data integrity and stable connection quotas.
⚡ 3. Real-Time Turn-by-Turn GPS Navigation
Intelligent routing engine delivering real-time lane guidance, optimal detour calculations, and ultra-low latency navigation responsiveness.
🎙️ 4. Natural Arabic & English Voice Guidance
Crystal-clear voice instructions alerting you well in advance of upcoming exits, roundabouts, and turns, keeping your focus strictly on the road.
📡 5. Resilient Offline Navigation
Drive with complete confidence even without an internet connection or in remote corridors with reliable offline routing.
📺 6. Live Picture-in-Picture (PiP) HUD
Keep your navigation visible in an elegant floating overlay while multitasking, answering calls, or switching apps.
⚠️ 7. Community-Driven Road & Hazard Alerts
Report and receive live road hazard updates, speed cameras, accidents, and road closures contributed by fellow drivers in real time.
🚗 8. Customizable Vehicle Icons
Personalize your on-screen vehicle marker with a curated selection of 3D luxury sedans, rugged SUVs, or aerodynamic tactical navigation arrows.
🔋 9. Battery & Data Optimized
Engineered with high-performance native rendering (MapLibre vector graphics) that maximizes battery endurance and minimizes background data.
Download Siro Map today and experience fast, smart, and independent navigation!
```
---
## 2. التصنيف والفئة على المتجر (Store Categorization & Tags)
* **الفئة الرئيسية (Primary Category)**:
`خرائط وملاحة (Maps & Navigation)`
* **الفئة الثانوية (Secondary Category)**:
`السفر والمعلومات المحلية (Travel & Local)`
* **العلامات والوسوم (Tags)**:
- `ملاحة (Navigation)`
- `خرائط بدون إنترنت (Offline Maps)`
- `تحديد المواقع (GPS)`
- `تخطيط المسار (Route Planner)`
- `القيادة وحركة المرور (Driving & Traffic)`
---
## 3. الأصول الرسومية المطلوبة (Store Graphic Assets)
1. **أيقونة التطبيق (App Icon)**:
- القياس: `512 x 512` بكسل
- الصيغة: `PNG 32-bit`
- المسار الجاهز: `assets/images/siro_map_playstore_512.png`
2. **الرسم المميز / البانر (Feature Graphic)**:
- القياس: `1024 x 500` بكسل
- الصيغة: `JPEG / PNG` (بدون شفافية)
- المسار الجاهز: `assets/images/siro_playstore_feature_graphic_1024x500.png`
3. **لقطات الشاشة (Screenshots)**:
- الحد الأدنى: 2 لقطات (المستحسن من 4 إلى 8 لقطات).
- القياس المناسب: عمودي `1080 x 1920` بكسل (أبعاد 16:9).
- اللقطات المقترحة:
1. شاشة الخريطة الرئيسية مع شريط البحث والملاحة الفيكتورية.
2. شاشة التوجيه الفعلي Turn-by-Turn مع بطاقة PiP ولوحة السرعة.
3. شاشة طبقات الخريطة وتخصيص أيقونة المركبة.
4. شاشة بطاقة التعريف ومنظومة سيرو ماب للسيادة المكانية.
---
## 4. إعدادات الخصوصية والاتصال (Privacy & Contact Information)
* **رابط سياسة الخصوصية (Privacy Policy URL)**:
```text
https://intaleqapp.com/privacy.html
```
* **البريد الإلكتروني للدعم (Support Email)**:
```text
support@intaleqapp.com
```
*(أو بريد المؤسس: info@intaleqapp.com)*
* **الموقع الإلكتروني (Website)**:
```text
https://intaleqapp.com/hamza.html
```
---
## 5. استبيان تصنيف المحتوى (Content Rating Questionnaire)
عند تعبئة استبيان المحتوى في Google Play Console:
- هل يحتوي التطبيق على محتوى عنيف؟ **لا (No)**
- هل يحتوي على محتوى جنسي؟ **لا (No)**
- هل يحتوي على لغة مسيئة؟ **لا (No)**
- هل يشارك التطبيق الموقع الجغرافي الدقيق للمستخدم مع أطراف خارجية لأغراض إعلانية؟ **لا (No)**
- هل يستخدم التطبيق الموقع لأغراض تقديم خدمات الملاحة والتوجيه للمستخدم؟ **نعم (Yes)**
- **النتيجة التلقائية**: تصنيف **مناسب للجميع (Everyone / PEGI 3)**.
---
## 6. تعليمات الوصول للفاحصين (App Access & Reviewer Instructions)
> **مكان اللصق في Google Play Console**:
> من القائمة الجانبية: **سياسة التطبيق (App content)** ➔ **الوصول إلى التطبيق (App access)** ➔ اختر **"جميع الوظائف أو بعضها خاضع لقيود" (All or some functionality is restricted)** ➔ أضف الإرشادات التالية باللغة الإنجليزية:
### نص الإرشادات بالإنجليزية (لإدخاله في خانة تعليمات فاحصي جوجل):
```text
IMPORTANT NOTICE FOR APP REVIEWERS:
1. Operational Geographic Coverage:
Siro Map operates on an independent, self-hosted regional spatial vector engine. Our vector map tiles and routing servers are currently live specifically for three operational countries in the MENA region:
- Jordan (الأردن)
- Iraq (العراق)
- Egypt (مصر)
If you test the app on a physical device or emulator located outside these three countries (e.g. from the United States or Europe) with local GPS enabled, the camera will center on your local coordinates where our regional map tiles are not rendered, resulting in an empty tile canvas. This is expected behavior due to localized regional hosting.
2. How to Test and Review the App Successfully:
OPTION A (Recommended - Instant In-App Search):
1. Launch the app and grant or dismiss location permissions.
2. In the top search bar, type any landmark or city within our coverage area:
- For Jordan: Type "Amman" or "عمان" or "City Mall"
- For Iraq: Type "Baghdad" or "بغداد" or "Tahrir Square"
- For Egypt: Type "Cairo" or "القاهرة" or "Nasr City"
3. Select any result. The map will instantly fly to the location, load rich vector tiles, 3D buildings, and gates.
4. Tap "Start Navigation" or "ابدأ الملاحة" to experience turn-by-turn routing, Arabic voice guidance, and vehicle markers.
OPTION B (Simulated GPS Location):
In your emulator or testing device, set a mock GPS location to any of our operational hubs:
- Amman (Jordan): Latitude 31.9539, Longitude 35.9106
- Baghdad (Iraq): Latitude 33.3152, Longitude 44.3661
- Cairo (Egypt): Latitude 30.0444, Longitude 31.2357
The app will immediately render full street networks and navigation HUD.
3. Authentication & Credentials:
No username or password is required. The application automatically registers via secure hardware device fingerprinting and provisions a consumer API key seamlessly on initial launch.
```
+17
View File
@@ -0,0 +1,17 @@
# siro_maps
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
+28
View File
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
avoid_print: false
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
@@ -0,0 +1,85 @@
import java.util.Properties
import java.io.FileInputStream
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
namespace = "com.urukmap.app"
compileSdk = flutter.compileSdkVersion
ndkVersion = "28.2.13676358"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
applicationId = "com.urukmap.app"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String?
keyPassword = keystoreProperties["keyPassword"] as String?
val storeFilePath = keystoreProperties["storeFile"] as String?
if (storeFilePath != null) {
storeFile = file(storeFilePath)
}
storePassword = keystoreProperties["storePassword"] as String?
}
}
buildTypes {
release {
val hasReleaseKey = keystorePropertiesFile.exists() && keystoreProperties["storeFile"] != null
signingConfig = if (hasReleaseKey) {
signingConfigs.getByName("release")
} else {
signingConfigs.getByName("debug")
}
// Enable R8 / ProGuard Code Optimization & Shrinking
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
ndk {
debugSymbolLevel = "none"
}
}
debug {
signingConfig = signingConfigs.getByName("debug")
}
}
}
dependencies {
implementation("androidx.car.app:app:1.4.0")
implementation("androidx.car.app:app-projected:1.4.0")
}
flutter {
source = "../.."
}
+75
View File
@@ -0,0 +1,75 @@
# ==============================================================================
# Siro Maps (map-saas) ProGuard & R8 Optimization Rules
# ==============================================================================
# ── 1. FLUTTER EMBEDDING & NATIVE CHANNELS ────────────────────────────────────
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
-keepattributes *Annotation*
-keepattributes SourceFile,LineNumberTable
-keepattributes Signature
-keepattributes InnerClasses,EnclosingMethod
# Keep native methods used by Flutter engine
-keepclasseswithmembers class * {
native <methods>;
}
# ── 2. ANDROID AUTO (CAR APP LIBRARY) ─────────────────────────────────────────
-keep class androidx.car.app.** { *; }
-keep interface androidx.car.app.** { *; }
-keep class androidx.car.app.model.** { *; }
-keep class androidx.car.app.navigation.** { *; }
-keep class androidx.car.app.navigation.model.** { *; }
-keep class androidx.car.app.validation.** { *; }
# Keep our custom CarAppService, Session, Screen and State models
-keep class com.urukmap.app.car.** { *; }
-keepclassmembers class com.urukmap.app.car.** { *; }
-dontwarn androidx.car.app.**
# ── 3. MAPLIBRE GL NATIVE & RENDERING ENGINE ──────────────────────────────────
-keep class org.maplibre.** { *; }
-keep interface org.maplibre.** { *; }
-keep class org.maplibre.android.** { *; }
-keep interface org.maplibre.android.** { *; }
-keep class org.maplibre.android.maps.** { *; }
-keep class org.maplibre.android.geometry.** { *; }
-keep class org.maplibre.android.style.** { *; }
-dontwarn org.maplibre.**
-dontwarn org.maplibre.android.**
# ── 4. SHRED PREFERENCES & DATA MODELS ────────────────────────────────────────
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient <fields>;
!private <fields>;
!private <methods>;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# ── 5. KOTLIN & COROUTINES ────────────────────────────────────────────────────
-dontwarn kotlin.**
-dontwarn kotlinx.coroutines.**
-keep class kotlin.Metadata { *; }
# ── 6. PLAY STORE CORE & DEFERRED COMPONENTS ──────────────────────────────────
-dontwarn com.google.android.play.core.**
-dontwarn com.google.android.play.core.splitcompat.**
-dontwarn com.google.android.play.core.splitinstall.**
-dontwarn com.google.android.play.core.tasks.**
# ── 7. GENERAL DEPENDENCIES ───────────────────────────────────────────────────
-dontwarn javax.annotation.**
-dontwarn org.checkerframework.**
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,107 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions for High-Precision Navigation and Background Audio -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- Android Auto Navigation Permissions required by Google Play Console -->
<uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES"/>
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE"/>
<application
android:label="Siro Map"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:supportsPictureInPicture="true"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Custom URI Scheme: siromaps:// -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="siromaps" />
</intent-filter>
<!-- Standard Geo Scheme: geo:lat,lng -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="geo" />
</intent-filter>
<!-- Google Navigation Scheme: google.navigation:q=lat,lng -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="google.navigation" />
</intent-filter>
<!-- Universal Web Deep Links: maps.siro.app & map-saas.intaleqapp.com -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="maps.siro.app" />
<data android:scheme="https" android:host="map-saas.intaleqapp.com" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<!-- Android Auto Navigation Service -->
<service
android:name=".car.SiroCarAppService"
android:exported="true">
<intent-filter>
<action android:name="androidx.car.app.CarAppService" />
<category android:name="androidx.car.app.category.NAVIGATION"/>
</intent-filter>
</service>
<meta-data
android:name="androidx.car.app.minCarApiLevel"
android:value="1" />
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,208 @@
package com.urukmap.app
import android.app.PictureInPictureParams
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.util.Rational
import com.urukmap.app.car.CarNavigationState
import com.urukmap.app.car.SiroCarAppService
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private val CAR_CHANNEL = "com.siro.siro_maps/car_navigation"
private val PIP_CHANNEL = "com.siro.siro_maps/pip"
private val DEEP_LINK_CHANNEL = "com.siro.siro_maps/deep_link"
private var pipMethodChannel: MethodChannel? = null
private var deepLinkMethodChannel: MethodChannel? = null
private var pendingDeepLink: String? = null
private var isNavigating: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleIntent(intent)
}
private fun handleIntent(intent: Intent?) {
if (intent?.action == Intent.ACTION_VIEW) {
val dataString = intent.dataString
if (!dataString.isNullOrEmpty()) {
pendingDeepLink = dataString
deepLinkMethodChannel?.invokeMethod("onDeepLink", mapOf("url" to dataString))
}
}
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// 1. Car Navigation Channel
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CAR_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"isCarAppConnected" -> {
result.success(SiroCarAppService.isConnected)
}
"updateNavState" -> {
try {
val lat = call.argument<Double>("lat") ?: 0.0
val lng = call.argument<Double>("lng") ?: 0.0
val bearing = call.argument<Double>("bearing") ?: 0.0
val speed = call.argument<Double>("speed") ?: 0.0
val instruction = call.argument<String>("instruction") ?: ""
val distanceToStep = call.argument<Double>("distanceToStep") ?: 0.0
val totalDistance = call.argument<Double>("totalDistance") ?: 0.0
val eta = call.argument<Double>("eta") ?: 0.0
val maneuver = call.argument<Int>("maneuver") ?: 0
val isNav = call.argument<Boolean>("isNavigating") ?: false
val isMapDarkMode = call.argument<Boolean>("isMapDarkMode") ?: false
updateNavigationState(isNav)
val newState = CarNavigationState(
lat = lat,
lng = lng,
bearing = bearing,
speed = speed,
instruction = instruction,
distanceToStep = distanceToStep,
totalDistance = totalDistance,
eta = eta,
maneuver = maneuver,
isNavigating = isNav,
isMapDarkMode = isMapDarkMode
)
SiroCarAppService.updateNavState(newState)
result.success(true)
} catch (e: Exception) {
result.error("UPDATE_FAILED", e.localizedMessage, null)
}
}
"stopNavigation" -> {
updateNavigationState(false)
SiroCarAppService.stopNavigation()
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
// 2. Deep Link Channel
deepLinkMethodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, DEEP_LINK_CHANNEL).apply {
setMethodCallHandler { call, result ->
when (call.method) {
"getInitialLink" -> {
val link = pendingDeepLink
pendingDeepLink = null
result.success(link)
}
else -> result.notImplemented()
}
}
}
// Deliver pending deep link if arrived before engine configuration
pendingDeepLink?.let { link ->
deepLinkMethodChannel?.invokeMethod("onDeepLink", mapOf("url" to link))
}
// 3. Picture-in-Picture (PiP) Channel
pipMethodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, PIP_CHANNEL).apply {
setMethodCallHandler { call, result ->
when (call.method) {
"enterPictureInPicture" -> {
val success = enterPipMode()
result.success(success)
}
"isPipSupported" -> {
val supported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
} else {
false
}
result.success(supported)
}
"setNavigating" -> {
val navigating = call.argument<Boolean>("isNavigating") ?: false
updateNavigationState(navigating)
result.success(true)
}
else -> result.notImplemented()
}
}
}
// 4. Device Hardware Channel
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.siro.siro_maps/device_hardware").setMethodCallHandler { call, result ->
if (call.method == "getHardwareInfo") {
val androidId = android.provider.Settings.Secure.getString(contentResolver, android.provider.Settings.Secure.ANDROID_ID) ?: ""
val info = mapOf(
"hardwareId" to androidId,
"brand" to Build.BRAND,
"manufacturer" to Build.MANUFACTURER,
"model" to Build.MODEL,
"device" to Build.DEVICE,
"board" to Build.BOARD,
"hardware" to Build.HARDWARE
)
result.success(info)
} else {
result.notImplemented()
}
}
}
private fun updateNavigationState(navigating: Boolean) {
isNavigating = navigating
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
try {
val pipParams = PictureInPictureParams.Builder()
.setAutoEnterEnabled(navigating)
.setAspectRatio(Rational(3, 4))
.build()
setPictureInPictureParams(pipParams)
} catch (_: Exception) {}
}
}
private fun enterPipMode(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
return try {
val pipParams = PictureInPictureParams.Builder()
.setAspectRatio(Rational(3, 4))
.build()
enterPictureInPictureMode(pipParams)
} catch (e: Exception) {
false
}
}
}
return false
}
override fun onUserLeaveHint() {
super.onUserLeaveHint()
// Auto-enter PiP on Android 8.0 - 11 when user presses Home while navigating
if (isNavigating && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
enterPipMode()
}
}
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
pipMethodChannel?.invokeMethod("onPipChanged", mapOf("isInPip" to isInPictureInPictureMode))
}
}
@@ -0,0 +1,44 @@
package com.urukmap.app.car
data class CarNavigationState(
val lat: Double = 0.0,
val lng: Double = 0.0,
val bearing: Double = 0.0,
val speed: Double = 0.0,
val instruction: String = "",
val distanceToStep: Double = 0.0,
val totalDistance: Double = 0.0,
val eta: Double = 0.0,
val maneuver: Int = 0,
val isNavigating: Boolean = false,
val isMapDarkMode: Boolean = false
) {
val formattedSpeed: String
get() = "${speed.toInt()} كم/س"
val formattedRemainingDistance: String
get() = if (totalDistance >= 1000) {
String.format("%.1f كم", totalDistance / 1000.0)
} else {
"${totalDistance.toInt()} م"
}
val formattedDistanceToStep: String
get() = if (distanceToStep >= 1000) {
String.format("بعد %.1f كم", distanceToStep / 1000.0)
} else {
"بعد ${distanceToStep.toInt()} م"
}
val formattedRemainingDuration: String
get() {
val minutes = (eta / 60.0).toInt()
return if (minutes >= 60) {
val hours = minutes / 60
val remMin = minutes % 60
"$hours س $remMin د"
} else {
"$minutes دقيقة"
}
}
}
@@ -0,0 +1,41 @@
package com.urukmap.app.car
import androidx.car.app.CarAppService
import androidx.car.app.Session
import androidx.car.app.validation.HostValidator
class SiroCarAppService : CarAppService() {
companion object {
var currentNavState: CarNavigationState = CarNavigationState()
var activeSession: SiroCarSession? = null
fun updateNavState(state: CarNavigationState) {
currentNavState = state
activeSession?.requestScreenUpdate()
}
fun stopNavigation() {
currentNavState = currentNavState.copy(isNavigating = false)
activeSession?.requestScreenUpdate()
}
val isConnected: Boolean
get() = activeSession != null
}
override fun createHostValidator(): HostValidator {
return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
}
override fun onCreateSession(): Session {
val session = SiroCarSession()
activeSession = session
return session
}
override fun onDestroy() {
activeSession = null
super.onDestroy()
}
}
@@ -0,0 +1,23 @@
package com.urukmap.app.car
import android.content.Intent
import androidx.car.app.Screen
import androidx.car.app.Session
class SiroCarSession : Session() {
private var activeScreen: SiroNavScreen? = null
init {
SiroCarAppService.activeSession = this
}
override fun onCreateScreen(intent: Intent): Screen {
val screen = SiroNavScreen(carContext)
activeScreen = screen
return screen
}
fun requestScreenUpdate() {
activeScreen?.invalidate()
}
}
@@ -0,0 +1,95 @@
package com.urukmap.app.car
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.*
class SiroNavScreen(carContext: CarContext) : Screen(carContext) {
override fun onGetTemplate(): Template {
val state = SiroCarAppService.currentNavState
return if (state.isNavigating) {
buildActiveNavTemplate(state)
} else {
buildIdleTemplate(state)
}
}
private fun buildActiveNavTemplate(state: CarNavigationState): Template {
val paneBuilder = Pane.Builder()
// 1. Current instruction & distance
paneBuilder.addRow(
Row.Builder()
.setTitle(state.instruction.ifEmpty { "تابع السير نحو الوجهة" })
.addText(state.formattedDistanceToStep)
.build()
)
// 2. Trip summary (Distance and Duration)
paneBuilder.addRow(
Row.Builder()
.setTitle("المسار المتبقي")
.addText("${state.formattedRemainingDistance} • الوصول خلال ${state.formattedRemainingDuration}")
.build()
)
// 3. Live Speed & Heading
paneBuilder.addRow(
Row.Builder()
.setTitle("السرعة والاتجاه")
.addText("${state.formattedSpeed} • الزاوية ${state.bearing.toInt()}°")
.build()
)
// Header Action Strip for seamless access across landscape, portrait, and Coolwalk multi-window
val actionStrip = ActionStrip.Builder()
.addAction(
Action.Builder()
.setTitle("إنهاء الملاحة")
.setOnClickListener {
SiroCarAppService.stopNavigation()
invalidate()
}
.build()
)
.build()
return PaneTemplate.Builder(paneBuilder.build())
.setTitle("ملاحة سيرو • جارية الآن")
.setHeaderAction(Action.APP_ICON)
.setActionStrip(actionStrip)
.build()
}
private fun buildIdleTemplate(state: CarNavigationState): Template {
val paneBuilder = Pane.Builder()
paneBuilder.addRow(
Row.Builder()
.setTitle("خرائط سيرو السيادية (Siro Maps)")
.addText("متصل بشاشة السيارة • جاهز للتوجيه")
.build()
)
paneBuilder.addRow(
Row.Builder()
.setTitle("وضع القيادة الحر")
.addText("السرعة: ${state.formattedSpeed}")
.build()
)
paneBuilder.addRow(
Row.Builder()
.setTitle("بدء الملاحة")
.addText("حدد وجهتك من تطبيق الهاتف لبدء الملاحة التفاعلية فوراً")
.build()
)
return PaneTemplate.Builder(paneBuilder.build())
.setTitle("خرائط سيرو")
.setHeaderAction(Action.APP_ICON)
.build()
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Some files were not shown because too many files have changed in this diff Show More