renusify 2.5.2 → 3.0.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 (212) hide show
  1. package/components/app/index.vue +74 -22
  2. package/components/app/toast/index.vue +76 -71
  3. package/components/app/toast/toast.vue +62 -44
  4. package/components/avatar/index.vue +208 -84
  5. package/components/button/buttonConfirm.vue +53 -26
  6. package/components/button/buttonGroup.js +0 -2
  7. package/components/button/buttonGroup.vue +310 -62
  8. package/components/button/index.vue +584 -100
  9. package/components/calendar/index.js +0 -2
  10. package/components/calendar/index.vue +326 -262
  11. package/components/calendar/month.vue +64 -55
  12. package/components/calendar/year.vue +30 -25
  13. package/components/card/index.vue +139 -59
  14. package/components/codeEditor/highlightCss.vue +38 -39
  15. package/components/codeEditor/highlightHtml.vue +64 -64
  16. package/components/codeEditor/highlightJs.vue +37 -38
  17. package/components/codeEditor/index.vue +129 -79
  18. package/components/codeEditor/run.vue +225 -39
  19. package/components/codeEditor/useCodeFormatter.js +150 -0
  20. package/components/confirm/index.vue +139 -80
  21. package/components/container/col.vue +5 -4
  22. package/components/container/divider.vue +28 -19
  23. package/components/container/index.vue +34 -15
  24. package/components/container/row.vue +26 -9
  25. package/components/container/spacer.vue +2 -4
  26. package/components/container/style.scss +3 -0
  27. package/components/content/index.vue +49 -32
  28. package/components/cropper/index.vue +401 -244
  29. package/components/float/index.vue +542 -415
  30. package/components/form/addressInput/index.vue +184 -109
  31. package/components/form/camInput/index.vue +370 -244
  32. package/components/form/checkInput/index.vue +138 -71
  33. package/components/form/checkboxInput/index.vue +87 -47
  34. package/components/form/colorInput/Alpha.vue +81 -83
  35. package/components/form/colorInput/Hue.vue +91 -68
  36. package/components/form/colorInput/Preview.vue +43 -47
  37. package/components/form/colorInput/Saturation.vue +101 -86
  38. package/components/form/colorInput/index.vue +71 -39
  39. package/components/form/colorInput/picker.vue +111 -106
  40. package/components/form/colorInput/useColor.js +153 -0
  41. package/components/form/dateInput/index.vue +691 -356
  42. package/components/form/dateInput/month.vue +63 -54
  43. package/components/form/dateInput/year.vue +35 -25
  44. package/components/form/fileInput/index.js +0 -1
  45. package/components/form/fileInput/index.vue +263 -106
  46. package/components/form/fileInput/single.vue +323 -164
  47. package/components/form/groupInput/index.vue +199 -101
  48. package/components/form/index.vue +189 -83
  49. package/components/form/input/index.vue +416 -377
  50. package/components/form/jsonInput/JsonView.vue +54 -56
  51. package/components/form/jsonInput/index.vue +247 -165
  52. package/components/form/maskInput/index.vue +252 -132
  53. package/components/form/numberInput/index.js +0 -1
  54. package/components/form/numberInput/index.vue +226 -117
  55. package/components/form/passwordInput/index.js +2 -1
  56. package/components/form/passwordInput/index.vue +269 -102
  57. package/components/form/radioInput/index.vue +143 -72
  58. package/components/form/rangeInput/index.vue +280 -167
  59. package/components/form/ratingInput/index.vue +57 -57
  60. package/components/form/selectInput/index.js +1 -3
  61. package/components/form/selectInput/index.vue +584 -296
  62. package/components/form/switchInput/index.vue +73 -59
  63. package/components/form/telInput/index.js +0 -1
  64. package/components/form/telInput/index.vue +238 -135
  65. package/components/form/textArea/index.vue +72 -35
  66. package/components/form/textEditor/index.vue +739 -0
  67. package/components/form/{text-editor → textEditor}/style.scss +8 -16
  68. package/components/form/textInput/index.vue +54 -32
  69. package/components/form/timeInput/index.vue +82 -55
  70. package/components/form/timeInput/range.vue +115 -94
  71. package/components/form/timeInput/timepicker.vue +382 -449
  72. package/components/form/uniqueInput/index.vue +105 -48
  73. package/components/form/unitInput/index.vue +139 -84
  74. package/components/formCreator/index.js +0 -1
  75. package/components/formCreator/index.vue +314 -148
  76. package/components/highlight/index.vue +41 -25
  77. package/components/highlight/style.scss +2 -2
  78. package/components/highlight/{mixin.js → useHighlight.js} +181 -160
  79. package/components/icon/index.vue +79 -33
  80. package/components/img/index.vue +249 -147
  81. package/components/img/preview.vue +180 -198
  82. package/components/img/svgImg.vue +42 -39
  83. package/components/index.js +5 -20
  84. package/components/infinite/index.js +1 -2
  85. package/components/infinite/index.vue +248 -66
  86. package/components/map/index.vue +428 -261
  87. package/components/map/route.vue +794 -487
  88. package/components/map/select.vue +118 -58
  89. package/components/menu/index.vue +201 -91
  90. package/components/meta/meta.js +26 -3
  91. package/components/modal/index.vue +382 -156
  92. package/components/notify/index.vue +204 -86
  93. package/components/notify/notification.vue +38 -55
  94. package/components/progress/circle.vue +189 -70
  95. package/components/progress/line.vue +266 -46
  96. package/components/searchBox/index.js +1 -3
  97. package/components/searchBox/index.vue +194 -101
  98. package/components/skeleton/index.vue +45 -20
  99. package/components/slider/index.vue +318 -156
  100. package/components/swiper/index.vue +254 -106
  101. package/components/table/crud/footer.vue +77 -53
  102. package/components/table/crud/header.vue +71 -72
  103. package/components/table/crud/index.vue +629 -399
  104. package/components/table/index.vue +721 -278
  105. package/components/timeAgo/index.vue +145 -96
  106. package/components/tour/index.vue +338 -235
  107. package/components/tree/index.vue +235 -89
  108. package/components/tree/tree-element.vue +106 -106
  109. package/directive/animate/index.js +77 -0
  110. package/directive/clickOutSide/index.js +98 -0
  111. package/directive/drag/index.js +153 -0
  112. package/directive/index.js +11 -13
  113. package/directive/intersect/index.js +263 -0
  114. package/directive/mask/index.js +67 -0
  115. package/directive/parallax/index.js +78 -0
  116. package/directive/ripple/index.js +14 -0
  117. package/directive/scroll/index.js +244 -0
  118. package/directive/sortable/index.js +274 -0
  119. package/directive/title/index.js +75 -0
  120. package/directive/touch/index.js +268 -0
  121. package/index.js +10 -8
  122. package/package.json +5 -2
  123. package/plugins/validation/Validate.js +88 -79
  124. package/scripts/generate-docs.mjs +226 -0
  125. package/scripts/menu.mjs +240 -0
  126. package/scripts/parser.mjs +1086 -0
  127. package/style/_index.scss +7 -0
  128. package/style/app.scss +13 -65
  129. package/style/colors.scss +5 -22
  130. package/style/functions/index.scss +8 -0
  131. package/style/mixins/index.scss +17 -5
  132. package/style/variables/base.scss +154 -175
  133. package/style/variables/color.scss +0 -12
  134. package/style/variables/utilities.scss +0 -180
  135. package/tools/helper.js +0 -8
  136. package/tools/icons.js +6 -1
  137. package/tools/root.js +71 -0
  138. package/components/app/style.scss +0 -41
  139. package/components/app/toast/style.scss +0 -20
  140. package/components/avatar/style.scss +0 -32
  141. package/components/bar/bottomNav.js +0 -1
  142. package/components/bar/bottomNav.vue +0 -28
  143. package/components/bar/bottomNavigationCircle.js +0 -2
  144. package/components/bar/bottomNavigationCircle.vue +0 -99
  145. package/components/bar/scss/bottomNav.scss +0 -67
  146. package/components/bar/scss/toolbar.scss +0 -174
  147. package/components/bar/toolbar/index.js +0 -8
  148. package/components/bar/toolbar/index.vue +0 -35
  149. package/components/bar/toolbar/laptop.vue +0 -33
  150. package/components/bar/toolbar/menuChilds.vue +0 -41
  151. package/components/bar/toolbar/menuLaptop.vue +0 -41
  152. package/components/bar/toolbar/menuMob.vue +0 -39
  153. package/components/bar/toolbar/mixin.js +0 -43
  154. package/components/bar/toolbar/mobile.vue +0 -34
  155. package/components/breadcrumb/bredcrumbItem.vue +0 -39
  156. package/components/breadcrumb/index.js +0 -3
  157. package/components/breadcrumb/index.vue +0 -71
  158. package/components/breadcrumb/style.scss +0 -51
  159. package/components/button/style.scss +0 -411
  160. package/components/card/style.scss +0 -86
  161. package/components/chart/chart.js +0 -1
  162. package/components/chart/chart.vue +0 -69
  163. package/components/chart/worldMap.js +0 -2
  164. package/components/chart/worldMap.vue +0 -1112
  165. package/components/chat/MessageList.vue +0 -163
  166. package/components/chat/chatInput.vue +0 -150
  167. package/components/chat/chatMsg.vue +0 -276
  168. package/components/chat/index.js +0 -11
  169. package/components/chat/index.vue +0 -113
  170. package/components/chip/index.js +0 -3
  171. package/components/chip/index.vue +0 -77
  172. package/components/chip/style.scss +0 -199
  173. package/components/codeEditor/mixin.js +0 -145
  174. package/components/countdown/index.js +0 -1
  175. package/components/countdown/index.vue +0 -105
  176. package/components/form/colorInput/mixin.js +0 -132
  177. package/components/form/fileInput/file.js +0 -148
  178. package/components/form/telInput/assets/flags.png +0 -0
  179. package/components/form/telInput/assets/flags@2x.png +0 -0
  180. package/components/form/text-editor/index.vue +0 -705
  181. package/components/icon/style.scss +0 -17
  182. package/components/infinite/div.js +0 -6
  183. package/components/infinite/div.vue +0 -193
  184. package/components/infinite/page.js +0 -3
  185. package/components/infinite/page.vue +0 -105
  186. package/components/list/index.js +0 -3
  187. package/components/list/index.vue +0 -122
  188. package/components/list/style.scss +0 -66
  189. package/components/message/index.js +0 -4
  190. package/components/message/index.vue +0 -40
  191. package/components/modal/style.scss +0 -146
  192. package/components/nestable/NestableItem.vue +0 -307
  193. package/components/nestable/editable.js +0 -44
  194. package/components/nestable/index.js +0 -1
  195. package/components/nestable/index.vue +0 -226
  196. package/components/nestable/methods.js +0 -416
  197. package/components/progress/style.scss +0 -229
  198. package/components/table/style.scss +0 -338
  199. package/components/tabs/index.js +0 -3
  200. package/components/tabs/index.vue +0 -151
  201. package/components/timeline/index.js +0 -6
  202. package/components/timeline/index.vue +0 -76
  203. package/directive/resize/index.js +0 -30
  204. package/directive/skeleton/index.js +0 -27
  205. package/directive/skeleton/style.scss +0 -37
  206. package/plugins/request/Request.js +0 -68
  207. package/style/animation.scss +0 -94
  208. package/style/style.scss +0 -8
  209. package/tools/rootable.js +0 -75
  210. /package/components/form/{text-editor → textEditor}/index.js +0 -0
  211. /package/components/form/{text-editor → textEditor}/preview.js +0 -0
  212. /package/components/form/{text-editor → textEditor}/preview.vue +0 -0
@@ -7,12 +7,26 @@
7
7
  </r-col>
8
8
  <r-col :style="'height: '+height" class="overflow-y-auto col-12 sm-3">
9
9
  <div class="pa-2">
10
+ <!--
11
+ @slot Header slot for custom route summary display
12
+ @binding {String} distance - Formatted distance string (e.g., "15.25 km")
13
+ @binding {String} time - Human-readable time duration (e.g., "2 hours 15 minutes")
14
+ -->
10
15
  <slot :distance="distance" :time="time" name="header">
11
16
  <div class="bodey-1">{{ $helper.ifHas(modelValue, '------', 'summary', 'name') }}</div>
12
17
  <r-divider class="my-3"></r-divider>
13
18
  <div>{{ time || '---' }}</div>
14
19
  <div>{{ distance || '0 km' }}</div>
15
20
  </slot>
21
+
22
+ <!--
23
+ @slot Points slot for custom waypoints list display
24
+ @binding {Function} add - Function to add a new waypoint
25
+ @binding {Function} del - Function to delete a waypoint
26
+ @binding {Function} open - Function to open a waypoint on the map
27
+ @binding {Object} points - Object containing all waypoints
28
+ @binding {Function} to - Function to reorder waypoints
29
+ -->
16
30
  <slot :add="add" :del="del" :open="open" :points="points" :to="to" name="points">
17
31
  <transition-group name="slide-up">
18
32
  <div v-for="(point,i) in points"
@@ -45,525 +59,818 @@
45
59
  <span>{{ $t('map_new_point', 'renusify') }}</span>
46
60
  </r-btn>
47
61
  </slot>
62
+
63
+ <!--
64
+ @slot Footer slot for additional content below the points list
65
+ -->
48
66
  <slot name="footer"></slot>
49
67
  </div>
50
68
  </r-col>
51
69
  </r-row>
52
70
  </r-container>
53
71
  </template>
54
- <script>
55
- export default {
56
- name: 'r-map-route',
57
- props: {
58
- modelValue: Object,
59
- by: {
60
- type: String, default: 'car', validator: function (value) {
61
- return ['car', 'foot', 'bike'].indexOf(value) !== -1
62
- }
63
- },
64
- height: {type: String, default: "500px"},
65
- marginTime: {type: Number, default: 1},
66
- color: {
67
- type: Array, default: () => {
68
- return []
69
- }
70
- },
71
- maxPoints: {type: Number, default: 3},
72
- darkMode: Boolean,
73
- editable: Boolean
72
+
73
+ <script setup>
74
+ import {ref, computed, onMounted, inject} from 'vue'
75
+
76
+ const props = defineProps({
77
+ /**
78
+ * Route data model value
79
+ * @type {Object}
80
+ * @model
81
+ * @description Contains route data including points, summary, and waypoints
82
+ * @property {Object} points - Waypoint coordinates keyed by unique IDs
83
+ * @property {Object} summary - Route summary with name, time, and distance
84
+ * @property {Array} waypoints - Detailed waypoint information
85
+ * @example
86
+ * {
87
+ * points: { 'abc123': [35.6997, 51.3380], 'def456': [35.7000, 51.3400] },
88
+ * summary: { name: 'Tehran Route', time: 3600, distance: 5000 },
89
+ * waypoints: [{ name: 'Start Point' }, { name: 'End Point' }]
90
+ * }
91
+ */
92
+ modelValue: Object,
93
+
94
+ /**
95
+ * Transportation mode for route calculation
96
+ * @type {String}
97
+ * @default 'car'
98
+ * @validator ['car', 'foot', 'bike']
99
+ * @description Determines which OSRM profile to use for routing
100
+ * - 'car': Optimized for car travel (fastest route)
101
+ * - 'foot': Optimized for walking (pedestrian paths)
102
+ * - 'bike': Optimized for cycling (bike lanes, lower gradients)
103
+ */
104
+ by: {
105
+ type: String,
106
+ default: 'car',
107
+ validator: (value) => ['car', 'foot', 'bike'].includes(value)
74
108
  },
75
- emits: ['update:modelValue'],
76
- data() {
77
- return {
78
- loading: false,
79
- timeout_id: null,
80
- req_id: null,
81
- map: null,
82
- L: null,
83
- center: [],
84
- markers: null,
85
- lines: null,
86
- routing: {},
87
- points: this.$helper.ifHas(this.modelValue, {}, 'points')
88
- }
109
+
110
+ /**
111
+ * Map container height
112
+ * @type {String}
113
+ * @default '500px'
114
+ * @description CSS height for the map and sidebar container
115
+ * @example '400px'
116
+ * @example '80vh'
117
+ * @example '100%'
118
+ */
119
+ height: {type: String, default: "500px"},
120
+
121
+ /**
122
+ * Time margin multiplier for route duration
123
+ * @type {Number}
124
+ * @default 1
125
+ * @description Multiplier applied to calculated route time for buffer/planning
126
+ * @example 1.2 // Adds 20% buffer to estimated time
127
+ * @example 1.5 // Adds 50% buffer to estimated time
128
+ */
129
+ marginTime: {type: Number, default: 1},
130
+
131
+ /**
132
+ * Route line colors
133
+ * @type {Array<String>}
134
+ * @default []
135
+ * @description Array of CSS color values for route segments between waypoints
136
+ * If array length < segments, colors cycle through available values
137
+ * @example ['#FF0000', '#00FF00'] // Red first segment, green second segment
138
+ * @example ['blue'] // All segments blue
139
+ */
140
+ color: {
141
+ type: Array,
142
+ default: () => []
89
143
  },
90
- created() {
91
- this.get()
92
- if (!this.$r.icons.map_marker) {
93
- this.$r.icons.map_marker = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24\" viewBox="0 0 24 24"><path fill="currentColor" d="M12 6.5A2.5 2.5 0 0 1 14.5 9a2.5 2.5 0 0 1-2.5 2.5A2.5 2.5 0 0 1 9.5 9A2.5 2.5 0 0 1 12 6.5M12 2a7 7 0 0 1 7 7c0 5.25-7 13-7 13S5 14.25 5 9a7 7 0 0 1 7-7m0 2a5 5 0 0 0-5 5c0 1 0 3 5 9.71C17 12 17 10 17 9a5 5 0 0 0-5-5Z"/></svg>'
144
+
145
+ /**
146
+ * Maximum number of waypoints allowed
147
+ * @type {Number}
148
+ * @default 3
149
+ * @description Limits the number of waypoints that can be added to the route
150
+ * @example 5 // Allows up to 5 waypoints
151
+ * @example 10 // Allows up to 10 waypoints
152
+ */
153
+ maxPoints: {type: Number, default: 3},
154
+
155
+ /**
156
+ * Enable dark mode for the map
157
+ * @type {Boolean}
158
+ * @default false
159
+ * @description Applies dark theme to the map display
160
+ */
161
+ darkMode: Boolean,
162
+
163
+ /**
164
+ * Enable waypoint editing features
165
+ * @type {Boolean}
166
+ * @default false
167
+ * @description When true, allows adding, deleting, dragging, and reordering waypoints
168
+ */
169
+ editable: Boolean
170
+ })
171
+
172
+ const emit = defineEmits([
173
+ /**
174
+ * Updated route data object
175
+ */
176
+ 'update:modelValue'
177
+ ])
178
+
179
+ // Inject renusify dependencies
180
+ const {$r, $helper, $t} = inject('renusify')
181
+
182
+ // Reactive state
183
+ const loading = ref(false)
184
+ const timeout_id = ref(null)
185
+ const req_id = ref(null)
186
+ const map = ref(null)
187
+ const L = ref(null)
188
+ const center = ref([])
189
+ const markers = ref(null)
190
+ const lines = ref(null)
191
+ const routing = ref({})
192
+ const points = ref($helper.ifHas(props.modelValue, {}, 'points'))
193
+
194
+ /**
195
+ * Computed array of waypoint IDs
196
+ * @type {ComputedRef<Array<String>>}
197
+ */
198
+ const points_keys = computed(() => Object.keys(points.value))
199
+
200
+ /**
201
+ * Computed array of waypoint coordinates
202
+ * @type {ComputedRef<Array<Array<Number>>>}
203
+ */
204
+ const points_vals = computed(() => Object.values(points.value))
205
+
206
+ /**
207
+ * Computed human-readable route duration
208
+ * @type {ComputedRef<String>}
209
+ */
210
+ const time = computed(() => {
211
+ return timeHumanity($helper.ifHas(props.modelValue, 0, 'summary', 'time'))
212
+ })
213
+
214
+ /**
215
+ * Computed formatted route distance
216
+ * @type {ComputedRef<String>}
217
+ */
218
+ const distance = computed(() => {
219
+ if (!$helper.ifHas(props.modelValue, false, 'summary', 'distance')) {
220
+ return ''
221
+ }
222
+ return ((props.modelValue.summary.distance / 1000).toFixed(2)) + ' km'
223
+ })
224
+
225
+ /**
226
+ * Computed origin marker icon
227
+ * @type {ComputedRef<String>}
228
+ */
229
+ const iconOrigin = computed(() => require('./images/origin.png'))
230
+
231
+ /**
232
+ * Computed intermediate point marker icon
233
+ * @type {ComputedRef<String>}
234
+ */
235
+ const iconPoint = computed(() => require('./images/point.png'))
236
+
237
+ /**
238
+ * Computed destination marker icon
239
+ * @type {ComputedRef<String>}
240
+ */
241
+ const iconDestination = computed(() => require('./images/destination.png'))
242
+
243
+ // Initialize component
244
+ onMounted(() => {
245
+ get()
246
+ })
247
+
248
+ // Methods
249
+
250
+ /**
251
+ * Get display position label for a waypoint
252
+ * @method pos
253
+ * @param {Number} i - Waypoint index
254
+ * @returns {String} Position label (e.g., "Origin", "Point 2", "Destination")
255
+ */
256
+ const pos = (i) => {
257
+ let n = i + 1
258
+ if (i === 0) {
259
+ n = $t('map_origin', 'renusify')
260
+ } else if (i === points_vals.value.length - 1) {
261
+ n = $t('map_destination', 'renusify')
262
+ }
263
+ return $t(['map_point', [n]], 'renusify')
264
+ }
265
+
266
+ /**
267
+ * Get full name/label for a waypoint
268
+ * @method name
269
+ * @param {Number} i - Waypoint index
270
+ * @returns {String} Complete waypoint name with position
271
+ */
272
+ const name = (i) => {
273
+ return pos(i) + ': ' + $helper.ifHas(props.modelValue, '', 'waypoints', i, 'name')
274
+ }
275
+
276
+ /**
277
+ * Open a waypoint's marker popup and center map on it
278
+ * @method open
279
+ * @param {Number} i - Waypoint index to open
280
+ */
281
+ const open = (i) => {
282
+ if (markers.value && markers.value._layers) {
283
+ const markerLayers = Object.values(markers.value._layers)
284
+ if (markerLayers[i]) {
285
+ center.value = Object.values(markerLayers[i]._latlng)
286
+ markerLayers[i].openPopup()
94
287
  }
95
- },
96
- computed: {
97
- points_keys() {
98
- return Object.keys(this.points)
99
- },
100
- points_vals() {
101
- return Object.values(this.points)
102
- },
103
- time() {
104
- return this.timeHumanity(this.$helper.ifHas(this.modelValue, 0, 'summary', 'time'))
105
- },
106
- distance() {
107
- if (!this.$helper.ifHas(this.modelValue, false, 'summary', 'distance')) {
108
- return ''
109
- }
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Delete a waypoint from the route
293
+ * @method del
294
+ * @param {String} i - Waypoint ID to delete
295
+ */
296
+ const del = (i) => {
297
+ delete points.value[i]
298
+ get()
299
+ }
110
300
 
111
- return ((this.modelValue.summary.distance / 1000).toFixed(2)) + ' km'
112
- },
113
- iconOrigin() {
114
- return require('./images/origin.png')
115
- },
116
- iconPoint() {
117
- return require('./images/point.png')
118
- },
119
- iconDestination() {
120
- return require('./images/destination.png')
301
+ /**
302
+ * Move an item in an array from one position to another
303
+ * @method array_move
304
+ * @param {Array} arr - Source array
305
+ * @param {Number} old - Original index
306
+ * @param {Number} new_pos - New index
307
+ * @returns {Array} Modified array
308
+ * @private
309
+ */
310
+ const array_move = (arr, old, new_pos) => {
311
+ const result = [...arr]
312
+ if (new_pos >= result.length) {
313
+ let k = new_pos - result.length + 1
314
+ while (k--) {
315
+ result.push(undefined)
121
316
  }
122
- },
123
- methods: {
124
- pos(i) {
125
- let n = i + 1
126
- if (i === 0) {
127
- n = this.$t('map_origin', 'renusify')
128
- } else if (i === this.points_vals.length - 1) {
129
- n = this.$t('map_destination', 'renusify')
130
- }
131
- return this.$t(['map_point', [n]], 'renusify')
132
- },
133
- name(i) {
134
- return this.pos(i) + ': ' + this.$helper.ifHas(this.modelValue, '', 'waypoints', i, 'name')
135
- },
136
- open(i) {
137
- this.center = Object.values(Object.values(this.markers._layers)[i]._latlng)
138
- Object.values(this.markers._layers)[i].openPopup()
139
- },
140
- del(i) {
141
- delete this.points[i]
142
- this.get()
143
- },
144
- array_move(arr, old, new_pos) {
145
- if (new_pos >= arr.length) {
146
- var k = new_pos - arr.length + 1;
147
- while (k--) {
148
- arr.push(undefined);
149
- }
150
- }
151
- arr.splice(new_pos, 0, arr.splice(old, 1)[0]);
152
- return arr
153
- },
154
- to(i, pos) {
155
- const val = this.array_move(this.points_vals, i, pos)
156
- const key = this.array_move(this.points_keys, i, pos)
157
- const way = this.array_move(this.modelValue.waypoints, i, pos)
158
- let res = {}
159
- key.forEach((item, i) => {
160
- res[item] = val[i]
161
- })
162
- this.points = res
163
- this.$emit('update:modelValue', {
164
- 'points': this.points,
165
- 'summary': this.modelValue.summary,
166
- 'waypoints': way
167
- })
168
- setTimeout(() => {
169
- this.get()
170
- }, 800)
171
- },
172
- openLastPopup(n = 0) {
173
- clearTimeout(this.timeout_id)
174
- if (Object.values(this.markers._layers)[this.points_vals.length - 1] === undefined) {
175
- this.timeout_id = setTimeout(() => {
176
- if (n < 10) {
177
- this.openLastPopup(n + 1)
178
- }
179
- }, 1000)
180
- return
181
- }
182
- Object.values(this.markers._layers)[this.points_vals.length - 1].openPopup()
183
- },
184
- add() {
185
- this.loading = true
186
- this.points[this.$helper.uniqueId(12)] = Object.values(this.map.getCenter())
187
- this.get()
188
- this.openLastPopup()
189
- this.loading = false
190
- },
191
- timeHumanity(seconds) {
192
- const numyears = Math.floor(seconds / 31536000);
193
- const numdays = Math.floor((seconds % 31536000) / 86400);
194
- const numhours = Math.floor(((seconds % 31536000) % 86400) / 3600);
195
- const numminutes = Math.ceil((((seconds % 31536000) % 86400) % 3600) / 60);
196
- let res = ''
197
- if (numyears > 0) {
198
- res += numyears + ' ' + this.$t("years", 'renusify')
199
- }
200
- if (numdays > 0) {
201
- res += ' ' + numdays + ' ' + this.$t("days", 'renusify')
202
- }
203
- if (numhours > 0) {
204
- res += ' ' + numhours + ' ' + this.$t("hours", 'renusify')
205
- }
206
- if (numminutes > 0) {
207
- res += ' ' + numminutes + ' ' + this.$t("minutes", 'renusify')
208
- }
209
- return res
210
- },
211
- handleLEvent(e) {
212
- this.L = e
213
- },
214
- handleMapEvent(e) {
215
- this.map = e
216
- if (this.points_vals.length > 0) {
217
- this.center = this.points_vals[0]
218
- }
219
- },
220
- drawMarkers() {
221
- if (!this.L) {
222
- return
223
- }
224
- if (this.markers) {
225
- this.map.removeLayer(this.markers)
226
- }
227
- const all = []
228
- const that = this
229
-
230
- this.points_vals.forEach((point, i) => {
231
- let icon = this.iconPoint
232
- if (i === 0) {
233
- icon = this.iconOrigin
234
- } else if (i === this.points_vals.length - 1) {
235
- icon = this.iconDestination
236
- }
237
- let m = new this.L.Marker(point, {
238
- draggable: this.editable,
239
- index: this.points_keys[i],
240
- icon: new this.L.icon({
241
- iconUrl: icon,
242
- iconSize: [50, 50],
243
- iconAnchor: [25, 50],
244
- popupAnchor: [0, -40]
245
- })
246
- })
247
- m.bindPopup(this.$t('map_move_marker', 'renusify') + '<hr>' + this.name(i))
248
- m.on('drag', function (e) {
249
- that.points[e.target.options.index] = Object.values(e.latlng)
250
- });
251
- m.on('dragend', function (e) {
252
- that.get()
253
- });
254
- all.push(m)
255
- })
256
- this.markers = this.L.layerGroup(all);
257
- this.map.addLayer(this.markers);
258
- },
259
- draw() {
260
- if (this.routing.code !== 'Ok' || !this.L || !this.routing.routes) {
261
- return
262
- }
317
+ }
318
+ result.splice(new_pos, 0, result.splice(old, 1)[0])
319
+ return result
320
+ }
263
321
 
264
- let alts = {'points': this.points, 'routes': [], 'summary': {'name': '', 'time': 0, 'distance': 0}}
265
- const lng = Object.values(this.routing.routes).length
266
- for (let i = 0; i < lng; i++) {
267
- let route = this._convertRoute(this.routing.routes[i]);
268
- alts.routes.push(route);
269
- alts.summary.name = route['name']
270
- alts.summary.time += parseFloat(route.summary.total_time) * this.marginTime
271
- alts.summary.distance += parseFloat(route.summary.total_distance)
272
- }
273
- alts.waypoints = this.routing.waypoints
274
- alts.waypoints.forEach((item) => {
275
- delete item.hint
276
- delete item.location
277
- })
322
+ /**
323
+ * Reorder waypoints in the route
324
+ * @method to
325
+ * @param {Number} i - Current waypoint index
326
+ * @param {Number} pos - New position index
327
+ */
328
+ const to = (i, pos) => {
329
+ const val = array_move(points_vals.value, i, pos)
330
+ const key = array_move(points_keys.value, i, pos)
331
+ const way = array_move(props.modelValue.waypoints || [], i, pos)
332
+
333
+ const res = {}
334
+ key.forEach((item, idx) => {
335
+ res[item] = val[idx]
336
+ })
337
+
338
+ points.value = res
339
+ emit('update:modelValue', {
340
+ 'points': points.value,
341
+ 'summary': props.modelValue?.summary || {},
342
+ 'waypoints': way
343
+ })
344
+
345
+ setTimeout(() => {
346
+ get()
347
+ }, 800)
348
+ }
278
349
 
279
- this.$emit('update:modelValue', {
280
- 'points': alts.points,
281
- 'summary': alts.summary,
282
- 'waypoints': alts.waypoints
283
- })
284
- let l = []
285
- alts.routes[0].waypointIndices.forEach((item, i) => {
286
- if (i < alts.routes[0].waypointIndices.length - 1) {
287
- const p = alts.routes[0].coordinates.slice(item, alts.routes[0].waypointIndices[i + 1])
288
- let color = 'red'
289
- if (this.color[i]) {
290
- color = this.color[i]
291
- } else if (this.color.length > 0) {
292
- color = i % 2 === 0 ? this.color[0] : (this.color[1] || this.color[0])
293
- }
294
-
295
- l.push(new L.polyline(p, {color: color, weight: 5}))
296
- }
297
- })
298
- this.lines = L.layerGroup(l);
299
- setTimeout(() => {
300
- this.map.addLayer(this.lines);
301
- this.drawMarkers()
302
- }, 1)
303
- },
304
- get() {
305
- this.$emit('update:modelValue', {
306
- 'points': this.points,
307
- 'summary': {},
308
- 'waypoints': []
309
- })
310
- this.drawMarkers()
311
- if (this.lines) {
312
- this.map.removeLayer(this.lines);
313
- }
314
- if (this.points_vals.length < 2) {
315
- return
350
+ /**
351
+ * Open the last waypoint's popup after adding a new point
352
+ * @method openLastPopup
353
+ * @param {Number} n - Retry counter (internal use)
354
+ * @private
355
+ */
356
+ const openLastPopup = (n = 0) => {
357
+ clearTimeout(timeout_id.value)
358
+
359
+ if (!markers.value || !markers.value._layers ||
360
+ Object.values(markers.value._layers)[points_vals.value.length - 1] === undefined) {
361
+ timeout_id.value = setTimeout(() => {
362
+ if (n < 10) {
363
+ openLastPopup(n + 1)
316
364
  }
317
- const that = this
318
- let url = 'https://routing.openstreetmap.de/routed-' + this.by + '/route/v1/driving/'
319
- this.points_vals.forEach(point => {
320
- url += point[1] + ',' + point[0] + ';'
321
- })
322
- url = url.substr(0, url.length - 1) + '?overview=false&steps=true'
323
-
324
- this.corslite(url, function (err, resp) {
325
- if (!err) {
326
- try {
327
- that.routing = JSON.parse(resp.responseText);
328
- that.draw()
329
- } catch (e) {
330
- console.error('Error parsing OSRM response: ' + e.toString())
331
- }
332
- } else {
333
- console.error('HTTP request failed: ' + err.type +
334
- (err.target && err.target.status ? ' HTTP ' + err.target.status + ': ' + err.target.statusText : ''))
335
- }
365
+ }, 1000)
366
+ return
367
+ }
368
+
369
+ Object.values(markers.value._layers)[points_vals.value.length - 1].openPopup()
370
+ }
371
+
372
+ /**
373
+ * Add a new waypoint at the current map center
374
+ * @method add
375
+ */
376
+ const add = () => {
377
+ loading.value = true
378
+ points.value[$helper.uniqueId(12)] = Object.values(map.value.getCenter())
379
+ get()
380
+ openLastPopup()
381
+ loading.value = false
382
+ }
383
+
384
+ /**
385
+ * Convert seconds to human-readable time string
386
+ * @method timeHumanity
387
+ * @param {Number} seconds - Time in seconds
388
+ * @returns {String} Formatted time string
389
+ * @example 3661 → "1 hour 1 minute"
390
+ */
391
+ const timeHumanity = (seconds) => {
392
+ if (!seconds) return ''
393
+
394
+ const numyears = Math.floor(seconds / 31536000)
395
+ const numdays = Math.floor((seconds % 31536000) / 86400)
396
+ const numhours = Math.floor(((seconds % 31536000) % 86400) / 3600)
397
+ const numminutes = Math.ceil((((seconds % 31536000) % 86400) % 3600) / 60)
398
+
399
+ let res = ''
400
+ if (numyears > 0) {
401
+ res += numyears + ' ' + $t("years", 'renusify')
402
+ }
403
+ if (numdays > 0) {
404
+ res += ' ' + numdays + ' ' + $t("days", 'renusify')
405
+ }
406
+ if (numhours > 0) {
407
+ res += ' ' + numhours + ' ' + $t("hours", 'renusify')
408
+ }
409
+ if (numminutes > 0) {
410
+ res += ' ' + numminutes + ' ' + $t("minutes", 'renusify')
411
+ }
412
+ return res.trim()
413
+ }
414
+
415
+ /**
416
+ * Handle Leaflet.js library instance
417
+ * @method handleLEvent
418
+ * @param {Object} e - Leaflet library instance
419
+ */
420
+ const handleLEvent = (e) => {
421
+ L.value = e
422
+ }
423
+
424
+ /**
425
+ * Handle map instance ready event
426
+ * @method handleMapEvent
427
+ * @param {Object} e - Leaflet map instance
428
+ */
429
+ const handleMapEvent = (e) => {
430
+ map.value = e
431
+ if (points_vals.value.length > 0) {
432
+ center.value = points_vals.value[0]
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Draw markers for all waypoints on the map
438
+ * @method drawMarkers
439
+ * @private
440
+ */
441
+ const drawMarkers = () => {
442
+ if (!L.value || !map.value) return
443
+
444
+ if (markers.value) {
445
+ map.value.removeLayer(markers.value)
446
+ }
447
+
448
+ const all = []
449
+
450
+ points_vals.value.forEach((point, i) => {
451
+ let icon = iconPoint.value
452
+ if (i === 0) {
453
+ icon = iconOrigin.value
454
+ } else if (i === points_vals.value.length - 1) {
455
+ icon = iconDestination.value
456
+ }
457
+
458
+ const m = new L.value.Marker(point, {
459
+ draggable: props.editable,
460
+ index: points_keys.value[i],
461
+ icon: new L.value.icon({
462
+ iconUrl: icon,
463
+ iconSize: [50, 50],
464
+ iconAnchor: [25, 50],
465
+ popupAnchor: [0, -40]
336
466
  })
337
- },
338
- corslite(url, callback) {
339
- if (typeof window.XMLHttpRequest === 'undefined') {
340
- return callback(Error('Browser not supported'));
341
- }
342
- if (this.req_id) {
343
- this.req_id.abort()
344
- }
345
- let x = new window.XMLHttpRequest();
467
+ })
346
468
 
347
- function isSuccessful(status) {
348
- return status >= 200 && status < 300 || status === 304;
349
- }
469
+ m.bindPopup($t('map_move_marker', 'renusify') + '<hr>' + name(i))
350
470
 
351
- function loaded() {
352
- if (
353
- // XDomainRequest
354
- x.status === undefined ||
355
- // modern browsers
356
- isSuccessful(x.status)) callback.call(x, null, x);
357
- else callback.call(x, x, null);
358
- }
471
+ m.on('drag', function (e) {
472
+ points.value[e.target.options.index] = Object.values(e.latlng)
473
+ })
359
474
 
360
- if ('onload' in x) {
361
- x.onload = loaded;
362
- } else {
363
- x.onreadystatechange = function readystate() {
364
- if (x.readyState === 4) {
365
- loaded();
366
- }
367
- };
368
- }
369
- x.timeout = 7000;
370
- x.onerror = function error(e) {
371
- callback.call(this, e || true, null);
372
- callback = function () {
373
- };
374
- };
375
-
376
- x.onprogress = function () {
377
- };
378
-
379
- x.ontimeout = function (e) {
380
- callback.call(this, e, null);
381
- callback = function () {
382
- };
383
- };
384
-
385
- x.onabort = function (e) {
386
- callback.call(this, e, null);
387
- callback = function () {
388
- };
389
- };
390
-
391
- x.open('GET', url, true);
392
-
393
- x.send(null);
394
- this.req_id = x
395
- return x;
396
- },
397
- _convertRoute: function (responseRoute) {
398
- var result = {
399
- name: '',
400
- coordinates: [],
401
- instructions: [],
402
- summary: {
403
- total_distance: responseRoute.distance,
404
- total_time: responseRoute.duration
405
- }
406
- },
407
- legNames = [],
408
- waypointIndices = [],
409
- index = 0,
410
- legCount = responseRoute.legs.length,
411
- hasSteps = responseRoute.legs[0].steps.length > 0,
412
- i,
413
- j,
414
- leg,
415
- step,
416
- geometry,
417
- type,
418
- modifier,
419
- text
420
-
421
-
422
- for (i = 0; i < legCount; i++) {
423
- leg = responseRoute.legs[i];
424
- legNames.push(leg.summary && leg.summary.charAt(0).toUpperCase() + leg.summary.substring(1));
425
- const lng = leg.steps.length
426
- for (j = 0; j < lng; j++) {
427
- step = leg.steps[j];
428
- geometry = this._decodePolyline(step.geometry);
429
- result.coordinates.push.apply(result.coordinates, geometry);
430
- type = this._maneuverToInstructionType(step.maneuver, i === legCount - 1);
431
- modifier = this._maneuverToModifier(step.maneuver);
432
- text = [step, {legCount: legCount, legIndex: i}];
433
-
434
- if (type) {
435
- if ((i == 0 && step.maneuver.type == 'depart') || step.maneuver.type == 'arrive') {
436
- waypointIndices.push(index);
437
- }
438
-
439
- result.instructions.push({
440
- type: type,
441
- distance: step.distance,
442
- time: step.duration,
443
- road: step.name,
444
- direction: this._bearingToDirection(step.maneuver.bearing_after),
445
- exit: step.maneuver.exit,
446
- index: index,
447
- mode: step.mode,
448
- modifier: modifier,
449
- text: text
450
- });
451
- }
452
-
453
- index += geometry.length;
475
+ m.on('dragend', function () {
476
+ get()
477
+ })
478
+
479
+ all.push(m)
480
+ })
481
+
482
+ markers.value = L.value.layerGroup(all)
483
+ map.value.addLayer(markers.value)
484
+ }
485
+
486
+ /**
487
+ * Draw route lines on the map based on calculated route
488
+ * @method draw
489
+ * @private
490
+ */
491
+ const draw = () => {
492
+ if (routing.value.code !== 'Ok' || !L.value || !routing.value.routes) {
493
+ return
494
+ }
495
+
496
+ const alts = {
497
+ 'points': points.value,
498
+ 'routes': [],
499
+ 'summary': {'name': '', 'time': 0, 'distance': 0}
500
+ }
501
+
502
+ const routesLength = Object.values(routing.value.routes).length
503
+ for (let i = 0; i < routesLength; i++) {
504
+ const route = convertRoute(routing.value.routes[i])
505
+ alts.routes.push(route)
506
+ alts.summary.name = route.name
507
+ alts.summary.time += parseFloat(route.summary.total_time) * props.marginTime
508
+ alts.summary.distance += parseFloat(route.summary.total_distance)
509
+ }
510
+
511
+ alts.waypoints = routing.value.waypoints || []
512
+ alts.waypoints.forEach((item) => {
513
+ delete item.hint
514
+ delete item.location
515
+ })
516
+
517
+ emit('update:modelValue', {
518
+ 'points': alts.points,
519
+ 'summary': alts.summary,
520
+ 'waypoints': alts.waypoints
521
+ })
522
+
523
+ const l = []
524
+ if (alts.routes[0]?.waypointIndices) {
525
+ alts.routes[0].waypointIndices.forEach((item, i) => {
526
+ if (i < alts.routes[0].waypointIndices.length - 1) {
527
+ const p = alts.routes[0].coordinates.slice(item, alts.routes[0].waypointIndices[i + 1])
528
+ let color = 'red'
529
+ if (props.color[i]) {
530
+ color = props.color[i]
531
+ } else if (props.color.length > 0) {
532
+ color = i % 2 === 0 ? props.color[0] : (props.color[1] || props.color[0])
454
533
  }
455
- }
456
534
 
457
- result.name = legNames.join(', ');
458
- if (!hasSteps) {
459
- result.coordinates = this._decodePolyline(responseRoute.geometry);
460
- } else {
461
- result.waypointIndices = waypointIndices;
535
+ l.push(new L.value.polyline(p, {color: color, weight: 5}))
462
536
  }
537
+ })
538
+ }
463
539
 
464
- return result;
465
- },
466
- _bearingToDirection: function (bearing) {
467
- var oct = Math.round(bearing / 45) % 8;
468
- return ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'][oct];
469
- },
470
- _maneuverToInstructionType: function (maneuver, lastLeg) {
471
- switch (maneuver.type) {
472
- case 'new name':
473
- return 'continue';
474
- case 'depart':
475
- return 'head';
476
- case 'arrive':
477
- return lastLeg ? 'destinationReached' : 'waypointReached';
478
- case 'roundabout':
479
- case 'rotary':
480
- return 'roundabout';
481
- case 'merge':
482
- case 'fork':
483
- case 'on ramp':
484
- case 'off ramp':
485
- case 'end of road':
486
- return this.$helper.replacer(maneuver.type, ' ', '_');
487
- default:
488
- return this.$helper.replacer(maneuver.modifier, ' ', '_');
489
- }
490
- },
491
- _maneuverToModifier: function (maneuver) {
492
- var modifier = maneuver.modifier;
493
-
494
- switch (maneuver.type) {
495
- case 'merge':
496
- case 'fork':
497
- case 'on ramp':
498
- case 'off ramp':
499
- case 'end of road':
500
- modifier = this._leftOrRight(modifier);
540
+ lines.value = L.value.layerGroup(l)
541
+ setTimeout(() => {
542
+ if (map.value && lines.value) {
543
+ map.value.addLayer(lines.value)
544
+ }
545
+ drawMarkers()
546
+ }, 1)
547
+ }
548
+
549
+ /**
550
+ * Calculate route by fetching from OSRM API
551
+ * @method get
552
+ */
553
+ const get = () => {
554
+ emit('update:modelValue', {
555
+ 'points': points.value,
556
+ 'summary': {},
557
+ 'waypoints': []
558
+ })
559
+
560
+ drawMarkers()
561
+
562
+ if (lines.value && map.value) {
563
+ map.value.removeLayer(lines.value)
564
+ }
565
+
566
+ if (points_vals.value.length < 2) {
567
+ return
568
+ }
569
+
570
+ let url = `https://routing.openstreetmap.de/routed-${props.by}/route/v1/driving/`
571
+ points_vals.value.forEach(point => {
572
+ url += point[1] + ',' + point[0] + ';'
573
+ })
574
+ url = url.substr(0, url.length - 1) + '?overview=false&steps=true'
575
+
576
+ corslite(url, function (err, resp) {
577
+ if (!err) {
578
+ try {
579
+ routing.value = JSON.parse(resp.responseText)
580
+ draw()
581
+ } catch (e) {
582
+ console.error('Error parsing OSRM response: ' + e.toString())
501
583
  }
584
+ } else {
585
+ console.error('HTTP request failed: ' + err.type +
586
+ (err.target && err.target.status ? ' HTTP ' + err.target.status + ': ' + err.target.statusText : ''))
587
+ }
588
+ })
589
+ }
590
+
591
+ /**
592
+ * Cross-origin request helper function
593
+ * @method corslite
594
+ * @param {String} url - Request URL
595
+ * @param {Function} callback - Callback function (err, response)
596
+ * @returns {XMLHttpRequest} XHR object
597
+ * @private
598
+ */
599
+ const corslite = (url, callback) => {
600
+ if (typeof window.XMLHttpRequest === 'undefined') {
601
+ return callback(Error('Browser not supported'))
602
+ }
603
+
604
+ if (req_id.value) {
605
+ req_id.value.abort()
606
+ }
607
+
608
+ const x = new window.XMLHttpRequest()
502
609
 
503
- return modifier && this.$helper.replacer(modifier, ' ', '_');
504
- },
505
- _leftOrRight(d) {
506
- return d.indexOf('left') >= 0 ? 'left' : 'right';
507
- },
508
- polyline_decode(str, precision) {
509
- var index = 0,
510
- lat = 0,
511
- lng = 0,
512
- coordinates = [],
513
- shift = 0,
514
- result = 0,
515
- byte = null,
516
- latitude_change,
517
- longitude_change,
518
- factor = Math.pow(10, precision || 5);
519
-
520
- while (index < str.length) {
521
-
522
- // Reset shift, result, and byte
523
- byte = null;
524
- shift = 0;
525
- result = 0;
526
-
527
- do {
528
- byte = str.charCodeAt(index++) - 63;
529
- result |= (byte & 0x1f) << shift;
530
- shift += 5;
531
- } while (byte >= 0x20);
532
-
533
- latitude_change = ((result & 1) ? ~(result >> 1) : (result >> 1));
534
-
535
- shift = result = 0;
536
-
537
- do {
538
- byte = str.charCodeAt(index++) - 63;
539
- result |= (byte & 0x1f) << shift;
540
- shift += 5;
541
- } while (byte >= 0x20);
542
-
543
- longitude_change = ((result & 1) ? ~(result >> 1) : (result >> 1));
544
-
545
- lat += latitude_change;
546
- lng += longitude_change;
547
-
548
- coordinates.push([lat / factor, lng / factor]);
610
+ function isSuccessful(status) {
611
+ return status >= 200 && status < 300 || status === 304
612
+ }
613
+
614
+ function loaded() {
615
+ if (x.status === undefined || isSuccessful(x.status)) {
616
+ callback.call(x, null, x)
617
+ } else {
618
+ callback.call(x, x, null)
619
+ }
620
+ }
621
+
622
+ if ('onload' in x) {
623
+ x.onload = loaded
624
+ } else {
625
+ x.onreadystatechange = function readystate() {
626
+ if (x.readyState === 4) {
627
+ loaded()
549
628
  }
629
+ }
630
+ }
631
+
632
+ x.timeout = 7000
633
+ x.onerror = function error(e) {
634
+ callback.call(this, e || true, null)
635
+ }
550
636
 
551
- return coordinates;
552
- },
553
- _decodePolyline: function (routeGeometry) {
554
- let cs = this.polyline_decode(routeGeometry, 5),
555
- result = [],
556
- i;
557
- const lng = cs.length;
558
- for (i = lng - 1; i >= 0; i--) {
559
- result[i] = L.latLng(cs[i]);
637
+ x.onprogress = function () {
638
+ }
639
+
640
+ x.ontimeout = function (e) {
641
+ callback.call(this, e, null)
642
+ }
643
+
644
+ x.onabort = function (e) {
645
+ callback.call(this, e, null)
646
+ }
647
+
648
+ x.open('GET', url, true)
649
+ x.send(null)
650
+
651
+ req_id.value = x
652
+ return x
653
+ }
654
+
655
+ /**
656
+ * Convert OSRM API response to internal route format
657
+ * @method convertRoute
658
+ * @param {Object} responseRoute - OSRM route object
659
+ * @returns {Object} Formatted route object
660
+ * @private
661
+ */
662
+ const convertRoute = (responseRoute) => {
663
+ const result = {
664
+ name: '',
665
+ coordinates: [],
666
+ instructions: [],
667
+ summary: {
668
+ total_distance: responseRoute.distance,
669
+ total_time: responseRoute.duration
670
+ }
671
+ }
672
+
673
+ const legNames = []
674
+ const waypointIndices = []
675
+ let index = 0
676
+ const legCount = responseRoute.legs.length
677
+ const hasSteps = legCount > 0 && responseRoute.legs[0].steps.length > 0
678
+
679
+ for (let i = 0; i < legCount; i++) {
680
+ const leg = responseRoute.legs[i]
681
+ legNames.push(leg.summary && leg.summary.charAt(0).toUpperCase() + leg.summary.substring(1))
682
+
683
+ const stepCount = leg.steps.length
684
+ for (let j = 0; j < stepCount; j++) {
685
+ const step = leg.steps[j]
686
+ const geometry = decodePolyline(step.geometry)
687
+ result.coordinates.push(...geometry)
688
+
689
+ const type = maneuverToInstructionType(step.maneuver, i === legCount - 1)
690
+ const modifier = maneuverToModifier(step.maneuver)
691
+ const text = [step, {legCount: legCount, legIndex: i}]
692
+
693
+ if (type) {
694
+ if ((i == 0 && step.maneuver.type == 'depart') || step.maneuver.type == 'arrive') {
695
+ waypointIndices.push(index)
696
+ }
697
+
698
+ result.instructions.push({
699
+ type: type,
700
+ distance: step.distance,
701
+ time: step.duration,
702
+ road: step.name,
703
+ direction: bearingToDirection(step.maneuver.bearing_after),
704
+ exit: step.maneuver.exit,
705
+ index: index,
706
+ mode: step.mode,
707
+ modifier: modifier,
708
+ text: text
709
+ })
560
710
  }
561
711
 
562
- return result;
712
+ index += geometry.length
563
713
  }
564
714
  }
715
+
716
+ result.name = legNames.join(', ')
717
+ if (!hasSteps) {
718
+ result.coordinates = decodePolyline(responseRoute.geometry)
719
+ } else {
720
+ result.waypointIndices = waypointIndices
721
+ }
722
+
723
+ return result
724
+ }
725
+
726
+ /**
727
+ * Convert bearing (degrees) to cardinal direction
728
+ * @method bearingToDirection
729
+ * @param {Number} bearing - Bearing in degrees (0-360)
730
+ * @returns {String} Cardinal direction (N, NE, E, SE, S, SW, W, NW)
731
+ * @private
732
+ */
733
+ const bearingToDirection = (bearing) => {
734
+ const oct = Math.round(bearing / 45) % 8
735
+ return ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'][oct]
736
+ }
737
+
738
+ /**
739
+ * Convert OSRM maneuver type to instruction type
740
+ * @method maneuverToInstructionType
741
+ * @param {Object} maneuver - OSRM maneuver object
742
+ * @param {Boolean} lastLeg - Whether this is the last leg of the route
743
+ * @returns {String} Instruction type
744
+ * @private
745
+ */
746
+ const maneuverToInstructionType = (maneuver, lastLeg) => {
747
+ switch (maneuver.type) {
748
+ case 'new name':
749
+ return 'continue'
750
+ case 'depart':
751
+ return 'head'
752
+ case 'arrive':
753
+ return lastLeg ? 'destinationReached' : 'waypointReached'
754
+ case 'roundabout':
755
+ case 'rotary':
756
+ return 'roundabout'
757
+ case 'merge':
758
+ case 'fork':
759
+ case 'on ramp':
760
+ case 'off ramp':
761
+ case 'end of road':
762
+ return maneuver.type.replaceAll(' ', '_')
763
+ default:
764
+ return maneuver.modifier?.replaceAll(' ', '_') || ''
765
+ }
766
+ }
767
+
768
+ /**
769
+ * Convert OSRM maneuver modifier to internal modifier format
770
+ * @method maneuverToModifier
771
+ * @param {Object} maneuver - OSRM maneuver object
772
+ * @returns {String} Modified instruction modifier
773
+ * @private
774
+ */
775
+ const maneuverToModifier = (maneuver) => {
776
+ let modifier = maneuver.modifier
777
+
778
+ switch (maneuver.type) {
779
+ case 'merge':
780
+ case 'fork':
781
+ case 'on ramp':
782
+ case 'off ramp':
783
+ case 'end of road':
784
+ modifier = leftOrRight(modifier)
785
+ }
786
+
787
+ return modifier && modifier.replaceAll(' ', '_')
788
+ }
789
+
790
+ /**
791
+ * Determine left or right direction from modifier string
792
+ * @method leftOrRight
793
+ * @param {String} d - Direction modifier string
794
+ * @returns {String} 'left' or 'right'
795
+ * @private
796
+ */
797
+ const leftOrRight = (d) => {
798
+ return d?.indexOf('left') >= 0 ? 'left' : 'right'
799
+ }
800
+
801
+ /**
802
+ * Decode polyline string to coordinate array (OSRM geometry format)
803
+ * @method polyline_decode
804
+ * @param {String} str - Encoded polyline string
805
+ * @param {Number} precision - Coordinate precision (default: 5)
806
+ * @returns {Array<Array<Number>>} Array of [lat, lng] coordinates
807
+ * @private
808
+ */
809
+ const polyline_decode = (str, precision) => {
810
+ let index = 0,
811
+ lat = 0,
812
+ lng = 0,
813
+ coordinates = [],
814
+ shift = 0,
815
+ result = 0,
816
+ byte = null,
817
+ latitude_change,
818
+ longitude_change,
819
+ factor = Math.pow(10, precision || 5)
820
+
821
+ while (index < str.length) {
822
+ // Reset shift, result, and byte
823
+ byte = null
824
+ shift = 0
825
+ result = 0
826
+
827
+ do {
828
+ byte = str.charCodeAt(index++) - 63
829
+ result |= (byte & 0x1f) << shift
830
+ shift += 5
831
+ } while (byte >= 0x20)
832
+
833
+ latitude_change = ((result & 1) ? ~(result >> 1) : (result >> 1))
834
+
835
+ shift = result = 0
836
+
837
+ do {
838
+ byte = str.charCodeAt(index++) - 63
839
+ result |= (byte & 0x1f) << shift
840
+ shift += 5
841
+ } while (byte >= 0x20)
842
+
843
+ longitude_change = ((result & 1) ? ~(result >> 1) : (result >> 1))
844
+
845
+ lat += latitude_change
846
+ lng += longitude_change
847
+
848
+ coordinates.push([lat / factor, lng / factor])
849
+ }
850
+
851
+ return coordinates
852
+ }
853
+
854
+ /**
855
+ * Decode polyline and convert to Leaflet LatLng objects
856
+ * @method decodePolyline
857
+ * @param {String} routeGeometry - Encoded polyline geometry string
858
+ * @returns {Array<L.LatLng>} Array of Leaflet LatLng objects
859
+ * @private
860
+ */
861
+ const decodePolyline = (routeGeometry) => {
862
+ const cs = polyline_decode(routeGeometry, 5),
863
+ result = []
864
+
865
+ const lng = cs.length
866
+ for (let i = lng - 1; i >= 0; i--) {
867
+ result[i] = L.value.latLng(cs[i])
868
+ }
869
+
870
+ return result
565
871
  }
566
872
  </script>
873
+
567
874
  <style lang="scss">
568
875
  @use "../../style/variables/base";
569
876