@jjlmoya/utils-tools 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/package.json +63 -0
  2. package/src/category/i18n/en.ts +172 -0
  3. package/src/category/i18n/es.ts +172 -0
  4. package/src/category/i18n/fr.ts +172 -0
  5. package/src/category/index.ts +23 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +90 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/schemas_fulfillment.test.ts +23 -0
  21. package/src/tests/seo_length.test.ts +23 -0
  22. package/src/tests/title_quality.test.ts +56 -0
  23. package/src/tests/tool_validation.test.ts +17 -0
  24. package/src/tool/date-diff-calculator/bibliography.astro +14 -0
  25. package/src/tool/date-diff-calculator/component.astro +370 -0
  26. package/src/tool/date-diff-calculator/i18n/en.ts +132 -0
  27. package/src/tool/date-diff-calculator/i18n/es.ts +132 -0
  28. package/src/tool/date-diff-calculator/i18n/fr.ts +132 -0
  29. package/src/tool/date-diff-calculator/index.ts +22 -0
  30. package/src/tool/date-diff-calculator/seo.astro +14 -0
  31. package/src/tool/date-diff-calculator/ui.ts +17 -0
  32. package/src/tool/drive-direct-link/bibliography.astro +14 -0
  33. package/src/tool/drive-direct-link/component.astro +280 -0
  34. package/src/tool/drive-direct-link/i18n/en.ts +118 -0
  35. package/src/tool/drive-direct-link/i18n/es.ts +118 -0
  36. package/src/tool/drive-direct-link/i18n/fr.ts +118 -0
  37. package/src/tool/drive-direct-link/index.ts +22 -0
  38. package/src/tool/drive-direct-link/seo.astro +14 -0
  39. package/src/tool/drive-direct-link/ui.ts +10 -0
  40. package/src/tool/email-list-cleaner/bibliography.astro +14 -0
  41. package/src/tool/email-list-cleaner/component.astro +375 -0
  42. package/src/tool/email-list-cleaner/i18n/en.ts +140 -0
  43. package/src/tool/email-list-cleaner/i18n/es.ts +140 -0
  44. package/src/tool/email-list-cleaner/i18n/fr.ts +140 -0
  45. package/src/tool/email-list-cleaner/index.ts +22 -0
  46. package/src/tool/email-list-cleaner/seo.astro +14 -0
  47. package/src/tool/email-list-cleaner/ui.ts +15 -0
  48. package/src/tool/env-badge-spain/bibliography.astro +14 -0
  49. package/src/tool/env-badge-spain/component.astro +303 -0
  50. package/src/tool/env-badge-spain/components/BadgeForm.astro +243 -0
  51. package/src/tool/env-badge-spain/components/BadgeResult.astro +151 -0
  52. package/src/tool/env-badge-spain/i18n/en.ts +153 -0
  53. package/src/tool/env-badge-spain/i18n/es.ts +153 -0
  54. package/src/tool/env-badge-spain/i18n/fr.ts +153 -0
  55. package/src/tool/env-badge-spain/index.ts +22 -0
  56. package/src/tool/env-badge-spain/seo.astro +14 -0
  57. package/src/tool/env-badge-spain/ui.ts +53 -0
  58. package/src/tool/morse-beacon/bibliography.astro +14 -0
  59. package/src/tool/morse-beacon/component.astro +534 -0
  60. package/src/tool/morse-beacon/i18n/en.ts +157 -0
  61. package/src/tool/morse-beacon/i18n/es.ts +157 -0
  62. package/src/tool/morse-beacon/i18n/fr.ts +157 -0
  63. package/src/tool/morse-beacon/index.ts +22 -0
  64. package/src/tool/morse-beacon/logic/MorseEngine.ts +124 -0
  65. package/src/tool/morse-beacon/seo.astro +14 -0
  66. package/src/tool/morse-beacon/ui.ts +18 -0
  67. package/src/tool/password-generator/bibliography.astro +14 -0
  68. package/src/tool/password-generator/component.astro +259 -0
  69. package/src/tool/password-generator/components/Config.astro +227 -0
  70. package/src/tool/password-generator/components/Display.astro +147 -0
  71. package/src/tool/password-generator/components/Strength.astro +70 -0
  72. package/src/tool/password-generator/i18n/en.ts +166 -0
  73. package/src/tool/password-generator/i18n/es.ts +166 -0
  74. package/src/tool/password-generator/i18n/fr.ts +166 -0
  75. package/src/tool/password-generator/index.ts +22 -0
  76. package/src/tool/password-generator/seo.astro +14 -0
  77. package/src/tool/password-generator/ui.ts +16 -0
  78. package/src/tool/routes/bibliography.astro +14 -0
  79. package/src/tool/routes/component.astro +543 -0
  80. package/src/tool/routes/i18n/en.ts +157 -0
  81. package/src/tool/routes/i18n/es.ts +157 -0
  82. package/src/tool/routes/i18n/fr.ts +157 -0
  83. package/src/tool/routes/index.ts +22 -0
  84. package/src/tool/routes/logic/GeocodingService.ts +60 -0
  85. package/src/tool/routes/logic/RouteManager.ts +192 -0
  86. package/src/tool/routes/logic/RouteService.ts +66 -0
  87. package/src/tool/routes/seo.astro +14 -0
  88. package/src/tool/routes/ui.ts +16 -0
  89. package/src/tool/rule-of-three/bibliography.astro +14 -0
  90. package/src/tool/rule-of-three/component.astro +369 -0
  91. package/src/tool/rule-of-three/i18n/en.ts +171 -0
  92. package/src/tool/rule-of-three/i18n/es.ts +171 -0
  93. package/src/tool/rule-of-three/i18n/fr.ts +171 -0
  94. package/src/tool/rule-of-three/index.ts +22 -0
  95. package/src/tool/rule-of-three/seo.astro +14 -0
  96. package/src/tool/rule-of-three/ui.ts +13 -0
  97. package/src/tool/seo-content-optimizer/bibliography.astro +14 -0
  98. package/src/tool/seo-content-optimizer/component.astro +552 -0
  99. package/src/tool/seo-content-optimizer/i18n/en.ts +136 -0
  100. package/src/tool/seo-content-optimizer/i18n/es.ts +136 -0
  101. package/src/tool/seo-content-optimizer/i18n/fr.ts +136 -0
  102. package/src/tool/seo-content-optimizer/index.ts +22 -0
  103. package/src/tool/seo-content-optimizer/seo.astro +14 -0
  104. package/src/tool/seo-content-optimizer/ui.ts +29 -0
  105. package/src/tool/speed-reader/bibliography.astro +14 -0
  106. package/src/tool/speed-reader/component.astro +586 -0
  107. package/src/tool/speed-reader/i18n/en.ts +152 -0
  108. package/src/tool/speed-reader/i18n/es.ts +152 -0
  109. package/src/tool/speed-reader/i18n/fr.ts +152 -0
  110. package/src/tool/speed-reader/index.ts +22 -0
  111. package/src/tool/speed-reader/logic/RSVPEngine.ts +106 -0
  112. package/src/tool/speed-reader/seo.astro +14 -0
  113. package/src/tool/speed-reader/ui.ts +14 -0
  114. package/src/tool/text-pixel-calculator/bibliography.astro +14 -0
  115. package/src/tool/text-pixel-calculator/component.astro +315 -0
  116. package/src/tool/text-pixel-calculator/components/Editor.astro +240 -0
  117. package/src/tool/text-pixel-calculator/components/Preview.astro +155 -0
  118. package/src/tool/text-pixel-calculator/components/Stats.astro +87 -0
  119. package/src/tool/text-pixel-calculator/i18n/en.ts +133 -0
  120. package/src/tool/text-pixel-calculator/i18n/es.ts +133 -0
  121. package/src/tool/text-pixel-calculator/i18n/fr.ts +133 -0
  122. package/src/tool/text-pixel-calculator/index.ts +22 -0
  123. package/src/tool/text-pixel-calculator/seo.astro +14 -0
  124. package/src/tool/text-pixel-calculator/ui.ts +15 -0
  125. package/src/tool/whatsapp-link/bibliography.astro +14 -0
  126. package/src/tool/whatsapp-link/component.astro +455 -0
  127. package/src/tool/whatsapp-link/i18n/en.ts +128 -0
  128. package/src/tool/whatsapp-link/i18n/es.ts +128 -0
  129. package/src/tool/whatsapp-link/i18n/fr.ts +128 -0
  130. package/src/tool/whatsapp-link/index.ts +22 -0
  131. package/src/tool/whatsapp-link/seo.astro +14 -0
  132. package/src/tool/whatsapp-link/ui.ts +15 -0
  133. package/src/tools.ts +15 -0
  134. package/src/types.ts +72 -0
@@ -0,0 +1,157 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
3
+ import type { RoutesUI } from '../ui';
4
+
5
+ const faqData = [
6
+ {
7
+ question: '¿Cuál es el algoritmo de optimización que utiliza?',
8
+ answer: 'Utiliza una resolución avanzada del Problema del Viajante (TSP). El algoritmo analiza todas las paradas y determina el orden secuencial que minimiza la distancia total recorrida, ahorrando tiempo y combustible.',
9
+ },
10
+ {
11
+ question: '¿Es seguro compartir mi ubicación?',
12
+ answer: 'Sí. La herramienta procesa los datos geográficos de forma local en tu navegador. Nosotros no almacenamos tus rutas, paradas ni historial de ubicaciones en nuestros servidores.',
13
+ },
14
+ {
15
+ question: '¿Cuántas paradas puedo optimizar a la vez?',
16
+ answer: 'Nuestra versión gratuita permite optimizar hasta 10 paradas de forma instantánea. Para rutas profesionales más amplias, el sistema está optimizado para mantener un rendimiento alto sin bloqueos en el navegador.',
17
+ },
18
+ {
19
+ question: '¿Puedo usar la ruta directamente en Google Maps?',
20
+ answer: '¡Claro! Una vez optimizada la ruta, la herramienta genera un enlace de navegación compatible con Google Maps para que puedas iniciar el viaje directamente desde tu móvil.',
21
+ },
22
+ ];
23
+
24
+ const howToData = [
25
+ {
26
+ name: 'Añadir punto de origen',
27
+ text: 'Escribe la dirección de inicio o pulsa en el mapa para fijar desde dónde comienza tu ruta.',
28
+ },
29
+ {
30
+ name: 'Introducir paradas de destino',
31
+ text: 'Añade todas las ubicaciones que necesitas visitar. No importa el orden, ya que el sistema las reordenará por ti.',
32
+ },
33
+ {
34
+ name: 'Optimizar la secuencia',
35
+ text: 'Pulsa el botón de optimizar. El algoritmo calculará en segundos el orden más eficiente para recorrer todas las paradas.',
36
+ },
37
+ {
38
+ name: 'Abrir en navegador',
39
+ text: 'Utiliza el botón de navegación para llevarte la ruta optimizada a tu aplicación de mapas favorita.',
40
+ },
41
+ ];
42
+
43
+ const faqSchema: WithContext<FAQPage> = {
44
+ '@context': 'https://schema.org',
45
+ '@type': 'FAQPage',
46
+ mainEntity: faqData.map((item) => ({
47
+ '@type': 'Question',
48
+ name: item.question,
49
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
50
+ })),
51
+ };
52
+
53
+ const howToSchema: WithContext<HowTo> = {
54
+ '@context': 'https://schema.org',
55
+ '@type': 'HowTo',
56
+ name: 'Cómo optimizar una ruta con múltiples paradas',
57
+ step: howToData.map((s) => ({ '@type': 'HowToStep', name: s.name, text: s.text })),
58
+ };
59
+
60
+ const appSchema: WithContext<SoftwareApplication> = {
61
+ '@context': 'https://schema.org',
62
+ '@type': 'SoftwareApplication',
63
+ name: 'Calculadora de Rutas Óptimas',
64
+ applicationCategory: 'UtilitiesApplication',
65
+ operatingSystem: 'Web',
66
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
67
+ description: 'Optimiza tus rutas de reparto o viaje gratis. Reordena paradas automáticamente para encontrar el camino más corto y eficiente.',
68
+ };
69
+
70
+ const ui: RoutesUI = {
71
+ titleSidebar: 'Puntos de Ruta',
72
+ descriptionSidebar: 'Haz clic en el mapa para añadir paradas. El primer punto es el inicio.',
73
+ emptyState: 'Sin puntos añadidos',
74
+ btnOptimize: 'Calcular Ruta Óptima',
75
+ btnCalculating: 'Calculando...',
76
+ btnClear: 'Borrar Todo',
77
+ labelDistance: 'Distancia Total Estimada',
78
+ labelDeleteAria: 'Eliminar',
79
+ labelLoadingAddress: 'Cargando dirección...',
80
+ labelPoint: 'Punto',
81
+ errorMinPoints: 'Se necesitan al menos dos puntos para calcular una ruta.',
82
+ errorCalculate: 'Error al calcular la ruta.',
83
+ errorAddress: 'Error al obtener dirección',
84
+ errorAddressName: 'Punto desconocido',
85
+ };
86
+
87
+ export const content: ToolLocaleContent<RoutesUI> = {
88
+ slug: 'rutas-optimas',
89
+ title: 'Calculadora de Rutas Óptimas Gratis',
90
+ description: 'Optimiza tus rutas de reparto o viaje gratis. Nuestra herramienta reordena tus paradas automáticamente para encontrar el camino más corto y eficiente.',
91
+ ui,
92
+ faqTitle: 'Preguntas Frecuentes',
93
+ faq: faqData,
94
+ howTo: howToData,
95
+ bibliographyTitle: 'Tecnologías y Fuentes',
96
+ bibliography: [
97
+ { name: 'Leaflet — Librería de mapas interactivos de código abierto', url: 'https://leafletjs.com/' },
98
+ { name: 'OpenStreetMap — Datos cartográficos abiertos', url: 'https://www.openstreetmap.org/' },
99
+ { name: 'CARTO — Tiles de mapas Voyager', url: 'https://carto.com/basemaps/' },
100
+ { name: 'Nominatim — Servicio de geocodificación inversa (OpenStreetMap)', url: 'https://nominatim.org/' },
101
+ { name: 'OSRM — Open Source Routing Machine (optimización de rutas)', url: 'http://project-osrm.org/' },
102
+ ],
103
+ schemas: [faqSchema, howToSchema, appSchema],
104
+ seo: [
105
+ {
106
+ type: 'title',
107
+ text: 'Optimizador de Rutas Inteligente: TSP para Repartos y Viajes',
108
+ level: 2,
109
+ },
110
+ {
111
+ type: 'paragraph',
112
+ html: 'El <strong>optimizador de rutas online</strong> es una herramienta gratuita que resuelve el clásico <em>Problema del Viajante (TSP)</em>. Añade todas tus paradas sin importar el orden y el algoritmo calculará automáticamente la secuencia más eficiente para minimizar la distancia total recorrida.',
113
+ },
114
+ {
115
+ type: 'title',
116
+ text: '¿Cómo funciona el algoritmo de optimización de rutas?',
117
+ level: 2,
118
+ },
119
+ {
120
+ type: 'paragraph',
121
+ html: 'La herramienta utiliza la API de <strong>OSRM (Open Source Routing Machine)</strong>, un motor de enrutamiento de alto rendimiento basado en datos de OpenStreetMap. El proceso es: primero calcula la ruta circular óptima entre todos los puntos (algoritmo TSP), luego determina el mejor punto de corte para convertirla en un recorrido lineal de ida, y finalmente dibuja la ruta sobre el mapa con la distancia total estimada.',
122
+ },
123
+ {
124
+ type: 'title',
125
+ text: 'Casos de uso: repartos, comerciales y viajes',
126
+ level: 2,
127
+ },
128
+ {
129
+ type: 'paragraph',
130
+ html: 'El optimizador de rutas es ideal para <strong>repartidores autónomos</strong> que necesitan organizar múltiples entregas diarias, <strong>comerciales a puerta fría</strong> que visitan clientes en una zona, o <strong>viajeros</strong> que quieren conocer varias ciudades o monumentos en el orden más eficiente. La herramienta procesa toda la información en el navegador, sin compartir tus datos con ningún servidor propio.',
131
+ },
132
+ {
133
+ type: 'stats',
134
+ columns: 2,
135
+ items: [
136
+ { value: 'TSP', label: 'Algoritmo', icon: 'mdi:chart-line' },
137
+ { value: 'OSRM', label: 'Motor de rutas', icon: 'mdi:map-marker-path' },
138
+ { value: 'Local-First', label: 'Privacidad', icon: 'mdi:lock-check' },
139
+ { value: 'Gratuito', label: 'Coste', icon: 'mdi:currency-eur-off' },
140
+ ],
141
+ },
142
+ {
143
+ type: 'title',
144
+ text: 'Privacidad y procesamiento local',
145
+ level: 2,
146
+ },
147
+ {
148
+ type: 'paragraph',
149
+ html: 'Toda la lógica de la herramienta se ejecuta directamente en tu navegador. Las coordenadas de tus paradas solo se envían a las APIs públicas de OSRM y Nominatim para calcular rutas y obtener nombres de dirección, pero <strong>no se almacenan en ningún servidor propio</strong>. Puedes usar la herramienta con total seguridad para planificar rutas de negocio con información sensible.',
150
+ },
151
+ {
152
+ type: 'tip',
153
+ title: 'Consejo de uso',
154
+ html: '<strong>Para mejores resultados</strong>, añade las paradas haciendo clic directamente en el mapa en lugar de usar búsqueda de direcciones. Los marcadores son arrastrables, por lo que puedes ajustar la posición exacta de cada punto después de colocarlo.',
155
+ },
156
+ ],
157
+ };
@@ -0,0 +1,157 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
3
+ import type { RoutesUI } from '../ui';
4
+
5
+ const faqData = [
6
+ {
7
+ question: "Quel algorithme d'optimisation utilise-t-il ?",
8
+ answer: "Il utilise une résolution avancée du Problème du Voyageur de Commerce (TSP). L'algorithme analyse tous les arrêts et détermine l'ordre séquentiel qui minimise la distance totale parcourue, économisant ainsi du temps et du carburant.",
9
+ },
10
+ {
11
+ question: 'Est-il sûr de partager ma localisation ?',
12
+ answer: "Oui. L'outil traite les données géographiques localement dans votre navigateur. Nous ne stockons pas vos itinéraires, arrêts ou historique de localisation sur nos serveurs.",
13
+ },
14
+ {
15
+ question: "Combien d'arrêts puis-je optimiser à la fois ?",
16
+ answer: "Notre version gratuite permet d'optimiser jusqu'à 10 arrêts instantanément. Pour des itinéraires professionnels plus étendus, le système est optimisé pour maintenir de hautes performances sans bloquer le navigateur.",
17
+ },
18
+ {
19
+ question: "Puis-je utiliser l'itinéraire directement dans Google Maps ?",
20
+ answer: "Bien sûr ! Une fois l'itinéraire optimisé, l'outil génère un lien de navigation compatible avec Google Maps pour que vous puissiez démarrer le voyage directement depuis votre téléphone.",
21
+ },
22
+ ];
23
+
24
+ const howToData = [
25
+ {
26
+ name: 'Ajouter le point de départ',
27
+ text: "Saisissez l'adresse de départ ou cliquez sur la carte pour fixer le point de départ de votre itinéraire.",
28
+ },
29
+ {
30
+ name: 'Saisir les arrêts de destination',
31
+ text: "Ajoutez tous les lieux que vous devez visiter. L'ordre n'a pas d'importance, car le système les réordonnera pour vous.",
32
+ },
33
+ {
34
+ name: "Optimiser la séquence",
35
+ text: "Cliquez sur le bouton d'optimisation. L'algorithme calculera en quelques secondes l'ordre le plus efficace pour parcourir tous les arrêts.",
36
+ },
37
+ {
38
+ name: 'Ouvrir dans le navigateur',
39
+ text: "Utilisez le bouton de navigation pour transférer l'itinéraire optimisé vers votre application de cartographie préférée.",
40
+ },
41
+ ];
42
+
43
+ const faqSchema: WithContext<FAQPage> = {
44
+ '@context': 'https://schema.org',
45
+ '@type': 'FAQPage',
46
+ mainEntity: faqData.map((item) => ({
47
+ '@type': 'Question',
48
+ name: item.question,
49
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
50
+ })),
51
+ };
52
+
53
+ const howToSchema: WithContext<HowTo> = {
54
+ '@context': 'https://schema.org',
55
+ '@type': 'HowTo',
56
+ name: 'Comment optimiser un itinéraire avec plusieurs arrêts',
57
+ step: howToData.map((s) => ({ '@type': 'HowToStep', name: s.name, text: s.text })),
58
+ };
59
+
60
+ const appSchema: WithContext<SoftwareApplication> = {
61
+ '@context': 'https://schema.org',
62
+ '@type': 'SoftwareApplication',
63
+ name: "Calculateur d'itinéraires optimaux",
64
+ applicationCategory: 'UtilitiesApplication',
65
+ operatingSystem: 'Web',
66
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
67
+ description: "Optimisez vos itinéraires de livraison ou de voyage gratuitement. Réorganisez automatiquement vos arrêts pour trouver le chemin le plus court et le plus efficace.",
68
+ };
69
+
70
+ const ui: RoutesUI = {
71
+ titleSidebar: "Points d'itinéraire",
72
+ descriptionSidebar: "Cliquez sur la carte pour ajouter des arrêts. Le premier point est le départ.",
73
+ emptyState: 'Aucun point ajouté',
74
+ btnOptimize: "Calculer l'itinéraire optimal",
75
+ btnCalculating: 'Calcul en cours...',
76
+ btnClear: 'Tout effacer',
77
+ labelDistance: 'Distance totale estimée',
78
+ labelDeleteAria: 'Supprimer',
79
+ labelLoadingAddress: 'Chargement de l\'adresse...',
80
+ labelPoint: 'Point',
81
+ errorMinPoints: 'Au moins deux points sont nécessaires pour calculer un itinéraire.',
82
+ errorCalculate: "Erreur lors du calcul de l'itinéraire.",
83
+ errorAddress: "Erreur lors de la récupération de l'adresse",
84
+ errorAddressName: 'Point inconnu',
85
+ };
86
+
87
+ export const content: ToolLocaleContent<RoutesUI> = {
88
+ slug: 'itineraires-optimaux',
89
+ title: "Calculateur d'itinéraires optimaux gratuit",
90
+ description: "Optimisez vos itinéraires de livraison ou de voyage gratuitement. Notre outil réorganise automatiquement vos arrêts pour trouver le chemin le plus court et le plus efficace.",
91
+ ui,
92
+ faqTitle: 'Questions fréquentes',
93
+ faq: faqData,
94
+ howTo: howToData,
95
+ bibliographyTitle: 'Technologies et sources',
96
+ bibliography: [
97
+ { name: 'Leaflet — Bibliothèque de cartes interactives open source', url: 'https://leafletjs.com/' },
98
+ { name: 'OpenStreetMap — Données cartographiques ouvertes', url: 'https://www.openstreetmap.org/' },
99
+ { name: 'CARTO — Tuiles de carte Voyager', url: 'https://carto.com/basemaps/' },
100
+ { name: 'Nominatim — Service de géocodage inverse (OpenStreetMap)', url: 'https://nominatim.org/' },
101
+ { name: "OSRM — Open Source Routing Machine (optimisation d'itinéraires)", url: 'http://project-osrm.org/' },
102
+ ],
103
+ schemas: [faqSchema, howToSchema, appSchema],
104
+ seo: [
105
+ {
106
+ type: 'title',
107
+ text: "Optimiseur d'itinéraires intelligent : TSP pour livraisons et voyages",
108
+ level: 2,
109
+ },
110
+ {
111
+ type: 'paragraph',
112
+ html: "L'<strong>optimiseur d'itinéraires en ligne</strong> est un outil gratuit qui résout le classique <em>Problème du Voyageur de Commerce (TSP)</em>. Ajoutez tous vos arrêts dans n'importe quel ordre et l'algorithme calculera automatiquement la séquence la plus efficace pour minimiser la distance totale parcourue.",
113
+ },
114
+ {
115
+ type: 'title',
116
+ text: "Comment fonctionne l'algorithme d'optimisation d'itinéraires ?",
117
+ level: 2,
118
+ },
119
+ {
120
+ type: 'paragraph',
121
+ html: "L'outil utilise l'API <strong>OSRM (Open Source Routing Machine)</strong>, un moteur de routage haute performance basé sur les données OpenStreetMap. Le processus : d'abord il calcule l'itinéraire circulaire optimal entre tous les points (algorithme TSP), puis détermine le meilleur point de coupe pour le convertir en trajet linéaire aller simple, et enfin trace l'itinéraire sur la carte avec la distance totale estimée.",
122
+ },
123
+ {
124
+ type: 'title',
125
+ text: "Cas d'usage : livraisons, commerciaux et voyages",
126
+ level: 2,
127
+ },
128
+ {
129
+ type: 'paragraph',
130
+ html: "L'optimiseur d'itinéraires est idéal pour les <strong>livreurs indépendants</strong> qui doivent organiser plusieurs livraisons quotidiennes, les <strong>commerciaux terrain</strong> visitant des clients dans une zone, ou les <strong>voyageurs</strong> souhaitant visiter plusieurs villes ou monuments dans l'ordre le plus efficace. L'outil traite toutes les informations dans le navigateur, sans partager vos données avec aucun serveur propriétaire.",
131
+ },
132
+ {
133
+ type: 'stats',
134
+ columns: 2,
135
+ items: [
136
+ { value: 'TSP', label: 'Algorithme', icon: 'mdi:chart-line' },
137
+ { value: 'OSRM', label: 'Moteur de routage', icon: 'mdi:map-marker-path' },
138
+ { value: 'Local-First', label: 'Confidentialité', icon: 'mdi:lock-check' },
139
+ { value: 'Gratuit', label: 'Coût', icon: 'mdi:currency-eur-off' },
140
+ ],
141
+ },
142
+ {
143
+ type: 'title',
144
+ text: 'Confidentialité et traitement local',
145
+ level: 2,
146
+ },
147
+ {
148
+ type: 'paragraph',
149
+ html: "Toute la logique de l'outil s'exécute directement dans votre navigateur. Les coordonnées de vos arrêts sont uniquement envoyées aux APIs publiques OSRM et Nominatim pour calculer les itinéraires et obtenir les noms d'adresse, mais <strong>ne sont stockées sur aucun serveur propriétaire</strong>. Vous pouvez utiliser l'outil en toute sécurité pour planifier des itinéraires professionnels avec des informations sensibles.",
150
+ },
151
+ {
152
+ type: 'tip',
153
+ title: "Conseil d'utilisation",
154
+ html: "<strong>Pour de meilleurs résultats</strong>, ajoutez les arrêts en cliquant directement sur la carte plutôt qu'en cherchant des adresses. Les marqueurs sont déplaçables, vous pouvez donc ajuster la position exacte de chaque point après l'avoir placé.",
155
+ },
156
+ ],
157
+ };
@@ -0,0 +1,22 @@
1
+ import type { ToolDefinition, ToolsToolEntry } from '../../types';
2
+ import type { RoutesUI } from './ui';
3
+ import RoutesComponent from './component.astro';
4
+ import RoutesSEO from './seo.astro';
5
+ import RoutesBibliography from './bibliography.astro';
6
+
7
+ export const routes: ToolsToolEntry<RoutesUI> = {
8
+ id: 'routes',
9
+ icons: { bg: 'mdi:map-marker-path', fg: 'mdi:map-search' },
10
+ i18n: {
11
+ es: () => import('./i18n/es').then((m) => m.content),
12
+ en: () => import('./i18n/en').then((m) => m.content),
13
+ fr: () => import('./i18n/fr').then((m) => m.content),
14
+ },
15
+ };
16
+
17
+ export const ROUTES_TOOL: ToolDefinition = {
18
+ entry: routes,
19
+ Component: RoutesComponent,
20
+ SEOComponent: RoutesSEO,
21
+ BibliographyComponent: RoutesBibliography,
22
+ };
@@ -0,0 +1,60 @@
1
+ export interface GeocodingResult {
2
+ address: string;
3
+ name: string;
4
+ }
5
+
6
+ export interface GeocodingStrings {
7
+ addressUnknown: string;
8
+ errorAddress: string;
9
+ errorAddressName: string;
10
+ }
11
+
12
+ interface NominatimAddress {
13
+ road?: string;
14
+ house_number?: string;
15
+ suburb?: string;
16
+ city?: string;
17
+ town?: string;
18
+ village?: string;
19
+ }
20
+
21
+ interface NominatimResponse {
22
+ address?: NominatimAddress;
23
+ display_name: string;
24
+ }
25
+
26
+ function resolveCity(addr: NominatimAddress): string {
27
+ return addr.city ?? addr.town ?? addr.village ?? '';
28
+ }
29
+
30
+ function buildAddressString(addr: NominatimAddress, displayName: string): string {
31
+ const parts: string[] = [];
32
+ if (addr.road) parts.push(addr.road);
33
+ if (addr.house_number) parts.push(addr.house_number);
34
+ if (addr.suburb && !addr.road) parts.push(addr.suburb);
35
+ const city = resolveCity(addr);
36
+ if (city) parts.push(city);
37
+ return parts.join(', ') || (displayName.split(',')[0] ?? '');
38
+ }
39
+
40
+ export class GeocodingService {
41
+ private strings: GeocodingStrings;
42
+
43
+ constructor(strings: GeocodingStrings) {
44
+ this.strings = strings;
45
+ }
46
+
47
+ async getAddress(lat: number, lng: number): Promise<GeocodingResult> {
48
+ try {
49
+ const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18&addressdetails=1`;
50
+ const response = await fetch(url, { headers: { 'User-Agent': 'RutasApp/1.0' } });
51
+ const data = await response.json() as NominatimResponse;
52
+ const addressStr = data.address
53
+ ? buildAddressString(data.address, data.display_name)
54
+ : this.strings.addressUnknown;
55
+ return { address: addressStr, name: addressStr };
56
+ } catch {
57
+ return { address: this.strings.errorAddress, name: this.strings.errorAddressName };
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,192 @@
1
+ import type * as LeafletNS from 'leaflet';
2
+ import type { GeocodingService } from './GeocodingService';
3
+ import type { RouteService, OsrmWaypoint, OsrmLeg, OsrmRoute } from './RouteService';
4
+
5
+ declare const L: typeof LeafletNS;
6
+
7
+ export interface RoutePoint {
8
+ id: number;
9
+ lat: number;
10
+ lng: number;
11
+ marker: L.Marker;
12
+ name: string;
13
+ address?: string;
14
+ }
15
+
16
+ export interface RouteManagerStrings {
17
+ labelLoadingAddress: string;
18
+ labelPoint: string;
19
+ labelDeleteAria: string;
20
+ errorMinPoints: string;
21
+ errorCalculate: string;
22
+ }
23
+
24
+ export class RouteManager extends EventTarget {
25
+ private map: L.Map | null = null;
26
+ private points: RoutePoint[] = [];
27
+ private routeLine: L.GeoJSON | null = null;
28
+ private geocoding: GeocodingService;
29
+ private routing: RouteService;
30
+ private strings: RouteManagerStrings;
31
+
32
+ constructor(geocoding: GeocodingService, routing: RouteService, strings: RouteManagerStrings) {
33
+ super();
34
+ this.geocoding = geocoding;
35
+ this.routing = routing;
36
+ this.strings = strings;
37
+ }
38
+
39
+ initMap(elementId: string) {
40
+ const el = document.getElementById(elementId);
41
+ if (!el) return;
42
+ this.map = L.map(elementId).setView([40.416775, -3.70379], 6);
43
+ L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
44
+ attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
45
+ subdomains: 'abcd',
46
+ maxZoom: 20,
47
+ }).addTo(this.map);
48
+ this.map.on('click', (e: L.LeafletMouseEvent) => { this.addPoint(e.latlng.lat, e.latlng.lng); });
49
+ if ('geolocation' in navigator) {
50
+ navigator.geolocation.getCurrentPosition((pos) => {
51
+ this.map?.setView([pos.coords.latitude, pos.coords.longitude], 13);
52
+ });
53
+ }
54
+ }
55
+
56
+ private getNumberedIcon(number: number): L.DivIcon {
57
+ return L.divIcon({
58
+ className: 'rut-icon-container',
59
+ html: `<div class="rut-icon">${number}</div>`,
60
+ iconSize: [30, 30],
61
+ iconAnchor: [15, 36],
62
+ popupAnchor: [0, -36],
63
+ });
64
+ }
65
+
66
+ async addPoint(lat: number, lng: number) {
67
+ if (!this.map) return;
68
+ const id = Date.now();
69
+ const index = this.points.length + 1;
70
+ const marker = L.marker([lat, lng], { draggable: true, icon: this.getNumberedIcon(index) }).addTo(this.map);
71
+ const point: RoutePoint = { id, lat, lng, marker, name: `${this.strings.labelPoint} ${index}`, address: this.strings.labelLoadingAddress };
72
+ this.points.push(point);
73
+ this.notifyUpdate();
74
+ marker.on('dragend', () => {
75
+ const pos = marker.getLatLng();
76
+ point.lat = pos.lat;
77
+ point.lng = pos.lng;
78
+ this.clearRoute();
79
+ this.updateAddress(point);
80
+ });
81
+ await this.updateAddress(point);
82
+ }
83
+
84
+ private async updateAddress(point: RoutePoint) {
85
+ const result = await this.geocoding.getAddress(point.lat, point.lng);
86
+ point.address = result.address;
87
+ point.name = result.name;
88
+ this.bindMarkerPopup(point);
89
+ this.notifyUpdate();
90
+ }
91
+
92
+ private bindMarkerPopup(point: RoutePoint) {
93
+ const div = document.createElement('div');
94
+ div.className = 'rut-popup';
95
+ div.innerHTML = `<strong class="rut-popup-name">${point.name}</strong><p class="rut-popup-coords">Lat: ${point.lat.toFixed(4)}, Lng: ${point.lng.toFixed(4)}</p><button class="rut-popup-btn" id="rut-del-${point.id}">${this.strings.labelDeleteAria}</button>`;
96
+ point.marker.bindPopup(div);
97
+ point.marker.on('popupopen', () => {
98
+ document.getElementById(`rut-del-${point.id}`)?.addEventListener('click', () => this.deletePoint(point.id));
99
+ });
100
+ }
101
+
102
+ deletePoint(id: number) {
103
+ const index = this.points.findIndex((p) => p.id === id);
104
+ if (index === -1) return;
105
+ const point = this.points[index];
106
+ if (point) this.map?.removeLayer(point.marker);
107
+ this.points.splice(index, 1);
108
+ this.refreshMarkerIcons();
109
+ this.clearRoute();
110
+ this.notifyUpdate();
111
+ }
112
+
113
+ private refreshMarkerIcons() {
114
+ this.points.forEach((point, index) => { point.marker.setIcon(this.getNumberedIcon(index + 1)); });
115
+ }
116
+
117
+ clearAll() {
118
+ this.points.forEach((p) => this.map?.removeLayer(p.marker));
119
+ this.points = [];
120
+ this.clearRoute();
121
+ this.notifyUpdate();
122
+ }
123
+
124
+ private clearRoute() {
125
+ if (this.routeLine && this.map) {
126
+ this.map.removeLayer(this.routeLine);
127
+ this.routeLine = null;
128
+ }
129
+ this.dispatchEvent(new CustomEvent('routeCleared'));
130
+ }
131
+
132
+ private findCutIndex(legs: OsrmLeg[]): number {
133
+ let maxIdx = 0;
134
+ let maxDist = -1;
135
+ legs.forEach((leg, idx) => {
136
+ if (leg.distance > maxDist) { maxDist = leg.distance; maxIdx = idx; }
137
+ });
138
+ return maxIdx;
139
+ }
140
+
141
+ private reorderPoints(waypoints: OsrmWaypoint[], legs: OsrmLeg[]): RoutePoint[] {
142
+ const mapped = waypoints
143
+ .map((wp) => (wp.waypoint_index !== undefined && wp.waypoint_index < this.points.length ? this.points[wp.waypoint_index] : undefined))
144
+ .filter((p): p is RoutePoint => p !== undefined);
145
+ if (mapped.length !== this.points.length) return this.points;
146
+ const startIdx = (this.findCutIndex(legs) + 1) % mapped.length;
147
+ return [...mapped.slice(startIdx), ...mapped.slice(0, startIdx)];
148
+ }
149
+
150
+ private drawRouteGeometry(route: OsrmRoute) {
151
+ this.clearRoute();
152
+ const feature: GeoJSON.Feature = { type: 'Feature', properties: {}, geometry: route.geometry as GeoJSON.Geometry };
153
+ this.routeLine = L.geoJSON(feature, {
154
+ style: { color: '#0891b2', weight: 5, opacity: 0.8, lineCap: 'round', lineJoin: 'round' },
155
+ }).addTo(this.map!);
156
+ this.map?.fitBounds(this.routeLine.getBounds(), { padding: [50, 50] });
157
+ }
158
+
159
+ async optimizeRoute() {
160
+ if (this.points.length < 2) {
161
+ this.dispatchEvent(new CustomEvent('error', { detail: this.strings.errorMinPoints }));
162
+ return;
163
+ }
164
+ this.dispatchEvent(new CustomEvent('loading', { detail: true }));
165
+ try {
166
+ const { waypoints, trip } = await this.routing.optimizeRoute(this.points);
167
+ this.points = this.reorderPoints(waypoints, trip.legs);
168
+ this.refreshMarkerIcons();
169
+ this.notifyUpdate();
170
+ const route = await this.routing.getRoute(this.points);
171
+ this.drawRouteGeometry(route);
172
+ this.dispatchEvent(new CustomEvent('routeCalculated', { detail: { distance: route.distance } }));
173
+ } catch (error) {
174
+ console.error(error);
175
+ this.dispatchEvent(new CustomEvent('error', { detail: this.strings.errorCalculate }));
176
+ } finally {
177
+ this.dispatchEvent(new CustomEvent('loading', { detail: false }));
178
+ }
179
+ }
180
+
181
+ panToPoint(id: number) {
182
+ const point = this.points.find((p) => p.id === id);
183
+ if (point && this.map) {
184
+ this.map.flyTo([point.lat, point.lng], 16);
185
+ point.marker.openPopup();
186
+ }
187
+ }
188
+
189
+ private notifyUpdate() {
190
+ this.dispatchEvent(new CustomEvent('update', { detail: this.points }));
191
+ }
192
+ }
@@ -0,0 +1,66 @@
1
+ export interface OsrmWaypoint {
2
+ waypoint_index?: number;
3
+ location: [number, number];
4
+ }
5
+
6
+ export interface OsrmLeg {
7
+ distance: number;
8
+ duration: number;
9
+ }
10
+
11
+ export interface OsrmTrip {
12
+ legs: OsrmLeg[];
13
+ distance: number;
14
+ }
15
+
16
+ export interface OsrmGeometry {
17
+ type: string;
18
+ coordinates: [number, number][];
19
+ }
20
+
21
+ export interface OsrmRoute {
22
+ geometry: OsrmGeometry;
23
+ distance: number;
24
+ }
25
+
26
+ export interface OptimizationResult {
27
+ waypoints: OsrmWaypoint[];
28
+ trip: OsrmTrip;
29
+ }
30
+
31
+ interface OsrmTripResponse {
32
+ code: string;
33
+ message?: string;
34
+ waypoints: OsrmWaypoint[];
35
+ trips: OsrmTrip[];
36
+ }
37
+
38
+ interface OsrmRouteResponse {
39
+ code: string;
40
+ message?: string;
41
+ routes: OsrmRoute[];
42
+ }
43
+
44
+ export class RouteService {
45
+ async optimizeRoute(points: { lat: number; lng: number }[]): Promise<OptimizationResult> {
46
+ const coords = points.map((p) => `${p.lng},${p.lat}`).join(';');
47
+ const url = `https://router.project-osrm.org/trip/v1/driving/${coords}?source=any&roundtrip=true&geometries=geojson&overview=full`;
48
+ const response = await fetch(url);
49
+ const data = await response.json() as OsrmTripResponse;
50
+ if (data.code !== 'Ok') throw new Error(data.message ?? 'Route optimization failed');
51
+ const trip = data.trips[0];
52
+ if (!trip) throw new Error('No trip returned');
53
+ return { waypoints: data.waypoints, trip };
54
+ }
55
+
56
+ async getRoute(points: { lat: number; lng: number }[]): Promise<OsrmRoute> {
57
+ const coords = points.map((p) => `${p.lng},${p.lat}`).join(';');
58
+ const url = `https://router.project-osrm.org/route/v1/driving/${coords}?geometries=geojson&overview=full`;
59
+ const response = await fetch(url);
60
+ const data = await response.json() as OsrmRouteResponse;
61
+ if (data.code !== 'Ok') throw new Error(data.message ?? 'Route calculation failed');
62
+ const route = data.routes[0];
63
+ if (!route) throw new Error('No route returned');
64
+ return route;
65
+ }
66
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { routes } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await routes.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SEORenderer content={{ locale, sections: content.seo }} />}