2026-04-14-1
This commit is contained in:
@@ -0,0 +1,304 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository, DataSource } from 'typeorm';
|
||||||
|
import { NeighborhoodPoint } from './entities/neighborhood-point.entity';
|
||||||
|
import { NeighborhoodPolygon } from './entities/neighborhood-polygon.entity';
|
||||||
|
import { AdminBoundary } from './entities/admin-boundary.entity';
|
||||||
|
import { PlaceJordan } from './entities/place-jordan.entity';
|
||||||
|
import { PlaceSyria } from './entities/place-syria.entity';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdministrativeLinkingService {
|
||||||
|
private readonly logger = new Logger(AdministrativeLinkingService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private dataSource: DataSource,
|
||||||
|
@InjectRepository(NeighborhoodPoint)
|
||||||
|
private readonly neighborhoodPointRepo: Repository<NeighborhoodPoint>,
|
||||||
|
@InjectRepository(NeighborhoodPolygon)
|
||||||
|
private readonly neighborhoodPolygonRepo: Repository<NeighborhoodPolygon>,
|
||||||
|
@InjectRepository(AdminBoundary)
|
||||||
|
private readonly adminBoundaryRepo: Repository<AdminBoundary>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private readonly overpassMirrors = [
|
||||||
|
'https://overpass-api.de/api/interpreter',
|
||||||
|
'https://overpass.kumi.systems/api/interpreter',
|
||||||
|
'https://z.overpass-api.de/api/interpreter'
|
||||||
|
];
|
||||||
|
|
||||||
|
private async fetchWithRetry(data: string, retries = 3): Promise<any> {
|
||||||
|
for (let i = 0; i <= retries; i++) {
|
||||||
|
const url = this.overpassMirrors[i % this.overpassMirrors.length];
|
||||||
|
try {
|
||||||
|
const response = await axios.post(url, data, {
|
||||||
|
timeout: 45000,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'IntaleqMapBot/1.0',
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`Mirror ${url} failed: ${error.message}. Trying next mirror...`);
|
||||||
|
if (i === retries) throw error;
|
||||||
|
await new Promise(res => setTimeout(res, 2000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 1: Sync Neighborhood Points from OSM for a given BBox
|
||||||
|
*/
|
||||||
|
async syncOsmNeighborhoodPoints(bbox: string = '31.86,35.94,32.22,36.25') {
|
||||||
|
this.logger.log(`Syncing neighborhood points for bbox: ${bbox}`);
|
||||||
|
const query = `[out:json][timeout:900];node["place"~"neighbourhood|suburb|town"](${bbox});out;`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await this.fetchWithRetry(`data=${encodeURIComponent(query)}`);
|
||||||
|
const elements = data.elements;
|
||||||
|
this.logger.log(`Found ${elements.length} points in OSM.`);
|
||||||
|
|
||||||
|
for (const e of elements) {
|
||||||
|
const nameAr = e.tags['name:ar'] || e.tags['name'];
|
||||||
|
const nameEn = e.tags['name:en'] || e.tags['name'];
|
||||||
|
const geometry = { type: 'Point', coordinates: [e.lon, e.lat] };
|
||||||
|
|
||||||
|
await this.dataSource.query(`
|
||||||
|
INSERT INTO neighborhood_points (osm_id, name_ar, name_en, place_type, geometry)
|
||||||
|
VALUES ($1, $2, $3, $4, ST_SetSRID(ST_GeomFromGeoJSON($5), 4326))
|
||||||
|
ON CONFLICT (osm_id) DO UPDATE SET
|
||||||
|
name_ar = EXCLUDED.name_ar,
|
||||||
|
name_en = EXCLUDED.name_en,
|
||||||
|
place_type = EXCLUDED.place_type,
|
||||||
|
geometry = EXCLUDED.geometry;
|
||||||
|
`, [e.id, nameAr, nameEn, e.tags['place'], JSON.stringify(geometry)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link points to parent districts using containment or nearest neighbor
|
||||||
|
await this.dataSource.query(`
|
||||||
|
UPDATE neighborhood_points np
|
||||||
|
SET district_id = COALESCE(
|
||||||
|
(
|
||||||
|
SELECT id FROM admin_boundaries ab
|
||||||
|
WHERE ab.admin_level IN (6, 8)
|
||||||
|
AND ST_Contains(ab.geom::geometry, np.geometry::geometry)
|
||||||
|
ORDER BY ab.admin_level DESC
|
||||||
|
LIMIT 1
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SELECT id FROM admin_boundaries ab
|
||||||
|
WHERE ab.admin_level IN (6, 8)
|
||||||
|
ORDER BY ab.geom::geometry <-> np.geometry::geometry
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
WHERE district_id IS NULL;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Diagnostics
|
||||||
|
const totalPoints = await this.dataSource.query(`SELECT count(*) as cnt FROM neighborhood_points`);
|
||||||
|
const linkedPoints = await this.dataSource.query(`SELECT count(*) as cnt FROM neighborhood_points WHERE district_id IS NOT NULL`);
|
||||||
|
const unlinkedPoints = await this.dataSource.query(`SELECT count(*) as cnt FROM neighborhood_points WHERE district_id IS NULL`);
|
||||||
|
const adminCount = await this.dataSource.query(`SELECT admin_level, count(*) as cnt FROM admin_boundaries GROUP BY admin_level ORDER BY admin_level`);
|
||||||
|
|
||||||
|
const diagnostics = {
|
||||||
|
osm_fetched: elements.length,
|
||||||
|
total_points_in_db: parseInt(totalPoints[0].cnt),
|
||||||
|
linked_to_district: parseInt(linkedPoints[0].cnt),
|
||||||
|
unlinked: parseInt(unlinkedPoints[0].cnt),
|
||||||
|
admin_boundaries: adminCount.map(r => ({ level: r.admin_level, count: parseInt(r.cnt) })),
|
||||||
|
};
|
||||||
|
this.logger.log(`Step 1 diagnostics: ${JSON.stringify(diagnostics)}`);
|
||||||
|
return diagnostics;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to sync OSM points: ${error.message}`, error.stack);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 2: Generate Voronoi Polygons for Neighborhoods
|
||||||
|
*/
|
||||||
|
async generateVoronoiNeighborhoods() {
|
||||||
|
this.logger.log('Generating Voronoi polygons for neighborhoods...');
|
||||||
|
|
||||||
|
// Pre-flight diagnostics
|
||||||
|
const prePoints = await this.dataSource.query(`SELECT count(*) as cnt FROM neighborhood_points WHERE district_id IS NOT NULL`);
|
||||||
|
const preDistricts = await this.dataSource.query(`SELECT district_id, count(*) as cnt FROM neighborhood_points WHERE district_id IS NOT NULL GROUP BY district_id`);
|
||||||
|
this.logger.log(`Pre-flight: ${prePoints[0].cnt} points across ${preDistricts.length} districts`);
|
||||||
|
|
||||||
|
// Log a sample to verify geometry exists
|
||||||
|
const samplePoints = await this.dataSource.query(`SELECT osm_id, name_ar, district_id, ST_AsText(geometry) as geom_text FROM neighborhood_points WHERE district_id IS NOT NULL LIMIT 3`);
|
||||||
|
this.logger.log(`Sample points: ${JSON.stringify(samplePoints)}`);
|
||||||
|
|
||||||
|
const sampleDistricts = await this.dataSource.query(`SELECT id, name_ar, admin_level, ST_AsText(ST_Centroid(geom)) as centroid FROM admin_boundaries WHERE admin_level IN (6, 8) LIMIT 3`);
|
||||||
|
this.logger.log(`Sample districts: ${JSON.stringify(sampleDistricts)}`);
|
||||||
|
|
||||||
|
// Delete old voronoi polygons
|
||||||
|
await this.dataSource.query(`DELETE FROM neighborhood_polygons WHERE method = 'voronoi'`);
|
||||||
|
this.logger.log('Deleted old voronoi polygons.');
|
||||||
|
|
||||||
|
// Generate Voronoi per district - do it one district at a time to avoid silent failures
|
||||||
|
let totalInserted = 0;
|
||||||
|
for (const row of preDistricts) {
|
||||||
|
const districtId = row.district_id;
|
||||||
|
const pointCount = parseInt(row.cnt);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (pointCount >= 2) {
|
||||||
|
// Normal Voronoi for 2+ points
|
||||||
|
const result = await this.dataSource.query(`
|
||||||
|
INSERT INTO neighborhood_polygons (osm_id, name_ar, name_en, place_type, parent_id, method, geometry)
|
||||||
|
WITH voronoi_cells AS (
|
||||||
|
SELECT (ST_Dump(ST_VoronoiPolygons(
|
||||||
|
ST_Collect(np.geometry::geometry),
|
||||||
|
0.00001,
|
||||||
|
d.geom::geometry
|
||||||
|
))).geom as cell
|
||||||
|
FROM neighborhood_points np
|
||||||
|
JOIN admin_boundaries d ON d.id = np.district_id
|
||||||
|
WHERE np.district_id = $1
|
||||||
|
GROUP BY d.geom
|
||||||
|
),
|
||||||
|
matched AS (
|
||||||
|
SELECT
|
||||||
|
np.osm_id, np.name_ar, np.name_en, np.place_type, np.district_id,
|
||||||
|
ST_Multi(ST_Intersection(vc.cell::geometry, d.geom::geometry)) as geometry
|
||||||
|
FROM voronoi_cells vc
|
||||||
|
CROSS JOIN LATERAL (
|
||||||
|
SELECT np2.*
|
||||||
|
FROM neighborhood_points np2
|
||||||
|
WHERE np2.district_id = $1
|
||||||
|
ORDER BY np2.geometry::geometry <-> vc.cell::geometry
|
||||||
|
LIMIT 1
|
||||||
|
) np
|
||||||
|
JOIN admin_boundaries d ON d.id = $1
|
||||||
|
)
|
||||||
|
SELECT osm_id, name_ar, name_en, place_type, district_id, 'voronoi', geometry
|
||||||
|
FROM matched
|
||||||
|
WHERE ST_IsValid(geometry) AND NOT ST_IsEmpty(geometry)
|
||||||
|
`, [districtId]);
|
||||||
|
|
||||||
|
totalInserted += (result?.length || result?.[1] || 0);
|
||||||
|
this.logger.log(`District ${districtId}: ${pointCount} points -> ${result?.length || result?.[1] || '?'} polygons`);
|
||||||
|
} else if (pointCount === 1) {
|
||||||
|
// Single point: assign entire district boundary
|
||||||
|
const result = await this.dataSource.query(`
|
||||||
|
INSERT INTO neighborhood_polygons (osm_id, name_ar, name_en, place_type, parent_id, method, geometry)
|
||||||
|
SELECT np.osm_id, np.name_ar, np.name_en, np.place_type, np.district_id, 'voronoi', ST_Multi(d.geom::geometry)
|
||||||
|
FROM neighborhood_points np
|
||||||
|
JOIN admin_boundaries d ON d.id = np.district_id
|
||||||
|
WHERE np.district_id = $1
|
||||||
|
`, [districtId]);
|
||||||
|
|
||||||
|
totalInserted += (result?.length || result?.[1] || 0);
|
||||||
|
this.logger.log(`District ${districtId}: 1 point -> assigned district boundary`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`Voronoi FAILED for district ${districtId} (${pointCount} points): ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post-flight count
|
||||||
|
const postCount = await this.dataSource.query(`SELECT count(*) as cnt FROM neighborhood_polygons`);
|
||||||
|
const finalCount = parseInt(postCount[0].cnt);
|
||||||
|
this.logger.log(`Step 2 complete: ${finalCount} total polygons in neighborhood_polygons`);
|
||||||
|
|
||||||
|
// Sample polygon names
|
||||||
|
const samplePolygons = await this.dataSource.query(`SELECT id, osm_id, name_ar, parent_id, method FROM neighborhood_polygons LIMIT 5`);
|
||||||
|
this.logger.log(`Sample polygons: ${JSON.stringify(samplePolygons)}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: finalCount > 0 ? 'success' : 'WARNING_EMPTY',
|
||||||
|
total_polygons: finalCount,
|
||||||
|
districts_processed: preDistricts.length,
|
||||||
|
sample: samplePolygons
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 3: Link all Places (Jordan & Syria) to the full administrative hierarchy
|
||||||
|
*/
|
||||||
|
async linkPlaces(country: 'jordan' | 'syria') {
|
||||||
|
const tableName = country === 'jordan' ? 'places_jordan' : 'places_syria';
|
||||||
|
|
||||||
|
// Pre-flight diagnostics
|
||||||
|
const polyCount = await this.dataSource.query(`SELECT count(*) as cnt FROM neighborhood_polygons`);
|
||||||
|
const placeCount = await this.dataSource.query(`SELECT count(*) as cnt FROM ${tableName} WHERE location IS NOT NULL`);
|
||||||
|
this.logger.log(`linkPlaces pre-flight: ${polyCount[0].cnt} polygons, ${placeCount[0].cnt} places with location in ${tableName}`);
|
||||||
|
|
||||||
|
if (parseInt(polyCount[0].cnt) === 0) {
|
||||||
|
this.logger.error('ABORT: neighborhood_polygons is EMPTY. Run generate-voronoi first!');
|
||||||
|
return {
|
||||||
|
status: 'FAILED',
|
||||||
|
error: 'neighborhood_polygons table is empty. Run generate-voronoi first.',
|
||||||
|
polygons: 0,
|
||||||
|
places: parseInt(placeCount[0].cnt)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Linking administrative hierarchy for ${tableName} using KNN...`);
|
||||||
|
|
||||||
|
// Update in smaller batches to avoid timeout and track progress
|
||||||
|
const updateResult = await this.dataSource.query(`
|
||||||
|
UPDATE ${tableName} p
|
||||||
|
SET
|
||||||
|
neighborhood_id = (
|
||||||
|
SELECT np.id FROM neighborhood_polygons np
|
||||||
|
ORDER BY p.location::geometry <-> np.geometry::geometry
|
||||||
|
LIMIT 1
|
||||||
|
),
|
||||||
|
sub_district_id = (
|
||||||
|
SELECT ab.id FROM admin_boundaries ab
|
||||||
|
WHERE ab.admin_level = 8
|
||||||
|
AND ST_Within(p.location::geometry, ab.geom::geometry)
|
||||||
|
LIMIT 1
|
||||||
|
),
|
||||||
|
district_id = (
|
||||||
|
SELECT ab.id FROM admin_boundaries ab
|
||||||
|
WHERE ab.admin_level = 6
|
||||||
|
AND ST_Within(p.location::geometry, ab.geom::geometry)
|
||||||
|
LIMIT 1
|
||||||
|
),
|
||||||
|
governorate_id = (
|
||||||
|
SELECT ab.id FROM admin_boundaries ab
|
||||||
|
WHERE ab.admin_level = 4
|
||||||
|
AND ST_Within(p.location::geometry, ab.geom::geometry)
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE p.location IS NOT NULL;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Post-flight diagnostics
|
||||||
|
const linkedNeighborhood = await this.dataSource.query(`SELECT count(*) as cnt FROM ${tableName} WHERE neighborhood_id IS NOT NULL`);
|
||||||
|
const linkedDistrict = await this.dataSource.query(`SELECT count(*) as cnt FROM ${tableName} WHERE district_id IS NOT NULL`);
|
||||||
|
const linkedGov = await this.dataSource.query(`SELECT count(*) as cnt FROM ${tableName} WHERE governorate_id IS NOT NULL`);
|
||||||
|
const totalPlaces = await this.dataSource.query(`SELECT count(*) as cnt FROM ${tableName}`);
|
||||||
|
|
||||||
|
// Sample a specific place to verify
|
||||||
|
const samplePlace = await this.dataSource.query(`
|
||||||
|
SELECT p.id, p.name_ar, p.neighborhood_id, p.district_id, p.governorate_id,
|
||||||
|
n.name_ar as neighborhood_name
|
||||||
|
FROM ${tableName} p
|
||||||
|
LEFT JOIN neighborhood_polygons n ON p.neighborhood_id = n.id
|
||||||
|
WHERE p.name_ar LIKE '%معصوم%'
|
||||||
|
LIMIT 3
|
||||||
|
`);
|
||||||
|
|
||||||
|
const diagnostics = {
|
||||||
|
status: 'completed',
|
||||||
|
country,
|
||||||
|
total_places: parseInt(totalPlaces[0].cnt),
|
||||||
|
linked_neighborhood: parseInt(linkedNeighborhood[0].cnt),
|
||||||
|
linked_district: parseInt(linkedDistrict[0].cnt),
|
||||||
|
linked_governorate: parseInt(linkedGov[0].cnt),
|
||||||
|
sample_masoum: samplePlace,
|
||||||
|
rows_affected: updateResult?.[1] || 'unknown'
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logger.log(`Step 3 diagnostics: ${JSON.stringify(diagnostics)}`);
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,17 +48,17 @@ export abstract class BasePlace {
|
|||||||
|
|
||||||
@Column({ type: 'int', nullable: true })
|
@Column({ type: 'int', nullable: true })
|
||||||
@Index()
|
@Index()
|
||||||
admin_level4_id: number;
|
governorate_id: number;
|
||||||
|
|
||||||
@Column({ type: 'int', nullable: true })
|
@Column({ type: 'int', nullable: true })
|
||||||
@Index()
|
@Index()
|
||||||
admin_level6_id: number;
|
district_id: number;
|
||||||
|
|
||||||
@Column({ type: 'int', nullable: true })
|
@Column({ type: 'int', nullable: true })
|
||||||
@Index()
|
@Index()
|
||||||
admin_level8_id: number;
|
sub_district_id: number;
|
||||||
|
|
||||||
@Column({ type: 'int', nullable: true })
|
@Column({ type: 'int', nullable: true })
|
||||||
@Index()
|
@Index()
|
||||||
admin_level10_id: number;
|
neighborhood_id: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('neighborhood_points')
|
||||||
|
export class NeighborhoodPoint {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ type: 'bigint', unique: true, nullable: true })
|
||||||
|
@Index()
|
||||||
|
osm_id: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@Index()
|
||||||
|
name_ar: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
name_en: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
place_type: string;
|
||||||
|
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
@Index()
|
||||||
|
district_id: number;
|
||||||
|
|
||||||
|
@Column({ type: 'geometry', spatialFeatureType: 'Point', srid: 4326 })
|
||||||
|
@Index({ spatial: true })
|
||||||
|
geometry: any;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('neighborhood_polygons')
|
||||||
|
export class NeighborhoodPolygon {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ type: 'bigint', nullable: true })
|
||||||
|
@Index()
|
||||||
|
osm_id: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@Index()
|
||||||
|
name_ar: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
name_en: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
place_type: string;
|
||||||
|
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
@Index()
|
||||||
|
parent_id: number; // Links to admin_boundaries (District/Sub-district)
|
||||||
|
|
||||||
|
@Column({ default: 'voronoi' })
|
||||||
|
method: string;
|
||||||
|
|
||||||
|
@Column({ type: 'float', default: 0.7 })
|
||||||
|
confidence: number;
|
||||||
|
|
||||||
|
@Column({ type: 'geometry', spatialFeatureType: 'MultiPolygon', srid: 4326 })
|
||||||
|
@Index({ spatial: true })
|
||||||
|
geometry: any;
|
||||||
|
}
|
||||||
@@ -19,6 +19,28 @@ export class GeocodingInitService implements OnModuleInit {
|
|||||||
await this.repo.query('CREATE EXTENSION IF NOT EXISTS postgis;');
|
await this.repo.query('CREATE EXTENSION IF NOT EXISTS postgis;');
|
||||||
await this.repo.query('CREATE EXTENSION IF NOT EXISTS pg_trgm;');
|
await this.repo.query('CREATE EXTENSION IF NOT EXISTS pg_trgm;');
|
||||||
|
|
||||||
|
// Migration: Rename old admin_level columns to descriptive names if they exist
|
||||||
|
const tables = ['places_jordan', 'places_syria', 'places_egypt'];
|
||||||
|
const columnMapping = [
|
||||||
|
{ old: 'admin_level4_id', new: 'governorate_id' },
|
||||||
|
{ old: 'admin_level6_id', new: 'district_id' },
|
||||||
|
{ old: 'admin_level8_id', new: 'sub_district_id' },
|
||||||
|
{ old: 'admin_level10_id', new: 'neighborhood_id' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const table of tables) {
|
||||||
|
for (const mapping of columnMapping) {
|
||||||
|
await this.repo.query(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = '${table}' AND column_name = '${mapping.old}') THEN
|
||||||
|
ALTER TABLE ${table} RENAME COLUMN ${mapping.old} TO ${mapping.new};
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup: Create a trigger to automatically update 'location' when lat/lng changes
|
// Cleanup: Create a trigger to automatically update 'location' when lat/lng changes
|
||||||
await this.repo.query(`
|
await this.repo.query(`
|
||||||
CREATE OR REPLACE FUNCTION sync_place_location() RETURNS trigger AS $$
|
CREATE OR REPLACE FUNCTION sync_place_location() RETURNS trigger AS $$
|
||||||
@@ -26,10 +48,13 @@ export class GeocodingInitService implements OnModuleInit {
|
|||||||
IF NEW.latitude IS NOT NULL AND NEW.longitude IS NOT NULL THEN
|
IF NEW.latitude IS NOT NULL AND NEW.longitude IS NOT NULL THEN
|
||||||
NEW.location := ST_SetSRID(ST_MakePoint(CAST(NEW.longitude AS FLOAT), CAST(NEW.latitude AS FLOAT)), 4326);
|
NEW.location := ST_SetSRID(ST_MakePoint(CAST(NEW.longitude AS FLOAT), CAST(NEW.latitude AS FLOAT)), 4326);
|
||||||
|
|
||||||
NEW.admin_level4_id := (SELECT id FROM admin_boundaries WHERE admin_level = 4 AND ST_Contains(geom, NEW.location) LIMIT 1);
|
-- Keep standard admin boundary logic for higher levels
|
||||||
NEW.admin_level6_id := (SELECT id FROM admin_boundaries WHERE admin_level = 6 AND ST_Contains(geom, NEW.location) LIMIT 1);
|
NEW.governorate_id := (SELECT id FROM admin_boundaries WHERE admin_level = 4 AND ST_Contains(geom, NEW.location) LIMIT 1);
|
||||||
NEW.admin_level8_id := (SELECT id FROM admin_boundaries WHERE admin_level = 8 AND ST_Contains(geom, NEW.location) LIMIT 1);
|
NEW.district_id := (SELECT id FROM admin_boundaries WHERE admin_level = 6 AND ST_Contains(geom, NEW.location) LIMIT 1);
|
||||||
NEW.admin_level10_id := (SELECT id FROM admin_boundaries WHERE admin_level = 10 AND ST_Contains(geom, NEW.location) LIMIT 1);
|
NEW.sub_district_id := (SELECT id FROM admin_boundaries WHERE admin_level = 8 AND ST_Contains(geom, NEW.location) LIMIT 1);
|
||||||
|
|
||||||
|
-- ROOT CAUSE FIX: Use the new Voronoi polygons for neighborhoods!
|
||||||
|
NEW.neighborhood_id := (SELECT id FROM neighborhood_polygons ORDER BY NEW.location::geometry <-> geometry::geometry LIMIT 1);
|
||||||
END IF;
|
END IF;
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END;
|
END;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { Controller, Get, Post, Delete, Body, Query, UseGuards, HttpException, H
|
|||||||
import { ApiTags, ApiOperation, ApiQuery } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiQuery } from '@nestjs/swagger';
|
||||||
import { GeocodingService } from './geocoding.service';
|
import { GeocodingService } from './geocoding.service';
|
||||||
import { AdminBoundariesService } from './admin-boundaries.service';
|
import { AdminBoundariesService } from './admin-boundaries.service';
|
||||||
|
import { JordanResearchService } from './jordan-research.service';
|
||||||
|
import { AdministrativeLinkingService } from './administrative-linking.service';
|
||||||
import { ApiKeyGuard } from '../common/guards/api-key.guard';
|
import { ApiKeyGuard } from '../common/guards/api-key.guard';
|
||||||
|
|
||||||
@ApiTags('geocoding')
|
@ApiTags('geocoding')
|
||||||
@@ -10,6 +12,8 @@ export class GeocodingController {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly geocodingService: GeocodingService,
|
private readonly geocodingService: GeocodingService,
|
||||||
private readonly adminBoundariesService: AdminBoundariesService,
|
private readonly adminBoundariesService: AdminBoundariesService,
|
||||||
|
private readonly jordanResearchService: JordanResearchService,
|
||||||
|
private readonly adminLinkingService: AdministrativeLinkingService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('search')
|
@Get('search')
|
||||||
@@ -120,4 +124,33 @@ export class GeocodingController {
|
|||||||
) {
|
) {
|
||||||
return this.adminBoundariesService.importFromFile(country, filePath);
|
return this.adminBoundariesService.importFromFile(country, filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('research/zarqa')
|
||||||
|
@ApiOperation({ summary: 'Generate a research report for Zarqa, Jordan (Sample Data)' })
|
||||||
|
async zarqaResearch() {
|
||||||
|
return this.jordanResearchService.generateZarqaReport();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('admin/sync-neighborhoods')
|
||||||
|
@UseGuards(ApiKeyGuard)
|
||||||
|
@ApiOperation({ summary: 'Sync neighborhood points from OSM for a bbox' })
|
||||||
|
@ApiQuery({ name: 'bbox', required: false })
|
||||||
|
async syncNeighborhoods(@Query('bbox') bbox?: string) {
|
||||||
|
return this.adminLinkingService.syncOsmNeighborhoodPoints(bbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('admin/generate-voronoi')
|
||||||
|
@UseGuards(ApiKeyGuard)
|
||||||
|
@ApiOperation({ summary: 'Generate Voronoi polygons for neighborhoods' })
|
||||||
|
async generateVoronoi() {
|
||||||
|
return this.adminLinkingService.generateVoronoiNeighborhoods();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('admin/link-places')
|
||||||
|
@UseGuards(ApiKeyGuard)
|
||||||
|
@ApiOperation({ summary: 'Link places to administrative hierarchy' })
|
||||||
|
@ApiQuery({ name: 'country', required: true, enum: ['jordan', 'syria'] })
|
||||||
|
async linkPlaces(@Query('country') country: 'jordan' | 'syria') {
|
||||||
|
return this.adminLinkingService.linkPlaces(country);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import { OsmArea } from './entities/osm-area.entity';
|
|||||||
import { OsmPointWithArea } from './entities/osm-point-with-area.entity';
|
import { OsmPointWithArea } from './entities/osm-point-with-area.entity';
|
||||||
import { AdminBoundary } from './entities/admin-boundary.entity';
|
import { AdminBoundary } from './entities/admin-boundary.entity';
|
||||||
import { AdminBoundariesService } from './admin-boundaries.service';
|
import { AdminBoundariesService } from './admin-boundaries.service';
|
||||||
|
import { JordanResearchService } from './jordan-research.service';
|
||||||
|
import { AdministrativeLinkingService } from './administrative-linking.service';
|
||||||
|
import { NeighborhoodPoint } from './entities/neighborhood-point.entity';
|
||||||
|
import { NeighborhoodPolygon } from './entities/neighborhood-polygon.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([
|
TypeOrmModule.forFeature([
|
||||||
@@ -18,10 +23,18 @@ import { AdminBoundariesService } from './admin-boundaries.service';
|
|||||||
PlaceEgypt,
|
PlaceEgypt,
|
||||||
OsmArea,
|
OsmArea,
|
||||||
OsmPointWithArea,
|
OsmPointWithArea,
|
||||||
AdminBoundary
|
AdminBoundary,
|
||||||
|
NeighborhoodPoint,
|
||||||
|
NeighborhoodPolygon
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
controllers: [GeocodingController],
|
controllers: [GeocodingController],
|
||||||
providers: [GeocodingService, GeocodingInitService, AdminBoundariesService],
|
providers: [
|
||||||
|
GeocodingService,
|
||||||
|
GeocodingInitService,
|
||||||
|
AdminBoundariesService,
|
||||||
|
JordanResearchService,
|
||||||
|
AdministrativeLinkingService
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class GeocodingModule {}
|
export class GeocodingModule {}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export class GeocodingService {
|
|||||||
const userQuery = `
|
const userQuery = `
|
||||||
SELECT
|
SELECT
|
||||||
p.id, p.name, p.name_ar, p.name_en, p.category,
|
p.id, p.name, p.name_ar, p.name_en, p.category,
|
||||||
|
p.neighborhood_id as db_neighborhood_id,
|
||||||
p.neighbourhood as original_neighbourhood,
|
p.neighbourhood as original_neighbourhood,
|
||||||
n.name_ar as neighbourhood,
|
n.name_ar as neighbourhood,
|
||||||
d.name_ar as district,
|
d.name_ar as district,
|
||||||
@@ -77,9 +78,9 @@ export class GeocodingService {
|
|||||||
CASE WHEN $2::float IS NOT NULL THEN ST_DistanceSphere(p.location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) ELSE 0 END as distance,
|
CASE WHEN $2::float IS NOT NULL THEN ST_DistanceSphere(p.location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) ELSE 0 END as distance,
|
||||||
similarity(COALESCE(p.name_ar, ''), $1) + similarity(COALESCE(p.name, ''), $1) + similarity(COALESCE(p.neighbourhood, ''), $1) as relevance
|
similarity(COALESCE(p.name_ar, ''), $1) + similarity(COALESCE(p.name, ''), $1) + similarity(COALESCE(p.neighbourhood, ''), $1) as relevance
|
||||||
FROM ${tableName} p
|
FROM ${tableName} p
|
||||||
LEFT JOIN admin_boundaries n ON p.admin_level10_id = n.id
|
LEFT JOIN neighborhood_polygons n ON p.neighborhood_id = n.id
|
||||||
LEFT JOIN admin_boundaries d ON p.admin_level8_id = d.id
|
LEFT JOIN admin_boundaries d ON p.sub_district_id = d.id
|
||||||
LEFT JOIN admin_boundaries g ON p.admin_level4_id = g.id
|
LEFT JOIN admin_boundaries g ON p.governorate_id = g.id
|
||||||
WHERE (p.name_ar % $1 OR p.name % $1 OR p.neighbourhood % $1 OR p.name_ar ILIKE $4 OR p.name ILIKE $4 OR p.neighbourhood ILIKE $4)
|
WHERE (p.name_ar % $1 OR p.name % $1 OR p.neighbourhood % $1 OR p.name_ar ILIKE $4 OR p.name ILIKE $4 OR p.neighbourhood ILIKE $4)
|
||||||
${hasLocation ? `AND (p.location && ST_Expand(ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326), $5::float) OR ST_DistanceSphere(p.location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) <= $6::float)` : ''}
|
${hasLocation ? `AND (p.location && ST_Expand(ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326), $5::float) OR ST_DistanceSphere(p.location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) <= $6::float)` : ''}
|
||||||
ORDER BY relevance DESC, distance ASC LIMIT 15
|
ORDER BY relevance DESC, distance ASC LIMIT 15
|
||||||
@@ -107,55 +108,27 @@ export class GeocodingService {
|
|||||||
const osmResults = await this.osmPointsRepository.query(osmQuery, [cleanQuery, lat || null, lon || null, ILikeQuery, radiusInDegrees, radius]);
|
const osmResults = await this.osmPointsRepository.query(osmQuery, [cleanQuery, lat || null, lon || null, ILikeQuery, radiusInDegrees, radius]);
|
||||||
allResults.push(...osmResults);
|
allResults.push(...osmResults);
|
||||||
|
|
||||||
// --- Consolidation: Overture Maps Integration (Now in primary DB) ---
|
// Note: We removed the raw query to 'overture_building' because raw overture tables
|
||||||
const overtureQuery = `
|
// do not have administrative linking, causing empty full_addresses, and
|
||||||
(SELECT id::text,
|
// scanning them with ILIKE without trigram indices causes a 2-second latency spike.
|
||||||
COALESCE(names->>'primary', names->>'common', 'Building') as name,
|
// Overture data is already properly ingested via the Scraper into places_jordan.
|
||||||
COALESCE(names->>'primary', names->>'common', '') as name_ar,
|
|
||||||
NULL as name_en,
|
|
||||||
'building' as category,
|
|
||||||
ST_Y(ST_Centroid(location)) as latitude,
|
|
||||||
ST_X(ST_Centroid(location)) as longitude,
|
|
||||||
'' as address,
|
|
||||||
'overture_buildings' as source,
|
|
||||||
CASE WHEN $2::float IS NOT NULL THEN ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) ELSE 0 END as distance,
|
|
||||||
0.6 as relevance
|
|
||||||
FROM overture_building
|
|
||||||
WHERE (names->>'primary' ILIKE $4 OR names->>'common' ILIKE $4)
|
|
||||||
LIMIT 15)
|
|
||||||
UNION ALL
|
|
||||||
(SELECT id::text,
|
|
||||||
COALESCE(names->>'primary', names->>'common', 'Street') as name,
|
|
||||||
COALESCE(names->>'primary', names->>'common', '') as name_ar,
|
|
||||||
NULL as name_en,
|
|
||||||
'transportation' as category,
|
|
||||||
ST_Y(ST_Centroid(location)) as latitude,
|
|
||||||
ST_X(ST_Centroid(location)) as longitude,
|
|
||||||
'' as address,
|
|
||||||
'overture_streets' as source,
|
|
||||||
CASE WHEN $2::float IS NOT NULL THEN ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) ELSE 0 END as distance,
|
|
||||||
0.6 as relevance
|
|
||||||
FROM overture_segment
|
|
||||||
WHERE (names->>'primary' ILIKE $4 OR names->>'common' ILIKE $4)
|
|
||||||
LIMIT 15)
|
|
||||||
`;
|
|
||||||
try {
|
|
||||||
const overtureResults = await this.osmPointsRepository.query(overtureQuery, [cleanQuery, lat || null, lon || null, ILikeQuery]);
|
|
||||||
allResults.push(...overtureResults);
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.warn('Overture tables search failed, logging error.', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedResults = allResults
|
const sortedResults = allResults
|
||||||
.sort((a, b) => hasLocation ? ((a.distance - b.distance) || (b.relevance - a.relevance)) : ((b.relevance - a.relevance) || (a.distance - b.distance)))
|
.sort((a, b) => hasLocation ? ((a.distance - b.distance) || (b.relevance - a.relevance)) : ((b.relevance - a.relevance) || (a.distance - b.distance)))
|
||||||
.slice(0, 25)
|
.slice(0, 25)
|
||||||
.map(r => ({
|
.map(r => {
|
||||||
...r,
|
// Build full administrative address from admin_boundaries
|
||||||
latitude: parseFloat(r.latitude),
|
const addressParts = [r.neighbourhood, r.district, r.governorate].filter(Boolean);
|
||||||
longitude: parseFloat(r.longitude),
|
const full_address = addressParts.length > 0 ? addressParts.join('، ') : (r.address || '');
|
||||||
distance_km: r.distance ? (Number(r.distance) / 1000).toFixed(2) : null,
|
return {
|
||||||
location: { lat: parseFloat(r.latitude), lng: parseFloat(r.longitude) },
|
...r,
|
||||||
}));
|
latitude: parseFloat(r.latitude),
|
||||||
|
longitude: parseFloat(r.longitude),
|
||||||
|
distance_km: r.distance ? (Number(r.distance) / 1000).toFixed(2) : null,
|
||||||
|
location: { lat: parseFloat(r.latitude), lng: parseFloat(r.longitude) },
|
||||||
|
full_address,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return { results: sortedResults };
|
return { results: sortedResults };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -177,9 +150,9 @@ export class GeocodingService {
|
|||||||
p.latitude, p.longitude, p.address, 'user_place' as source,
|
p.latitude, p.longitude, p.address, 'user_place' as source,
|
||||||
ST_DistanceSphere(p.location::geometry, ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326)) as distance
|
ST_DistanceSphere(p.location::geometry, ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326)) as distance
|
||||||
FROM ${tableName} p
|
FROM ${tableName} p
|
||||||
LEFT JOIN admin_boundaries n ON p.admin_level10_id = n.id
|
LEFT JOIN neighborhood_polygons n ON p.neighborhood_id = n.id
|
||||||
LEFT JOIN admin_boundaries d ON p.admin_level8_id = d.id
|
LEFT JOIN admin_boundaries d ON p.sub_district_id = d.id
|
||||||
LEFT JOIN admin_boundaries g ON p.admin_level4_id = g.id
|
LEFT JOIN admin_boundaries g ON p.governorate_id = g.id
|
||||||
WHERE p.location IS NOT NULL
|
WHERE p.location IS NOT NULL
|
||||||
ORDER BY p.location::geometry <-> ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326) ASC LIMIT 3
|
ORDER BY p.location::geometry <-> ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326) ASC LIMIT 3
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { PlaceJordan } from './entities/place-jordan.entity';
|
||||||
|
import axios from 'axios';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JordanResearchService {
|
||||||
|
private readonly logger = new Logger(JordanResearchService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(PlaceJordan)
|
||||||
|
private readonly placeRepo: Repository<PlaceJordan>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private async fetchWithRetry(url: string, data?: string, method: 'get' | 'post' = 'get', retries = 2) {
|
||||||
|
for (let i = 0; i <= retries; i++) {
|
||||||
|
try {
|
||||||
|
const config = {
|
||||||
|
timeout: 20000,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (compatible; IntaleqMapBot/1.0; +https://intaleq.xyz)',
|
||||||
|
'Accept': 'application/json, application/sparql-results+json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const response = method === 'post'
|
||||||
|
? await axios.post(url, data, config)
|
||||||
|
: await axios.get(url, { ...config, params: data ? { query: data, format: 'json' } : {} });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (i === retries) {
|
||||||
|
this.logger.error(`Failed to fetch from ${url} after ${retries} retries: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
await new Promise(res => setTimeout(res, 2000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source 1: OSM (Overpass API) with Geometries
|
||||||
|
*/
|
||||||
|
async fetchOsmZarqa() {
|
||||||
|
this.logger.log('Fetching Zarqa Geometries from OSM...');
|
||||||
|
const query = `[out:json];(relation["boundary"="administrative"]["admin_level"~"6|8|10"](31.86,35.94,32.22,36.25);node["place"~"neighbourhood|suburb|town"](31.86,35.94,32.22,36.25););out geom;`;
|
||||||
|
try {
|
||||||
|
const data = await this.fetchWithRetry('https://overpass-api.de/api/interpreter', `data=${encodeURIComponent(query)}`, 'post');
|
||||||
|
return data.elements.map(e => ({
|
||||||
|
id: e.id,
|
||||||
|
name_ar: e.tags['name:ar'] || e.tags['name'],
|
||||||
|
type: e.tags['admin_level'] ? `admin_level_${e.tags['admin_level']}` : `place_${e.tags['place']}`,
|
||||||
|
geometry: e.geometry ? e.geometry : (e.lat && e.lon ? { type: 'Point', coordinates: [e.lon, e.lat] } : null)
|
||||||
|
}));
|
||||||
|
} catch (e) { return { error: `OSM Fetch failed: ${e.message}` }; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source 2: Overture (Local DB) - Corrected columns
|
||||||
|
*/
|
||||||
|
async fetchOvertureZarqa() {
|
||||||
|
try {
|
||||||
|
const tableCheck = await this.placeRepo.query("SELECT count(*) FROM information_schema.tables WHERE table_name = 'overture_segment'");
|
||||||
|
if (parseInt(tableCheck[0].count) === 0) return { error: 'table overture_segment does not exist in this database' };
|
||||||
|
|
||||||
|
// Probe columns to avoid 'highway' error
|
||||||
|
const columns = await this.placeRepo.query("SELECT column_name FROM information_schema.columns WHERE table_name = 'overture_segment'");
|
||||||
|
const colList = columns.map(c => c.column_name);
|
||||||
|
const roadClassCol = colList.includes('road_class') ? 'road_class' : (colList.includes('class') ? 'class' : 'NULL');
|
||||||
|
|
||||||
|
return await this.placeRepo.query(`
|
||||||
|
SELECT DISTINCT COALESCE(names->>'primary', names->>'common') as name_ar, ${roadClassCol} as road_class,
|
||||||
|
ST_AsGeoJSON(ST_Centroid(location)) as centroid
|
||||||
|
FROM overture_segment
|
||||||
|
WHERE (names->>'primary' IS NOT NULL OR names->>'common' IS NOT NULL)
|
||||||
|
AND ST_Within(location, ST_MakeEnvelope(35.94, 31.86, 36.25, 32.22, 4326))
|
||||||
|
LIMIT 10
|
||||||
|
`);
|
||||||
|
} catch (e) { return { error: `Overture query failed: ${e.message}` }; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source 3: Wikidata (SPARQL) with User-Agent
|
||||||
|
*/
|
||||||
|
async fetchWikidataZarqa() {
|
||||||
|
const sparql = `SELECT ?item ?itemLabel WHERE { ?item wdt:P131 wd:Q231710. SERVICE wikibase:label { bd:serviceParam wikibase:language "ar,en". } } LIMIT 20`;
|
||||||
|
try {
|
||||||
|
const data = await this.fetchWithRetry('https://query.wikidata.org/sparql', sparql, 'get');
|
||||||
|
return data.results.bindings.map(b => ({ id: b.item.value.split('/').pop(), name_ar: b.itemLabel.value }));
|
||||||
|
} catch (e) { return { error: `Wikidata Fetch failed: ${e.message}` }; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source 4 & 5: Static Files (HDX & GADM)
|
||||||
|
*/
|
||||||
|
async checkStaticFiles() {
|
||||||
|
// Check various common paths from overture_ingest script or data directory
|
||||||
|
const paths = ['/data/infrastructure/osm-data/', './data/', './infrastructure/osm-data/'];
|
||||||
|
const files = ['jor_adm2_geoboundaries.geojson', 'gadm41_JOR_2.json'];
|
||||||
|
const results = {};
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
let found = false;
|
||||||
|
for (const p of paths) {
|
||||||
|
if (fs.existsSync(path.join(p, file))) {
|
||||||
|
results[file] = `Found at ${p}`;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) results[file] = 'Not Found locally - Check if overture_ingest.sh was run for division_area';
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateZarqaReport() {
|
||||||
|
const [osm, overture, wikidata, staticFiles] = await Promise.all([
|
||||||
|
this.fetchOsmZarqa(),
|
||||||
|
this.fetchOvertureZarqa(),
|
||||||
|
this.fetchWikidataZarqa(),
|
||||||
|
this.checkStaticFiles()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
area: 'Zarqa Governorate, Jordan',
|
||||||
|
report_timestamp: new Date().toISOString(),
|
||||||
|
sources: {
|
||||||
|
osm,
|
||||||
|
overture,
|
||||||
|
wikidata,
|
||||||
|
static_files: staticFiles,
|
||||||
|
hdx: "Requires GeoBoundaries GeoJSON for Level 2 (Districts)",
|
||||||
|
gadm: "Requires GADM v4.1 for Level 1-2"
|
||||||
|
},
|
||||||
|
comparison_summary: "Multi-source research enabled with geometries. OSM provides precise neighborhood centroids and boundaries where available. Use the provided centroids to visualize Zarqa subunits."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -294,6 +294,11 @@
|
|||||||
<input type="checkbox" id="toggle-pois" checked onchange="updateLayers()" style="accent-color: var(--primary); width: 16px; height: 16px;">
|
<input type="checkbox" id="toggle-pois" checked onchange="updateLayers()" style="accent-color: var(--primary); width: 16px; height: 16px;">
|
||||||
<span>عرض المعالم / Show POIs</span>
|
<span>عرض المعالم / Show POIs</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label style="display: flex; align-items: center; gap: 10px; margin-top: 12px; cursor: pointer; font-size: 13px;">
|
||||||
|
<input type="checkbox" id="toggle-arrows" checked onchange="updateLayers()" style="accent-color: var(--primary); width: 16px; height: 16px;">
|
||||||
|
<span>اتجاهات الشوارع / Street Directions</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- DB NAMES INDICATOR -->
|
<!-- DB NAMES INDICATOR -->
|
||||||
@@ -672,9 +677,15 @@
|
|||||||
function updateLayers() {
|
function updateLayers() {
|
||||||
const show3D = document.getElementById('toggle-3d').checked;
|
const show3D = document.getElementById('toggle-3d').checked;
|
||||||
const showPOIs = document.getElementById('toggle-pois').checked;
|
const showPOIs = document.getElementById('toggle-pois').checked;
|
||||||
|
const showArrows = document.getElementById('toggle-arrows').checked;
|
||||||
|
|
||||||
if (map.getLayer('3d-buildings')) {
|
// Toggle 3D Overture buildings
|
||||||
map.setLayoutProperty('3d-buildings', 'visibility', show3D ? 'visible' : 'none');
|
if (map.getLayer('building-3d')) {
|
||||||
|
map.setLayoutProperty('building-3d', 'visibility', show3D ? 'visible' : 'none');
|
||||||
|
}
|
||||||
|
// Show flat footprints when 3D is off
|
||||||
|
if (map.getLayer('overture-building-footprint')) {
|
||||||
|
map.setLayoutProperty('overture-building-footprint', 'visibility', show3D ? 'none' : 'visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (map.getLayer('intaleq-db-label')) {
|
if (map.getLayer('intaleq-db-label')) {
|
||||||
@@ -685,6 +696,18 @@
|
|||||||
if (map.getLayer('intaleq_pois')) {
|
if (map.getLayer('intaleq_pois')) {
|
||||||
map.setLayoutProperty('intaleq_pois', 'visibility', showPOIs ? 'visible' : 'none');
|
map.setLayoutProperty('intaleq_pois', 'visibility', showPOIs ? 'visible' : 'none');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Toggle places label layers + Overture name layers
|
||||||
|
['places-syria-labels', 'places-jordan-labels', 'places-egypt-labels', 'overture-building-names', 'overture-street-names'].forEach(id => {
|
||||||
|
if (map.getLayer(id)) {
|
||||||
|
map.setLayoutProperty(id, 'visibility', showPOIs ? 'visible' : 'none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle street direction arrows
|
||||||
|
if (map.getLayer('road-direction-arrows')) {
|
||||||
|
map.setLayoutProperty('road-direction-arrows', 'visibility', showArrows ? 'visible' : 'none');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -42,6 +42,16 @@
|
|||||||
"https://tiles.intaleqapp.com/places_egypt/{z}/{x}/{y}"
|
"https://tiles.intaleqapp.com/places_egypt/{z}/{x}/{y}"
|
||||||
],
|
],
|
||||||
"maxzoom": 14
|
"maxzoom": 14
|
||||||
|
},
|
||||||
|
"overture_buildings": {
|
||||||
|
"type": "vector",
|
||||||
|
"tiles": ["https://tiles.intaleqapp.com/overture_building/{z}/{x}/{y}"],
|
||||||
|
"maxzoom": 14
|
||||||
|
},
|
||||||
|
"overture_segments": {
|
||||||
|
"type": "vector",
|
||||||
|
"tiles": ["https://tiles.intaleqapp.com/overture_segment/{z}/{x}/{y}"],
|
||||||
|
"maxzoom": 14
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"layers": [
|
"layers": [
|
||||||
@@ -909,78 +919,18 @@
|
|||||||
{
|
{
|
||||||
"id": "building-3d",
|
"id": "building-3d",
|
||||||
"type": "fill-extrusion",
|
"type": "fill-extrusion",
|
||||||
"source": "local-osm-polygons",
|
"source": "overture_buildings",
|
||||||
"source-layer": "planet_osm_polygon",
|
"source-layer": "overture_building",
|
||||||
"minzoom": 15,
|
"minzoom": 15,
|
||||||
"filter": [
|
|
||||||
"has",
|
|
||||||
"building"
|
|
||||||
],
|
|
||||||
"paint": {
|
"paint": {
|
||||||
"fill-extrusion-color": [
|
"fill-extrusion-color": "#DDD8D0",
|
||||||
"match",
|
|
||||||
[
|
|
||||||
"get",
|
|
||||||
"building"
|
|
||||||
],
|
|
||||||
"commercial",
|
|
||||||
"#E0D8CC",
|
|
||||||
"retail",
|
|
||||||
"#E8DDD0",
|
|
||||||
"industrial",
|
|
||||||
"#D8DDE4",
|
|
||||||
"church",
|
|
||||||
"#E4DCF0",
|
|
||||||
"mosque",
|
|
||||||
"#D4EAD8",
|
|
||||||
"hospital",
|
|
||||||
"#F0E0E0",
|
|
||||||
"school",
|
|
||||||
"#E8ECD4",
|
|
||||||
"university",
|
|
||||||
"#E4E8D4",
|
|
||||||
"hotel",
|
|
||||||
"#E0E4EE",
|
|
||||||
"apartments",
|
|
||||||
"#E8E4DC",
|
|
||||||
"#E4DFDA"
|
|
||||||
],
|
|
||||||
"fill-extrusion-height": [
|
"fill-extrusion-height": [
|
||||||
"coalesce",
|
"coalesce",
|
||||||
[
|
["to-number", ["get", "height"], 0],
|
||||||
"to-number",
|
["*", ["coalesce", ["to-number", ["get", "num_floors"], null], 3], 3.5],
|
||||||
[
|
|
||||||
"get",
|
|
||||||
"height"
|
|
||||||
],
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"*",
|
|
||||||
[
|
|
||||||
"to-number",
|
|
||||||
[
|
|
||||||
"get",
|
|
||||||
"building:levels"
|
|
||||||
],
|
|
||||||
3
|
|
||||||
],
|
|
||||||
3.5
|
|
||||||
],
|
|
||||||
12
|
12
|
||||||
],
|
],
|
||||||
"fill-extrusion-base": [
|
"fill-extrusion-base": 0,
|
||||||
"coalesce",
|
|
||||||
[
|
|
||||||
"to-number",
|
|
||||||
[
|
|
||||||
"get",
|
|
||||||
"min_height"
|
|
||||||
],
|
|
||||||
0
|
|
||||||
],
|
|
||||||
0
|
|
||||||
],
|
|
||||||
"fill-extrusion-opacity": 0.85,
|
"fill-extrusion-opacity": 0.85,
|
||||||
"fill-extrusion-vertical-gradient": true
|
"fill-extrusion-vertical-gradient": true
|
||||||
}
|
}
|
||||||
|
|||||||
+86
-164
@@ -919,6 +919,35 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "road-direction-arrows",
|
||||||
|
"type": "symbol",
|
||||||
|
"source": "local-osm-lines",
|
||||||
|
"source-layer": "planet_osm_line",
|
||||||
|
"minzoom": 15,
|
||||||
|
"filter": [
|
||||||
|
"all",
|
||||||
|
["in", "highway", "motorway", "trunk", "primary", "secondary", "tertiary", "residential", "unclassified", "living_street"],
|
||||||
|
["==", "oneway", "yes"]
|
||||||
|
],
|
||||||
|
"layout": {
|
||||||
|
"symbol-placement": "line",
|
||||||
|
"symbol-spacing": 80,
|
||||||
|
"text-field": "▸",
|
||||||
|
"text-font": ["Noto Sans Regular"],
|
||||||
|
"text-size": ["interpolate", ["linear"], ["zoom"], 15, 12, 18, 18],
|
||||||
|
"text-keep-upright": false,
|
||||||
|
"text-rotation-alignment": "map",
|
||||||
|
"text-allow-overlap": true,
|
||||||
|
"text-ignore-placement": true,
|
||||||
|
"text-padding": 0
|
||||||
|
},
|
||||||
|
"paint": {
|
||||||
|
"text-color": ["interpolate", ["linear"], ["zoom"], 15, "rgba(100,116,139,0.4)", 18, "rgba(100,116,139,0.7)"],
|
||||||
|
"text-halo-color": "rgba(255,255,255,0.3)",
|
||||||
|
"text-halo-width": 0.5
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "building-fill-flat",
|
"id": "building-fill-flat",
|
||||||
"type": "fill",
|
"type": "fill",
|
||||||
@@ -938,120 +967,43 @@
|
|||||||
{
|
{
|
||||||
"id": "building-3d",
|
"id": "building-3d",
|
||||||
"type": "fill-extrusion",
|
"type": "fill-extrusion",
|
||||||
"source": "local-osm-polygons",
|
"source": "overture_buildings",
|
||||||
"source-layer": "planet_osm_polygon",
|
"source-layer": "overture_building",
|
||||||
"minzoom": 14,
|
"minzoom": 14,
|
||||||
"layout": {
|
"layout": {
|
||||||
"visibility": "none"
|
"visibility": "visible"
|
||||||
},
|
},
|
||||||
"filter": [
|
|
||||||
"has",
|
|
||||||
"building"
|
|
||||||
],
|
|
||||||
"paint": {
|
"paint": {
|
||||||
"fill-extrusion-color": [
|
"fill-extrusion-color": "#DDD8D0",
|
||||||
"match",
|
|
||||||
[
|
|
||||||
"get",
|
|
||||||
"building"
|
|
||||||
],
|
|
||||||
"commercial",
|
|
||||||
"#DDD5C5",
|
|
||||||
"retail",
|
|
||||||
"#E5D8C8",
|
|
||||||
"industrial",
|
|
||||||
"#D2D8E0",
|
|
||||||
"church",
|
|
||||||
"#DDD4EE",
|
|
||||||
"mosque",
|
|
||||||
"#CCE4D0",
|
|
||||||
"hospital",
|
|
||||||
"#EDD8D8",
|
|
||||||
"school",
|
|
||||||
"#E0E6CC",
|
|
||||||
"university",
|
|
||||||
"#D8E0C8",
|
|
||||||
"hotel",
|
|
||||||
"#D8DCF0",
|
|
||||||
"apartments",
|
|
||||||
"#E0DCD4",
|
|
||||||
"#DDD8D2"
|
|
||||||
],
|
|
||||||
"fill-extrusion-height": [
|
"fill-extrusion-height": [
|
||||||
"interpolate",
|
"interpolate",
|
||||||
[
|
["linear"],
|
||||||
"linear"
|
["zoom"],
|
||||||
],
|
|
||||||
[
|
|
||||||
"zoom"
|
|
||||||
],
|
|
||||||
14,
|
14,
|
||||||
[
|
[
|
||||||
"*",
|
"*",
|
||||||
[
|
["coalesce", ["to-number", ["get", "num_floors"], null], 3],
|
||||||
"coalesce",
|
|
||||||
[
|
|
||||||
"to-number",
|
|
||||||
[
|
|
||||||
"get",
|
|
||||||
"building:levels"
|
|
||||||
],
|
|
||||||
null
|
|
||||||
],
|
|
||||||
3
|
|
||||||
],
|
|
||||||
2.5
|
2.5
|
||||||
],
|
],
|
||||||
17,
|
17,
|
||||||
[
|
[
|
||||||
"coalesce",
|
"coalesce",
|
||||||
[
|
["to-number", ["get", "height"], null],
|
||||||
"to-number",
|
|
||||||
[
|
|
||||||
"get",
|
|
||||||
"height"
|
|
||||||
],
|
|
||||||
null
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
"*",
|
"*",
|
||||||
[
|
["coalesce", ["to-number", ["get", "num_floors"], null], 3],
|
||||||
"to-number",
|
|
||||||
[
|
|
||||||
"get",
|
|
||||||
"building:levels"
|
|
||||||
],
|
|
||||||
3
|
|
||||||
],
|
|
||||||
3.5
|
3.5
|
||||||
],
|
],
|
||||||
12
|
12
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"fill-extrusion-base": [
|
"fill-extrusion-base": 0,
|
||||||
"coalesce",
|
|
||||||
[
|
|
||||||
"to-number",
|
|
||||||
[
|
|
||||||
"get",
|
|
||||||
"min_height"
|
|
||||||
],
|
|
||||||
null
|
|
||||||
],
|
|
||||||
0
|
|
||||||
],
|
|
||||||
"fill-extrusion-opacity": [
|
"fill-extrusion-opacity": [
|
||||||
"interpolate",
|
"interpolate",
|
||||||
[
|
["linear"],
|
||||||
"linear"
|
["zoom"],
|
||||||
],
|
14, 0.55,
|
||||||
[
|
16, 0.85
|
||||||
"zoom"
|
|
||||||
],
|
|
||||||
14,
|
|
||||||
0.6,
|
|
||||||
16,
|
|
||||||
0.88
|
|
||||||
],
|
],
|
||||||
"fill-extrusion-vertical-gradient": true
|
"fill-extrusion-vertical-gradient": true
|
||||||
}
|
}
|
||||||
@@ -1222,33 +1174,28 @@
|
|||||||
"unclassified",
|
"unclassified",
|
||||||
"living_street"
|
"living_street"
|
||||||
],
|
],
|
||||||
"minzoom": 16,
|
"minzoom": 15,
|
||||||
"layout": {
|
"layout": {
|
||||||
"text-field": [
|
"text-field": [
|
||||||
"coalesce",
|
"coalesce",
|
||||||
[
|
["get", "name:ar"],
|
||||||
"get",
|
["get", "name"],
|
||||||
"name:ar"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"get",
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
""
|
""
|
||||||
],
|
],
|
||||||
"text-font": [
|
"text-font": ["Noto Sans Regular"],
|
||||||
"Noto Sans Regular"
|
"text-size": ["interpolate", ["linear"], ["zoom"], 15, 10, 18, 13],
|
||||||
],
|
|
||||||
"text-size": 11,
|
|
||||||
"symbol-placement": "line",
|
"symbol-placement": "line",
|
||||||
"text-letter-spacing": 0.04,
|
"text-letter-spacing": 0.05,
|
||||||
"text-padding": 4,
|
"text-padding": 15,
|
||||||
"text-allow-overlap": false
|
"symbol-spacing": 300,
|
||||||
|
"text-max-angle": 30,
|
||||||
|
"text-allow-overlap": false,
|
||||||
|
"text-ignore-placement": false
|
||||||
},
|
},
|
||||||
"paint": {
|
"paint": {
|
||||||
"text-color": "#4A5568",
|
"text-color": "#4A5568",
|
||||||
"text-halo-color": "rgba(255,255,255,0.85)",
|
"text-halo-color": "rgba(255,255,255,0.92)",
|
||||||
"text-halo-width": 1.5
|
"text-halo-width": 1.8
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1265,45 +1212,28 @@
|
|||||||
"motorway",
|
"motorway",
|
||||||
"trunk"
|
"trunk"
|
||||||
],
|
],
|
||||||
"minzoom": 13,
|
"minzoom": 12,
|
||||||
"layout": {
|
"layout": {
|
||||||
"text-field": [
|
"text-field": [
|
||||||
"coalesce",
|
"coalesce",
|
||||||
[
|
["get", "name:ar"],
|
||||||
"get",
|
["get", "name"],
|
||||||
"name:ar"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"get",
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
""
|
""
|
||||||
],
|
],
|
||||||
"text-font": [
|
"text-font": ["Noto Sans Bold"],
|
||||||
"Noto Sans Regular"
|
"text-size": ["interpolate", ["linear"], ["zoom"], 12, 10, 14, 13, 18, 16],
|
||||||
],
|
|
||||||
"text-size": [
|
|
||||||
"interpolate",
|
|
||||||
[
|
|
||||||
"linear"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"zoom"
|
|
||||||
],
|
|
||||||
13,
|
|
||||||
11,
|
|
||||||
16,
|
|
||||||
14
|
|
||||||
],
|
|
||||||
"symbol-placement": "line",
|
"symbol-placement": "line",
|
||||||
"text-letter-spacing": 0.05,
|
"text-letter-spacing": 0.06,
|
||||||
"text-padding": 5,
|
"text-padding": 20,
|
||||||
"text-allow-overlap": false
|
"symbol-spacing": 350,
|
||||||
|
"text-max-angle": 25,
|
||||||
|
"text-allow-overlap": false,
|
||||||
|
"text-ignore-placement": false
|
||||||
},
|
},
|
||||||
"paint": {
|
"paint": {
|
||||||
"text-color": "#2D3748",
|
"text-color": "#1A2332",
|
||||||
"text-halo-color": "rgba(255,255,255,0.9)",
|
"text-halo-color": "rgba(255,255,255,0.95)",
|
||||||
"text-halo-width": 2
|
"text-halo-width": 2.5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1929,14 +1859,11 @@
|
|||||||
"type": "fill",
|
"type": "fill",
|
||||||
"source": "overture_buildings",
|
"source": "overture_buildings",
|
||||||
"source-layer": "overture_building",
|
"source-layer": "overture_building",
|
||||||
"minzoom": 14,
|
"maxzoom": 14,
|
||||||
"layout": {
|
|
||||||
"visibility": "visible"
|
|
||||||
},
|
|
||||||
"paint": {
|
"paint": {
|
||||||
"fill-color": "#dcd8d0",
|
"fill-color": "#DDD8D0",
|
||||||
"fill-opacity": 0.8,
|
"fill-opacity": 0.85,
|
||||||
"fill-outline-color": "#c4beb4"
|
"fill-outline-color": "#C4BEB4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1978,31 +1905,26 @@
|
|||||||
"source": "overture_segments",
|
"source": "overture_segments",
|
||||||
"source-layer": "overture_segment",
|
"source-layer": "overture_segment",
|
||||||
"minzoom": 14,
|
"minzoom": 14,
|
||||||
|
"filter": ["has", "names"],
|
||||||
"layout": {
|
"layout": {
|
||||||
"text-field": [
|
"text-field": ["coalesce", ["get", "names"], ""],
|
||||||
"coalesce",
|
"text-font": ["Noto Sans Regular"],
|
||||||
[
|
"text-size": ["interpolate", ["linear"], ["zoom"], 14, 9, 16, 12, 18, 14],
|
||||||
"get",
|
|
||||||
"names"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"text-font": [
|
|
||||||
"Noto Sans Regular"
|
|
||||||
],
|
|
||||||
"text-size": 11,
|
|
||||||
"symbol-placement": "line",
|
"symbol-placement": "line",
|
||||||
"text-padding": 10,
|
"text-rotation-alignment": "map",
|
||||||
|
"text-pitch-alignment": "viewport",
|
||||||
|
"text-keep-upright": true,
|
||||||
|
"text-padding": 20,
|
||||||
"text-letter-spacing": 0.04,
|
"text-letter-spacing": 0.04,
|
||||||
"text-max-angle": 30,
|
"text-max-angle": 30,
|
||||||
"symbol-spacing": 250,
|
"symbol-spacing": 300,
|
||||||
"text-allow-overlap": false,
|
"text-allow-overlap": false,
|
||||||
"text-ignore-placement": false
|
"text-ignore-placement": false
|
||||||
},
|
},
|
||||||
"paint": {
|
"paint": {
|
||||||
"text-color": "#666",
|
"text-color": "#4A5568",
|
||||||
"text-halo-color": "rgba(255, 255, 255, 0.9)",
|
"text-halo-color": "rgba(255, 255, 255, 0.95)",
|
||||||
"text-halo-width": 2
|
"text-halo-width": 2.2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -21,13 +21,17 @@ const MapComponent: React.FC<MapComponentProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map.current) return;
|
if (!map.current) return;
|
||||||
|
|
||||||
// Toggle 3D Buildings
|
// Toggle 3D Buildings (now on Overture source)
|
||||||
if (map.current.getLayer('building-3d')) {
|
if (map.current.getLayer('building-3d')) {
|
||||||
map.current.setLayoutProperty('building-3d', 'visibility', show3D ? 'visible' : 'none');
|
map.current.setLayoutProperty('building-3d', 'visibility', show3D ? 'visible' : 'none');
|
||||||
}
|
}
|
||||||
|
// Toggle flat Overture footprint (show when 3D is off)
|
||||||
|
if (map.current.getLayer('overture-building-footprint')) {
|
||||||
|
map.current.setLayoutProperty('overture-building-footprint', 'visibility', show3D ? 'none' : 'visible');
|
||||||
|
}
|
||||||
|
|
||||||
// Toggle POI Layers
|
// Toggle POI Layers
|
||||||
const poiLayers = ['poi-icons', 'place-labels'];
|
const poiLayers = ['poi-icons', 'place-labels', 'overture-building-names'];
|
||||||
poiLayers.forEach(layerId => {
|
poiLayers.forEach(layerId => {
|
||||||
if (map.current!.getLayer(layerId)) {
|
if (map.current!.getLayer(layerId)) {
|
||||||
map.current!.setLayoutProperty(layerId, 'visibility', showPOIs ? 'visible' : 'none');
|
map.current!.setLayoutProperty(layerId, 'visibility', showPOIs ? 'visible' : 'none');
|
||||||
@@ -389,7 +393,7 @@ const MapComponent: React.FC<MapComponentProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={mapContainer} style={{ width: '100%', height: '100%', position: 'relative' }}>
|
<div ref={mapContainer} style={{ width: '100%', height: '100%', position: 'relative' }}>
|
||||||
{/* Intaleq Branding Watermark */}
|
{/* Intaleq Premium Branding Watermark */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -398,25 +402,29 @@ const MapComponent: React.FC<MapComponentProps> = ({
|
|||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.7)',
|
background: 'rgba(255, 255, 255, 0.85)',
|
||||||
padding: '4px 8px',
|
backdropFilter: 'blur(12px) saturate(180%)',
|
||||||
borderRadius: '4px',
|
WebkitBackdropFilter: 'blur(12px) saturate(180%)',
|
||||||
|
padding: '6px 12px',
|
||||||
|
borderRadius: '10px',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
gap: '6px'
|
gap: '8px',
|
||||||
|
boxShadow: '0 4px 16px rgba(0,0,0,0.08)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.35)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/intaleq-logo.png"
|
src="/intaleq-logo.png"
|
||||||
alt="Intaleq"
|
alt="Intaleq"
|
||||||
style={{ height: '16px', filter: 'drop-shadow(0 1px 1px rgba(0,0,0,0.1))' }}
|
style={{ height: '18px', filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.1))' }}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
fontSize: '10px',
|
fontSize: '10px',
|
||||||
fontWeight: 600,
|
fontWeight: 800,
|
||||||
color: '#3c4043',
|
color: '#c0a048',
|
||||||
letterSpacing: '0.02em',
|
letterSpacing: '0.8px',
|
||||||
textTransform: 'uppercase'
|
textTransform: 'uppercase'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
def export_minimalist_project():
|
||||||
|
output_filename = "project_source_code_light.txt"
|
||||||
|
|
||||||
|
# 1. الملفات المسموح بها بالاسم الكامل (حساسة لحالة الأحرف)
|
||||||
|
strictly_allowed_names = {'style.json', 'style_mobile.json', '.env', 'env'}
|
||||||
|
|
||||||
|
# 2. الامتدادات البرمجية المسموح بها (الكود المصدري فقط)
|
||||||
|
allowed_logic_extensions = {'.ts', '.sh'}
|
||||||
|
|
||||||
|
# 3. مجلدات يتم تجاوزها فوراً لتسريع العملية
|
||||||
|
ignore_dirs = {'node_modules', 'dist', '.git', 'coverage', 'build', '.idea', '.vscode'}
|
||||||
|
|
||||||
|
file_counter = 0
|
||||||
|
|
||||||
|
print("جاري استخراج الملفات المطلوبة فقط (Light Mode)...")
|
||||||
|
|
||||||
|
with open(output_filename, 'w', encoding='utf-8') as outfile:
|
||||||
|
outfile.write("=" * 60 + "\n")
|
||||||
|
outfile.write("ملفات المشروع المختارة (Style JSON + Source Code)\n")
|
||||||
|
outfile.write("=" * 60 + "\n\n")
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk('.'):
|
||||||
|
# استثناء المجلدات الثقيلة
|
||||||
|
dirs[:] = [d for d in dirs if d not in ignore_dirs]
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
name_lower = file.lower()
|
||||||
|
ext = os.path.splitext(name_lower)[1]
|
||||||
|
|
||||||
|
# شرط القبول المنطقي:
|
||||||
|
# - إما أن يكون الاسم مطابقاً تماماً لملفات الستايل أو env
|
||||||
|
# - أو أن يكون امتداد ملف كود (ts, sh)
|
||||||
|
if file in strictly_allowed_names or ext in allowed_logic_extensions:
|
||||||
|
|
||||||
|
# استثناء إضافي لملفات الاختبار أو التكوين الفرعية لتقليل الحجم
|
||||||
|
if '.spec.ts' in name_lower or 'test.ts' in name_lower:
|
||||||
|
continue
|
||||||
|
|
||||||
|
filepath = os.path.join(root, file)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(filepath, 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
file_counter += 1
|
||||||
|
outfile.write("-" * 60 + "\n")
|
||||||
|
outfile.write(f"[{file_counter}] اسم الملف: {file}\n")
|
||||||
|
outfile.write(f"المسار: {filepath}\n")
|
||||||
|
outfile.write("-" * 60 + "\n")
|
||||||
|
|
||||||
|
for line_number, line in enumerate(lines, 1):
|
||||||
|
# كتابة رقم السطر مع الكود
|
||||||
|
outfile.write(f"{line_number:04d} | {line.rstrip()}\n")
|
||||||
|
|
||||||
|
outfile.write("\n\n")
|
||||||
|
print(f"تمت الإضافة: {filepath}")
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# تجاوز أي ملف يسبب خطأ في القراءة
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\n✅ اكتملت العملية بنجاح.")
|
||||||
|
print(f"إجمالي الملفات المضافة: {file_counter}")
|
||||||
|
print(f"الملف الناتج: {output_filename}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
export_minimalist_project()
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user