PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 9.1.1
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v9.1.1
9.1.2 9.1.1 9.1.0 9.0.3 9.0.2 9.0.1 9.0.0 8.5.79 8.5.78 8.5.77 8.5.76 8.5.75 8.5.74 8.5.73 8.5.72 8.5.71 8.5.70 8.5.69 8.5.68 8.5.35 8.5.36 8.5.37 8.5.38 8.5.39 8.5.4 All 221 releases
wpvr / src / Api / Transformers / TourTransformer.php

TourTransformer.php in WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress 9.1.1, at src/Api/Transformers/TourTransformer.php

659 lines 37.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace RexTheme\WPVR\Api\Transformers;
4
5 use RexTheme\WPVR\Api\Contracts\TransformerInterface;
6
7 class TourTransformer implements TransformerInterface {
8
9 protected bool $is_pro;
10
11 public function __construct( bool $is_pro = false ) {
12 $this->is_pro = $is_pro;
13 }
14
15 /**
16 * Convert raw panodata PHP array → normalized API JSON shape.
17 */
18 public function toApi( array $raw ): array {
19 if ( ! $this->is_pro ) {
20 $raw = wpvr_get_effective_panodata( $raw );
21 }
22
23 $scenes = $this->scenes_to_api( $raw['panodata']['scene-list'] ?? [] );
24 // Enabled: check new key first, then fall back to legacy `autoRotate` speed (truthy = enabled).
25 $auto_rotate_enabled = ( $raw['autorotate-enabled'] ?? '' ) === 'on' || ! empty( $raw['autoRotate'] );
26 // Speed: new key first, then legacy `autoRotate` (which doubles as speed in old tours).
27 $auto_rotate_speed = isset( $raw['autorotate-speed'] )
28 ? (float) $raw['autorotate-speed']
29 : ( isset( $raw['autoRotate'] ) ? (float) $raw['autoRotate'] : -5 );
30 // Delay: new key first, then legacy `autoRotateInactivityDelay`.
31 $auto_rotate_delay = isset( $raw['autorotate-delay'] )
32 ? (int) $raw['autorotate-delay']
33 : ( isset( $raw['autoRotateInactivityDelay'] ) ? (int) $raw['autoRotateInactivityDelay'] : 2000 );
34 $auto_rotate = [
35 'enabled' => $auto_rotate_enabled,
36 'speed' => $auto_rotate_speed,
37 'delay' => $auto_rotate_delay,
38 'stopDelay' => isset( $raw['autorotationstopdelay'] ) && $raw['autorotationstopdelay'] !== ''
39 ? (int) $raw['autorotationstopdelay']
40 : ( isset( $raw['autoRotateStopDelay'] ) && $raw['autoRotateStopDelay'] !== '' ? (int) $raw['autoRotateStopDelay'] : null ),
41 ];
42
43 $default_scene_id = ! empty( $raw['defaultscene'] ) ? $raw['defaultscene'] : null;
44 $has_explicit_default_scene = ( $raw['defaultscene-auto'] ?? 'off' ) !== 'on';
45
46 return [
47 'defaultSceneId' => $has_explicit_default_scene ? $default_scene_id : null,
48 'tourType' => $this->detect_tour_type( $raw ),
49 'settings' => [
50 'autoLoad' => ! empty( $raw['autoLoad'] ),
51 'showControls' => isset( $raw['showControls'] ) ? (bool) $raw['showControls'] : true,
52 'previewText' => $raw['previewtext'] ?? '',
53 'previewImage' => $raw['preview'] ?? '',
54 'sceneFadeDuration' => isset( $raw['scenefadeduration'] ) ? (int) $raw['scenefadeduration'] : 0,
55 'showSceneInfo' => ( $raw['scene-info-enabled'] ?? 'on' ) !== 'off',
56 'autoRotate' => $auto_rotate,
57 ],
58 'floorPlan' => $this->floor_plan_to_api( $raw ),
59 'backgroundTour' => [
60 'enabled' => ( $raw['bg_tour_enabler'] ?? 'off' ) === 'on',
61 'title' => $raw['bg_tour_title'] ?? '',
62 'subtitle' => $raw['bg_tour_subtitle'] ?? '',
63 ],
64 'videoData' => [
65 'url' => $raw['vidurl'] ?? '',
66 'autoplay' => ( $raw['video-autoplay'] ?? $raw['autoplay'] ?? 'off' ) === 'on',
67 'loop' => ( $raw['video-loop'] ?? $raw['loop'] ?? 'off' ) === 'on',
68 ],
69 'streetViewData' => [
70 'embedUrl' => $raw['streetviewurl'] ?? '',
71 ],
72 'scenes' => $scenes,
73 'advancedSettings' => $this->advanced_settings_to_api( $raw ),
74 ];
75 }
76
77 /**
78 * Convert normalized API JSON → raw panodata PHP array.
79 */
80 public function fromApi( array $data ): array {
81 $settings = $data['settings'] ?? [];
82 $floor_plan = $data['floorPlan'] ?? [];
83 $bg_tour = $data['backgroundTour'] ?? [];
84 $auto_rotate = $settings['autoRotate'] ?? [];
85
86 $video_data = $data['videoData'] ?? [];
87 $street_view_data = $data['streetViewData'] ?? [];
88 $advanced_control = is_array( $data['advancedControl'] ?? null )
89 ? $data['advancedControl']
90 : ( is_array( $data['proData'] ?? null ) ? $data['proData'] : [] );
91 $tour_type = in_array( $data['tourType'] ?? 'image', [ 'image', 'video', 'street-view' ], true )
92 ? $data['tourType']
93 : 'image';
94 if ( ! $this->is_pro && $tour_type === 'street-view' ) {
95 $tour_type = 'image';
96 $street_view_data = [];
97 }
98
99 $scenes = is_array( $data['scenes'] ?? null ) ? $data['scenes'] : [];
100 $requested_default_scene_id = (string) ( $data['defaultSceneId'] ?? '' );
101 $has_explicit_default_scene = $requested_default_scene_id !== ''
102 && ! empty( array_filter(
103 $scenes,
104 static function ( $scene ) use ( $requested_default_scene_id ) {
105 return is_array( $scene ) && ( $scene['id'] ?? '' ) === $requested_default_scene_id;
106 }
107 ) );
108 $default_scene_id = $has_explicit_default_scene
109 ? $requested_default_scene_id
110 : ( $scenes[0]['id'] ?? '' );
111
112 $raw = [
113 'autoLoad' => ! empty( $settings['autoLoad'] ) && $settings['autoLoad'] !== 'off' && $settings['autoLoad'] !== 'false',
114 'showControls' => ! empty( $settings['showControls'] ),
115 'draggable' => ( isset( $advanced_control['draggable'] ) && ( $advanced_control['draggable'] === 'off' || $advanced_control['draggable'] === false ) ) ? 'off' : 'on',
116 'mouseZoom' => ( isset( $advanced_control['mouseZoom'] ) && ( $advanced_control['mouseZoom'] === 'off' || $advanced_control['mouseZoom'] === false ) ) ? 'off' : 'on',
117 'diskeyboard' => ( isset( $advanced_control['diskeyboard'] ) && ( $advanced_control['diskeyboard'] === 'off' || $advanced_control['diskeyboard'] === false ) ) ? 'on' : 'off',
118 'keyboardzoom' => ( isset( $advanced_control['keyboardzoom'] ) && ( $advanced_control['keyboardzoom'] === 'off' || $advanced_control['keyboardzoom'] === false ) ) ? false : true,
119 'previewtext' => $settings['previewText'] ?? '',
120 'scenefadeduration' => isset( $settings['sceneFadeDuration'] ) ? (string) $settings['sceneFadeDuration'] : '0',
121 'scene-info-enabled' => array_key_exists( 'showSceneInfo', $settings ) && ! $settings['showSceneInfo'] ? 'off' : 'on',
122 'defaultscene' => $default_scene_id,
123 'defaultscene-auto' => $has_explicit_default_scene ? 'off' : 'on',
124 'autorotate-enabled' => ! empty( $auto_rotate['enabled'] ) ? 'on' : 'off',
125 'autorotate-speed' => $auto_rotate['speed'] ?? -5,
126 'autorotate-delay' => $auto_rotate['delay'] ?? 2000,
127 'autorotationstopdelay' => $auto_rotate['stopDelay'] ?? '',
128 // Legacy keys for PRO plugin / shortcode compatibility.
129 'autoRotate' => ! empty( $auto_rotate['enabled'] ) ? ( $auto_rotate['speed'] ?? -5 ) : '',
130 'autoRotateInactivityDelay' => $auto_rotate['delay'] ?? 2000,
131 'autoRotateStopDelay' => $auto_rotate['stopDelay'] ?? '',
132 'floorplan-enabled' => ! empty( $floor_plan['enabled'] ) ? 'on' : 'off',
133 'floorplan-image' => $floor_plan['imageUrl'] ?? '',
134 'floorplan-compass' => ! empty( $floor_plan['directionIndicator']['enabled'] ) ? 'on' : 'off',
135 'floorplan-compass-color' => $floor_plan['directionIndicator']['color'] ?? '#6D28D9',
136 // Legacy keys — used by legacy rendering and pro plugin.
137 'floor_plan_tour_enabler' => ! empty( $floor_plan['enabled'] ) ? 'on' : 'off',
138 'floor_plan_attachment_url' => $floor_plan['imageUrl'] ?? '',
139 'floor_plan_custom_color' => ! empty( $floor_plan['pointerColor'] ) ? $floor_plan['pointerColor'] : '#cca92c',
140 'floor_plan_direction_indicator' => ! empty( $floor_plan['directionIndicator']['enabled'] ) ? 'on' : 'off',
141 'floor_plan_pointer_position' => $this->pointer_positions_from_api( $floor_plan ),
142 'floor_plan_data_list' => $this->pointer_data_list_from_api( $floor_plan ),
143 'bg_tour_enabler' => ! empty( $bg_tour['enabled'] ) ? 'on' : 'off',
144 'bg_tour_title' => $bg_tour['title'] ?? '',
145 'bg_tour_subtitle' => $bg_tour['subtitle'] ?? '',
146 'genericform' => ! empty( $advanced_control['genericform'] ) && $advanced_control['genericform'] !== 'off' ? 'on' : 'off',
147 'genericformshortcode' => sanitize_text_field( (string) ( $advanced_control['genericformshortcode'] ?? '' ) ),
148 'genericformicon' => sanitize_text_field( (string) ( $advanced_control['genericformicon'] ?? 'fab fa-wpforms' ) ),
149 'genericformiconcolor' => sanitize_hex_color( $advanced_control['genericformiconcolor'] ?? '' ) ?: '#f7fffb',
150 'calltoaction' => ! empty( $advanced_control['calltoaction'] ) && $advanced_control['calltoaction'] !== 'off' ? 'on' : 'off',
151 'buttontext' => sanitize_text_field( (string) ( $advanced_control['buttontext'] ?? 'Click Here' ) ),
152 'buttonurl' => esc_url_raw( (string) ( $advanced_control['buttonurl'] ?? '' ) ),
153 'button_configuration' => $this->button_configuration_to_api( $advanced_control['button_configuration'] ?? [] ),
154 'preview' => esc_url_raw( $settings['previewImage'] ?? '' ),
155 'panoid' => '',
156 'customcontrol' => $this->customcontrol_to_api(
157 is_array( $advanced_control['customcontrol'] ?? null )
158 ? $advanced_control['customcontrol']
159 : []
160 ),
161 'tour-type' => $tour_type,
162 'vidurl' => esc_url_raw( $video_data['url'] ?? '' ),
163 'video-autoplay' => ! empty( $video_data['autoplay'] ) ? 'on' : 'off',
164 'video-loop' => ! empty( $video_data['loop'] ) ? 'on' : 'off',
165 'autoplay' => ! empty( $video_data['autoplay'] ) ? 'on' : 'off',
166 'loop' => ! empty( $video_data['loop'] ) ? 'on' : 'off',
167 'streetviewurl' => esc_url_raw( $street_view_data['embedUrl'] ?? '' ),
168 'streetview' => ! empty( $street_view_data['embedUrl'] ) ? 'on' : 'off',
169 'panodata' => [
170 'scene-list' => $this->scenes_from_api(
171 $scenes,
172 $default_scene_id,
173 ( $settings['showSceneInfo'] ?? true ) !== false
174 ),
175 ],
176 ];
177
178 if ( $tour_type === 'video' && ! empty( $video_data['url'] ) ) {
179 $raw['vidid'] = 'vid' . ( $data['tourId'] ?? $data['id'] ?? wp_rand( 1000, 99999 ) );
180 }
181
182 return $raw;
183 }
184
185 // -------------------------------------------------------------------------
186
187 private function floor_plan_to_api( array $raw ): array {
188 // Merge pointer positions and scene assignments into a unified array.
189 $positions = $raw['floor_plan_pointer_position'] ?? [];
190 $data_list = $raw['floor_plan_data_list'] ?? [];
191
192 // Build id → sceneId map from data_list ({id:'1', name:'plan1', value:'scene-id'}).
193 $scene_map = [];
194 foreach ( $data_list as $item ) {
195 $item = is_object( $item ) ? $item : (object) $item;
196 $scene_map[ $item->id ?? '' ] = $item->value ?? '';
197 }
198
199 $pointers = [];
200 foreach ( $positions as $pos ) {
201 $pos = is_object( $pos ) ? $pos : (object) $pos;
202 $pos_id = $pos->id ?? '';
203 // Extract numeric suffix from 'pointer-N'.
204 preg_match( '/(\d+)$/', $pos_id, $m );
205 $num = $m[1] ?? '';
206 $scene_id = $scene_map[ $num ] ?? $scene_map[ $pos_id ] ?? '';
207
208 $pointers[] = [
209 'id' => $pos_id,
210 'top' => $pos->data_top ?? '0%',
211 'left' => $pos->data_left ?? '0%',
212 'sceneId' => $scene_id,
213 ];
214 }
215
216 return [
217 // Read from new key first, fall back to legacy key.
218 'enabled' => ( $raw['floorplan-enabled'] ?? 'off' ) === 'on' || ( $raw['floor_plan_tour_enabler'] ?? 'off' ) === 'on',
219 'imageUrl' => $raw['floorplan-image'] ?? $raw['floor_plan_attachment_url'] ?? '',
220 'pointerColor' => ! empty( $raw['floor_plan_custom_color'] ) ? $raw['floor_plan_custom_color'] : '#cca92c',
221 'pointers' => $pointers,
222 'directionIndicator' => [
223 'enabled' => ( $raw['floorplan-compass'] ?? 'off' ) === 'on' || ( $raw['floor_plan_direction_indicator'] ?? 'off' ) === 'on',
224 'color' => $raw['floorplan-compass-color'] ?? '#6D28D9',
225 ],
226 ];
227 }
228
229 private function pointer_positions_from_api( array $floor_plan ): array {
230 $pointers = $floor_plan['pointers'] ?? [];
231 $color = $floor_plan['pointerColor'] ?? '#cca92c';
232 $result = [];
233 $index = 1;
234 foreach ( $pointers as $pointer ) {
235 $top = $pointer['top'] ?? '0%';
236 $left = $pointer['left'] ?? '0%';
237 $result[] = (object) [
238 'id' => 'pointer-' . $index,
239 'text' => (string) $index,
240 'data_top' => $top,
241 'data_left' => $left,
242 'style' => "background:{$color};top:{$top};left:{$left};",
243 ];
244 $index++;
245 }
246 return $result;
247 }
248
249 private function pointer_data_list_from_api( array $floor_plan ): array {
250 $pointers = $floor_plan['pointers'] ?? [];
251 $result = [];
252 $index = 1;
253 foreach ( $pointers as $pointer ) {
254 $result[] = (object) [
255 'id' => (string) $index,
256 'name' => 'plan' . $index,
257 'value' => $pointer['sceneId'] ?? '',
258 ];
259 $index++;
260 }
261 return $result;
262 }
263
264 /**
265 * Detect tour type supporting both new-UI ('tour-type' key) and legacy
266 * ('streetviewdata'/'vidid' keys) save formats.
267 */
268 private function detect_tour_type( array $raw ): string {
269 if ( ! empty( $raw['tour-type'] ) ) {
270 $allowed = [ 'image', 'video', 'street-view' ];
271 return in_array( $raw['tour-type'], $allowed, true ) ? $raw['tour-type'] : 'image';
272 }
273 if ( isset( $raw['streetviewdata'] ) || ! empty( $raw['streetviewurl'] ) ) {
274 return 'street-view';
275 }
276 if ( ! empty( $raw['vidid'] ) || ! empty( $raw['vidurl'] ) ) {
277 return 'video';
278 }
279 return 'image';
280 }
281
282 /**
283 * Extract pro advanced-control fields from raw panodata for the API response.
284 * tourLayout is stored as an array by pro but the React store uses a plain string.
285 */
286 protected function advanced_settings_to_api( array $raw ): array {
287 $b = static function ( $val, $default ) {
288 if ( is_bool( $val ) ) return $val;
289 return filter_var( $val ?? $default, FILTER_VALIDATE_BOOLEAN );
290 };
291
292 $tour_layout_raw = $raw['tourLayout'] ?? 'default';
293 $tour_layout = is_array( $tour_layout_raw )
294 ? ( $tour_layout_raw['layout'] ?? 'default' )
295 : (string) $tour_layout_raw;
296
297 // globalzoom is on when any of hfov/maxHfov/minHfov is set.
298 $globalzoom = ( ! empty( $raw['hfov'] ) || ! empty( $raw['maxHfov'] ) || ! empty( $raw['minHfov'] ) );
299
300 return [
301 'tourLayout' => $tour_layout,
302 'layout_icon_bg_color' => (string) ( $raw['layout_icon_bg_color'] ?? '#5a536e' ),
303 'layout_icon_color' => (string) ( $raw['layout_icon_color'] ?? '#ffffff' ),
304 'diskeyboard' => ! in_array( $raw['diskeyboard'] ?? 'off', [ 'on', 'true', true ], true ),
305 'keyboardzoom' => $b( $raw['keyboardzoom'] ?? null, true ),
306 'draggable' => $b( $raw['draggable'] ?? null, true ),
307 'mouseZoom' => $b( $raw['mouseZoom'] ?? null, true ),
308 'gyro' => $b( $raw['gyro'] ?? null, false ),
309 'deviceorientationcontrol' => $b( $raw['deviceorientationcontrol']?? null, false ),
310 'compass' => $b( $raw['compass'] ?? null, false ),
311 'vrgallery' => $b( $raw['vrgallery'] ?? null, false ),
312 'vrgallery_title' => $b( $raw['vrgallery_title'] ?? null, false ),
313 'vrgallery_icon_size' => $b( $raw['vrgallery_icon_size'] ?? null, false ),
314 'vrgallery_display' => $b( $raw['vrgallery_display'] ?? null, false ),
315 'scene_navigation' => $b( $raw['scene_navigation'] ?? null, false ),
316 'scene_navigation_content_type' => (string) ( $raw['scene_navigation_content_type'] ?? 'scene_id' ),
317 'sceneAnimation' => $b( $raw['sceneAnimation'] ?? null, false ),
318 'sceneAnimationName' => (string) ( $raw['sceneAnimationName'] ?? 'none' ),
319 'sceneAnimationTransitionDuration'=> (string) ( $raw['sceneAnimationTransitionDuration'] ?? '500' ),
320 'sceneAnimationTransitionDelay' => (string) ( $raw['sceneAnimationTransitionDelay'] ?? '0' ),
321 'bg_music' => $b( $raw['bg_music'] ?? null, false ),
322 'bg_music_url' => (string) ( $raw['bg_music_url'] ?? '' ),
323 'autoplay_bg_music' => $b( $raw['autoplay_bg_music'] ?? null, false ),
324 'loop_bg_music' => $b( $raw['loop_bg_music'] ?? null, false ),
325 'explainerSwitch' => $b( $raw['explainerSwitch'] ?? null, false ),
326 'explainerContent' => (string) ( $raw['explainerContent'] ?? '' ),
327 'cpLogoSwitch' => $b( $raw['cpLogoSwitch'] ?? null, false ),
328 'cpLogoImg' => (string) ( $raw['cpLogoImg'] ?? '' ),
329 'cpLogoContent' => (string) ( $raw['cpLogoContent'] ?? '' ),
330 'globalzoom' => $globalzoom,
331 'hfov' => isset( $raw['hfov'] ) && $raw['hfov'] !== '' ? (int) $raw['hfov'] : null,
332 'maxHfov' => isset( $raw['maxHfov'] ) && $raw['maxHfov'] !== '' ? (int) $raw['maxHfov'] : null,
333 'minHfov' => isset( $raw['minHfov'] ) && $raw['minHfov'] !== '' ? (int) $raw['minHfov'] : null,
334 'genericform' => ( ( $raw['genericform'] ?? 'off' ) === 'on' ),
335 'genericformshortcode' => (string) ( $raw['genericformshortcode'] ?? '' ),
336 'genericformicon' => (string) ( $raw['genericformicon'] ?? 'fab fa-wpforms' ),
337 'genericformiconcolor' => (string) ( $raw['genericformiconcolor'] ?? '#f7fffb' ),
338 'calltoaction' => ( ( $raw['calltoaction'] ?? 'off' ) === 'on' ),
339 'buttontext' => (string) ( $raw['buttontext'] ?? 'Click Here' ),
340 'buttonurl' => (string) ( $raw['buttonurl'] ?? '' ),
341 'button_configuration' => $this->button_configuration_to_api( $raw['button_configuration'] ?? [] ),
342 'customcss_enable' => ( ( $raw['customcss_enable'] ?? 'off' ) === 'on' ),
343 'customcss' => (string) ( $raw['customcss'] ?? '' ),
344 'customcontrol' => $this->customcontrol_to_api( $raw['customcontrol'] ?? [] ),
345 ];
346 }
347
348 private function customcontrol_to_api( $raw_ctrl ): array {
349 if ( ! is_array( $raw_ctrl ) ) {
350 $raw_ctrl = [];
351 }
352 $defaults = [
353 'panupSwitch' => 'off', 'panupColor' => '#f7fffb', 'panupIcon' => 'fas fa-angle-up',
354 'panDownSwitch' => 'off', 'panDownColor' => '#f7fffb', 'panDownIcon' => 'fas fa-angle-down',
355 'panLeftSwitch' => 'off', 'panLeftColor' => '#f7fffb', 'panLeftIcon' => 'fas fa-angle-left',
356 'panRightSwitch' => 'off', 'panRightColor' => '#f7fffb', 'panRightIcon' => 'fas fa-angle-right',
357 'panZoomInSwitch' => 'off', 'panZoomInColor' => '#f7fffb', 'panZoomInIcon' => 'fas fa-plus-circle',
358 'panZoomOutSwitch' => 'off', 'panZoomOutColor' => '#f7fffb', 'panZoomOutIcon' => 'fas fa-minus-circle',
359 'panFullscreenSwitch' => 'off', 'panFullscreenColor' => '#f7fffb', 'panFullscreenIcon' => 'fas fa-expand',
360 'gyroscopeSwitch' => 'off', 'gyroscopeColor' => '#f7fffb', 'gyroscopeIcon' => 'fas fa-dot-circle',
361 'backToHomeSwitch' => 'off', 'backToHomeColor' => '#f7fffb', 'backToHomeIcon' => 'fas fa-home',
362 'explainerColor' => '#f7fffb', 'explainerIcon' => 'fas fa-video',
363 ];
364 return array_merge( $defaults, array_intersect_key( $raw_ctrl, $defaults ) );
365 }
366
367 private function button_configuration_to_api( $raw_config ): array {
368 if ( ! is_array( $raw_config ) ) {
369 $raw_config = [];
370 }
371
372 $defaults = [
373 'button_open_new_tab' => 'off',
374 'button_background_color' => '#201cfe',
375 'button_font_color' => '#ffffff',
376 'button_font_size' => '14',
377 'button_font_weight' => '400',
378 'button_line_height' => '1',
379 'button_text_decoration' => 'none',
380 'button_transform' => 'none',
381 'button_alignment' => 'left',
382 'button_text_style' => 'normal',
383 'button_letter_spacing' => '1',
384 'button_word_spacing' => '0',
385 'button_border_width' => '1',
386 'button_border_style' => 'solid',
387 'button_border_color' => '#201cfe',
388 'button_border_radius' => '6',
389 'button_pt' => '10',
390 'button_pr' => '15',
391 'button_pb' => '10',
392 'button_pl' => '15',
393 ];
394 $config = array_merge( $defaults, array_intersect_key( $raw_config, $defaults ) );
395
396 $config['button_open_new_tab'] = in_array(
397 $config['button_open_new_tab'],
398 [ true, 1, '1', 'on' ],
399 true
400 ) ? 'on' : 'off';
401
402 $allowed_values = [
403 'button_font_weight' => [ '400', '500', '600', '700', '800', '900' ],
404 'button_text_decoration' => [ 'none', 'underline', 'overline', 'line-through' ],
405 'button_transform' => [ 'none', 'uppercase', 'lowercase', 'capitalize' ],
406 'button_alignment' => [ 'left', 'right', 'center', 'justified' ],
407 'button_text_style' => [ 'normal', 'italic', 'oblique' ],
408 'button_border_style' => [ 'solid', 'dashed', 'dotted', 'double', 'none' ],
409 ];
410 foreach ( $allowed_values as $key => $allowed ) {
411 $value = (string) $config[ $key ];
412 $config[ $key ] = in_array( $value, $allowed, true ) ? $value : $defaults[ $key ];
413 }
414
415 foreach ( [ 'button_background_color', 'button_font_color', 'button_border_color' ] as $key ) {
416 $value = (string) $config[ $key ];
417 $config[ $key ] = preg_match( '/^#[0-9a-fA-F]{6}$/', $value ) ? strtolower( $value ) : $defaults[ $key ];
418 }
419
420 foreach ( [
421 'button_font_size', 'button_line_height', 'button_letter_spacing',
422 'button_word_spacing', 'button_border_width', 'button_border_radius',
423 'button_pt', 'button_pr', 'button_pb', 'button_pl',
424 ] as $key ) {
425 $value = $config[ $key ];
426 $config[ $key ] = is_numeric( $value ) && (float) $value >= 0
427 ? (string) $value
428 : $defaults[ $key ];
429 }
430
431 return $config;
432 }
433
434 protected function scenes_to_api( array $scene_list ): array {
435 $n = static function ( $val, $cast ) {
436 if ( ! isset( $val ) || $val === '' ) return null;
437 return $cast === 'int' ? (int) $val : (float) $val;
438 };
439
440 $scenes = [];
441 foreach ( $scene_list as $scene ) {
442 $scenes[] = [
443 // Free fields
444 'id' => $scene['scene-id'] ?? '',
445 'name' => $scene['scene-ititle'] ?? '',
446 'type' => $scene['scene-type'] ?? 'equirectangular',
447 'imageUrl' => $scene['scene-attachment-url'] ?? '',
448 'isDefault' => ( $scene['dscene'] ?? 'off' ) === 'on',
449 'hotspots' => $this->hotspots_to_api( $scene['hotspot-list'] ?? [] ),
450 // Pro: metadata
451 'author' => $scene['scene-author'] ?? '',
452 'authorUrl' => $scene['scene-author-url'] ?? '',
453 'vaov' => $n( $scene['scene-vaov'] ?? null, 'int' ),
454 'haov' => $n( $scene['scene-haov'] ?? null, 'int' ),
455 'verticalOffset' => $n( $scene['scene-vertical-offset'] ?? null, 'float' ),
456 // Pro: default face orientation
457 'defaultFace' => $scene['ptyscene'] ?? 'off',
458 'pitch' => $n( $scene['scene-pitch'] ?? null, 'float' ),
459 'yaw' => $n( $scene['scene-yaw'] ?? null, 'float' ),
460 // Pro: vertical drag limits
461 'limitVertical' => $scene['cvgscene'] ?? 'off',
462 'maxPitch' => $n( $scene['scene-maxpitch'] ?? null, 'float' ),
463 'minPitch' => $n( $scene['scene-minpitch'] ?? null, 'float' ),
464 // Pro: horizontal drag limits
465 'limitHorizontal' => $scene['chgscene'] ?? 'off',
466 'maxYaw' => $n( $scene['scene-maxyaw'] ?? null, 'float' ),
467 'minYaw' => $n( $scene['scene-minyaw'] ?? null, 'float' ),
468 // Pro: per-scene zoom override
469 'customZoom' => $scene['czscene'] ?? 'off',
470 'zoom' => $n( $scene['scene-zoom'] ?? null, 'int' ),
471 'maxZoom' => $n( $scene['scene-maxzoom'] ?? null, 'int' ),
472 'minZoom' => $n( $scene['scene-minzoom'] ?? null, 'int' ),
473 // Pro: cubemap faces (6-element array, null when not set)
474 'cubemapFaces' => [
475 $scene['scene-attachment-url-face0'] ?? null,
476 $scene['scene-attachment-url-face1'] ?? null,
477 $scene['scene-attachment-url-face2'] ?? null,
478 $scene['scene-attachment-url-face3'] ?? null,
479 $scene['scene-attachment-url-face4'] ?? null,
480 $scene['scene-attachment-url-face5'] ?? null,
481 ],
482 ];
483 }
484 // Older tours did not store whether a hotspot entry point inherited
485 // the target scene face or was edited manually. Keep those links in
486 // inherit mode: only edits made through the new Navigation Entry Point
487 // controls explicitly record the custom mode.
488 foreach ( $scenes as &$scene ) {
489 foreach ( $scene['hotspots'] as &$hotspot ) {
490 if ( in_array( $hotspot['sceneEntryPointMode'] ?? '', [ 'inherit', 'custom' ], true ) ) {
491 continue;
492 }
493 $hotspot['sceneEntryPointMode'] = 'inherit';
494 }
495 unset( $hotspot );
496 }
497 unset( $scene );
498
499 return $scenes;
500 }
501
502 protected function scenes_from_api( array $scenes, string $default_scene_id = '', bool $show_scene_info = true ): array {
503 $scene_list = [];
504 $index = 1;
505 foreach ( $scenes as $scene ) {
506 $scene_id = $scene['id'] ?? '';
507 $faces = $scene['cubemapFaces'] ?? [];
508 $scene_data = [
509 // Free fields
510 'scene-id' => $scene_id,
511 'scene-ititle' => $scene['name'] ?? '',
512 'scene-type' => $this->is_pro && ( $scene['type'] ?? '' ) === 'cubemap' ? 'cubemap' : 'equirectangular',
513 'scene-attachment-url' => $scene['imageUrl'] ?? '',
514 'scene-show-info' => $show_scene_info ? 'on' : 'off',
515 'hotspot-list' => $this->hotspots_from_api( $scene['hotspots'] ?? [] ),
516 'dscene' => ( $default_scene_id !== '' && $scene_id === $default_scene_id ) ? 'on' : 'off',
517 ];
518
519 if ( $this->is_pro ) {
520 $scene_data = array_merge( $scene_data, [
521 // Pro: metadata
522 'scene-author' => $scene['author'] ?? '',
523 'scene-author-url' => $scene['authorUrl'] ?? '',
524 'scene-vaov' => $scene['vaov'] ?? '',
525 'scene-haov' => $scene['haov'] ?? '',
526 'scene-vertical-offset' => $scene['verticalOffset'] ?? '',
527 // Pro: default face orientation
528 'ptyscene' => $scene['defaultFace'] ?? 'off',
529 'scene-pitch' => $scene['pitch'] ?? '',
530 'scene-yaw' => $scene['yaw'] ?? '',
531 // Pro: vertical drag limits
532 'cvgscene' => $scene['limitVertical'] ?? 'off',
533 'scene-maxpitch' => $scene['maxPitch'] ?? '',
534 'scene-minpitch' => $scene['minPitch'] ?? '',
535 // Pro: horizontal drag limits
536 'chgscene' => $scene['limitHorizontal'] ?? 'off',
537 'scene-maxyaw' => $scene['maxYaw'] ?? '',
538 'scene-minyaw' => $scene['minYaw'] ?? '',
539 // Pro: per-scene zoom override
540 'czscene' => $scene['customZoom'] ?? 'off',
541 'scene-zoom' => $scene['zoom'] ?? '',
542 'scene-maxzoom' => $scene['maxZoom'] ?? '',
543 'scene-minzoom' => $scene['minZoom'] ?? '',
544 // Pro: cubemap faces
545 'scene-attachment-url-face0' => $faces[0] ?? '',
546 'scene-attachment-url-face1' => $faces[1] ?? '',
547 'scene-attachment-url-face2' => $faces[2] ?? '',
548 'scene-attachment-url-face3' => $faces[3] ?? '',
549 'scene-attachment-url-face4' => $faces[4] ?? '',
550 'scene-attachment-url-face5' => $faces[5] ?? '',
551 ] );
552 }
553
554 $scene_list[ $index ] = $scene_data;
555 $index++;
556 }
557 return $scene_list;
558 }
559
560 protected function hotspots_to_api( array $hotspot_list ): array {
561 $hotspots = [];
562 foreach ( $hotspot_list as $hotspot ) {
563 if ( empty( $hotspot['hotspot-title'] ) && empty( $hotspot['hotspot-type'] ) ) {
564 continue;
565 }
566 $scene_pitch = isset( $hotspot['hotspot-scene-pitch'] ) && $hotspot['hotspot-scene-pitch'] !== ''
567 ? (float) $hotspot['hotspot-scene-pitch']
568 : null;
569 $scene_yaw = isset( $hotspot['hotspot-scene-yaw'] ) && $hotspot['hotspot-scene-yaw'] !== ''
570 ? (float) $hotspot['hotspot-scene-yaw']
571 : null;
572 $hotspots[] = [
573 'id' => $hotspot['hotspot-id'] ?? wp_generate_uuid4(),
574 'type' => $hotspot['hotspot-type'] ?? 'info',
575 'pitch' => isset( $hotspot['hotspot-pitch'] ) ? (float) $hotspot['hotspot-pitch'] : 0,
576 'yaw' => isset( $hotspot['hotspot-yaw'] ) ? (float) $hotspot['hotspot-yaw'] : 0,
577 'text' => $hotspot['hotspot-title'] ?? '',
578 'content' => $hotspot['hotspot-content'] ?? '',
579 'url' => $hotspot['hotspot-url'] ?? '',
580 'urlOpen' => $hotspot['hotspot-url-open'] ?? 'off',
581 'hover' => $hotspot['hotspot-hover'] ?? '',
582 'targetSceneId' => $hotspot['hotspot-scene'] ?? '',
583 'customClass' => $hotspot['hotspot-customclass'] ?? '',
584 // Pro styling fields
585 'iconClass' => ( ( $hotspot['hotspot-customclass-pro'] ?? '' ) === 'none' || ( $hotspot['hotspot-customclass-pro'] ?? '' ) === '' ) ? '' : $hotspot['hotspot-customclass-pro'],
586 'iconBgColor' => $hotspot['hotspot-customclass-color-icon-value'] ?? '#00b4ff',
587 'iconColor' => $hotspot['hotspot-custom-icon-color-value'] ?? '#ffffff',
588 'blink' => $hotspot['hotspot-blink'] ?? 'on',
589 'shape' => $hotspot['hotspot-shape'] ?? 'round',
590 'border' => $hotspot['hotspot-border'] ?? 'off',
591 'borderWidth' => $hotspot['hotspot-border-width'] ?? '1',
592 'borderStyle' => $hotspot['hotspot-border-style'] ?? 'none',
593 'borderColor' => $hotspot['hotspot-border-color'] ?? '#00b4ff',
594 // Pro navigation entry point fields
595 'scenePitch' => $scene_pitch,
596 'sceneYaw' => $scene_yaw,
597 'sceneEntryPointMode' => $hotspot['hotspot-scene-entry-point-mode'] ?? null,
598 ];
599 }
600 return $hotspots;
601 }
602
603 protected function hotspots_from_api( array $hotspots ): array {
604 $hotspot_list = [];
605 foreach ( $hotspots as $hotspot ) {
606 $hotspot_type = sanitize_key( $hotspot['type'] ?? 'info' );
607 $is_fluent_form = $hotspot_type === 'fluent_form';
608 $raw_content = (string) ( $hotspot['content'] ?? '' );
609 $raw_hover = (string) ( $hotspot['hover'] ?? '' );
610
611 $content = function_exists( 'sanitize_content_preserve_styles' )
612 ? sanitize_content_preserve_styles( $raw_content, $is_fluent_form )
613 : wp_kses_post( $raw_content );
614 $hover = function_exists( 'sanitize_content_preserve_styles' )
615 ? sanitize_content_preserve_styles( $raw_hover, false )
616 : wp_kses_post( $raw_hover );
617
618 $hs = [
619 'hotspot-id' => $hotspot['id'] ?? wp_generate_uuid4(),
620 'hotspot-type' => $hotspot_type,
621 'hotspot-pitch' => (string) ( $hotspot['pitch'] ?? 0 ),
622 'hotspot-yaw' => (string) ( $hotspot['yaw'] ?? 0 ),
623 'hotspot-title' => sanitize_text_field( $hotspot['text'] ?? '' ),
624 'hotspot-content' => $content,
625 'hotspot-url' => sanitize_text_field( $hotspot['url'] ?? '' ),
626 'hotspot-url-open' => ( $hotspot['urlOpen'] ?? 'off' ) === 'on' ? 'on' : 'off',
627 'hotspot-hover' => $hover,
628 'hotspot-scene' => sanitize_text_field( $hotspot['targetSceneId'] ?? '' ),
629 'hotspot-customclass' => sanitize_text_field( $hotspot['customClass'] ?? '' ),
630 'hotspot-scene-list' => 'none',
631 ];
632
633 if ( $this->is_pro ) {
634 $hs = array_merge( $hs, [
635 // Pro styling fields
636 'hotspot-customclass-pro' => !empty( $hotspot['iconClass'] ) ? $hotspot['iconClass'] : 'none',
637 'hotspot-customclass-color-icon-value' => $hotspot['iconBgColor'] ?? '#00b4ff',
638 'hotspot-custom-icon-color-value' => $hotspot['iconColor'] ?? '#ffffff',
639 'hotspot-blink' => $hotspot['blink'] ?? 'on',
640 'hotspot-shape' => $hotspot['shape'] ?? 'round',
641 'hotspot-border' => $hotspot['border'] ?? 'off',
642 'hotspot-border-width' => $hotspot['borderWidth'] ?? '1',
643 'hotspot-border-style' => $hotspot['borderStyle'] ?? 'none',
644 'hotspot-border-color' => $hotspot['borderColor'] ?? '#00b4ff',
645 // Pro navigation entry point fields
646 'hotspot-scene-pitch' => $hotspot['scenePitch'] !== null ? (string) $hotspot['scenePitch'] : '',
647 'hotspot-scene-yaw' => $hotspot['sceneYaw'] !== null ? (string) $hotspot['sceneYaw'] : '',
648 'hotspot-scene-entry-point-mode' => in_array( $hotspot['sceneEntryPointMode'] ?? '', [ 'inherit', 'custom' ], true )
649 ? $hotspot['sceneEntryPointMode']
650 : 'inherit',
651 ] );
652 }
653
654 $hotspot_list[] = $hs;
655 }
656 return $hotspot_list;
657 }
658 }
659