Home Reference Source

lib/parks/phantasialand/phantasialand.js

import fetch from 'node-fetch';
import moment from 'moment-timezone';
import cheerio from 'cheerio';
import {Park} from '../park.js';
import Location from '../location.js';

import dotenv from 'dotenv';
import {entityType, queueType, scheduleType} from '../types.js';
import {entityCategory, entityTags} from '../tags.js';
dotenv.config();

/**
* Phantasialand Park Object
* Make sure all environment variables are set in an .env file which should be in the main location.
* Not setting these variables will make the module exit early without returning data.
*
* This class is here to fetch the POI data and to attach queue times data to it.
* After the fetches this data is send to the end user and from there he could do whatever he wants to do.
*
* Most park specific parameters are set already
* @class
*/
export class Phantasialand extends Park {
  /**
  * Create a new Phantasialand Park object
  * @param {object} options
  */
  constructor(options = {}) {
    options.name = 'Phantasialand';
    options.timezone = 'Europe/Berlin';

    // Setting the parks entrance as it's default location
    options.latitude = 50.798954;
    options.longitude = 6.879314;

    // Options for our park Object
    options.supportswaittimes = true;
    options.supportsschedule = false;
    options.supportsrideschedules = true;
    options.fastPass = false;
    options.FastPassReturnTimes = false;

    // Options for location faking
    options.longitudeMin = 6.878342628;
    options.longitudeMax = 6.877570152;
    options.latitudeMin = 50.800659529;
    options.latitudeMax = 50.799683077;

    // Api options
    options.apiKey = process.env.PHANTASIALAND_API_KEY;

    options.poiUrl = process.env.PHANTASIALAND_POI_URL;
    options.waitTimesURL = process.env.PHANTASIALAND_WAITTIMES_URL;
    options.hoursURL = process.env.PHANTASIALAND_HOURS_URL;

    // Language options
    options.languages = process.env.LANGUAGES;

    options.langoptions = `{'en', 'fr', 'de', 'nl'}`;

    super(options);

    // Check for existance
    if (!this.config.poiUrl) throw new Error('Missing Phantasialand poi url!');
    if (!this.config.apiKey) throw new Error('Missing Phantasialand apiKey!');
    if (!this.config.waitTimesURL) throw new Error('Missing Phantasialand waittimes url!');
    if (!this.config.hoursURL) throw new Error('Missing Phantasialand Operating Hours url!');
    if (!this.config.languages) {
      this.config.languages = 'en';
    };

    this.config.langpref = [`${this.config.languages}`, 'de'];
  }

  /**
  * Get Phantasialand POI data
  * This data contains general ride names, descriptions etc.
  * @example
  * import tpapi from '@alexvv13/tpapi';
  *
  * const park = new tpapi.park.Phantasialand();
  *
  * park.getPois().then((pois) => {
  * console.log(pois)
  * });
  * @return {string} All P POIS without queuetimes
  */
  async getPOIS() {
    // So phantasialand is kinda strange with this, but it provides ALL languages at once, set your env var as no one lang.
    const pickName = (title) => {
      const n = this.config.langpref.find((lang) => title[lang]);
      return n !== undefined ? title[n] : title;
    };
    return fetch(`${this.config.poiUrl}`,
        {
          method: 'GET',
        },
    )
        .then((res) => res.json())
        .then((rideData) => {
          const rides = {};
          rideData.forEach((ride) => {
            const category = [];
            const tags = [];
            if (ride.tags) {
              ride.tags.forEach((tag) => {
                // Category
                if (tag === 'ATTRACTION_TYPE_CHILDREN') {
                  category.push(entityCategory.youngest);
                }
                if (tag === 'ATTRACTION_TYPE_FAMILY') {
                  category.push(entityCategory.family);
                }
                if (tag === 'ATTRACTION_TYPE_ACTION') {
                  category.push(entityCategory.thrill);
                }

                // Tags
                if (tag === 'ATTRACTION_TYPE_ROOFED') {
                  tags.push(entityTags.indoor);
                }
                if (tag === 'ATTRACTION_TYPE_PICTURES') {
                  tags.push(entityTags.onridePhoto);
                }
                if (tag === 'ATTRACTION_TYPE_CHILDREN_MAY_SCARE') {
                  tags.push(entityTags.scary);
                }
              });
            }
            let minAge = undefined;
            let maxAge = undefined;
            let minHeight = undefined;
            let maxHeight = undefined;
            let minHeightAccompanied = undefined;

            if (ride.minAge) {
              minAge = ride.minAge;
            }
            if (ride.maxAge) {
              maxAge = ride.maxAge;
            }
            if (ride.minSize) {
              minHeight = ride.minSize;
            }
            if (ride.maxSize) {
              maxHeight = ride.maxSize;
            }
            if (ride.minSizeEscort) {
              minHeightAccompanied = ride.minSizeEscort;
            }
            const restrictions = {
              minAge,
              minHeight,
              minHeightAccompanied,
              maxHeight,
              maxAge,
            };
            const location = (ride.entrance && ride.entrance.world) ? ride.entrance.world : undefined;
            rides[ride.id] = {
              name: pickName(ride.title),
              id: ride.id,
              location: {
                area: ride.area,
                longitude: location ? location.lng : null,
                latitude: location ? location.lat : null,
              },
              meta: {
                category,
                type: ride.category,
                descriptions: {
                  description: pickName(ride.description),
                  short_description: pickName(ride.tagline),
                },
                tags,
                restrictions,
              },
            };
          });
          return Promise.resolve(rides);
        });
  }

  /**
   * Fetch service data
   */
  async buildServicePOI() {
    return await this.cache.wrap(`servicedata`, async () => {
      const rideData = await this.getPOIS();
      const rides = {};
      Object.keys(rideData).forEach((ride) => {
        // We only want services
        if (rideData[ride].meta.type !== 'SERVICE') return undefined;

        const id = rideData[ride].id;

        rides[id] = {
          name: rideData[ride].name,
          id: `Phantasialand_${id}`,
          location: {
            area: rideData[ride].location.area,
            longitude: rideData[ride].location.longitude,
            latitude: rideData[ride].location.latitude,
          },
          meta: {
            category: rideData[ride].meta.category,
            type: entityType.ride,
            descriptions: {
              description: rideData[ride].meta.descriptions.description,
              short_description: rideData[ride].meta.descriptions.short_description,
            },
            tags: rideData[ride].meta.tags,
          },
        };
      });
      return Promise.resolve(rides);
    }, 1000 * 60 * 60 * this.config.cachepoistime /* cache for 12 hours */);
  }

  /**
   * Fetch shop data
   */
  async buildMerchandisePOI() {
    return await this.cache.wrap(`shopdata`, async () => {
      const rideData = await this.getPOIS();
      const rides = {};
      Object.keys(rideData).forEach((ride) => {
        // We only want shops
        if (rideData[ride].meta.type !== 'SHOPS') return undefined;

        const id = rideData[ride].id;

        rides[id] = {
          name: rideData[ride].name,
          id: `Phantasialand_${id}`,
          location: {
            area: rideData[ride].location.area,
            longitude: rideData[ride].location.longitude,
            latitude: rideData[ride].location.latitude,
          },
          meta: {
            category: rideData[ride].meta.category,
            type: entityType.ride,
            descriptions: {
              description: rideData[ride].meta.descriptions.description,
              short_description: rideData[ride].meta.descriptions.short_description,
            },
            tags: rideData[ride].meta.tags,
          },
        };
      });
      return Promise.resolve(rides);
    }, 1000 * 60 * 60 * this.config.cachepoistime /* cache for 12 hours */);
  }

  /**
   * Fetch event data
   */
  async buildEventPOI() {
    return await this.cache.wrap(`eventdata`, async () => {
      const rideData = await this.getPOIS();
      const rides = {};
      Object.keys(rideData).forEach((ride) => {
        // We only want event locations
        if (rideData[ride].meta.type !== 'EVENT_LOCATIONS') return undefined;

        const id = rideData[ride].id;

        rides[id] = {
          name: rideData[ride].name,
          id: `Phantasialand_${id}`,
          location: {
            area: rideData[ride].location.area,
            longitude: rideData[ride].location.longitude,
            latitude: rideData[ride].location.latitude,
          },
          meta: {
            category: rideData[ride].meta.category,
            type: entityType.ride,
            descriptions: {
              description: rideData[ride].meta.descriptions.description,
              short_description: rideData[ride].meta.descriptions.short_description,
            },
            tags: rideData[ride].meta.tags,
          },
        };
      });
      return Promise.resolve(rides);
    }, 1000 * 60 * 60 * this.config.cachepoistime /* cache for 12 hours */);
  }

  /**
   * Fetch hotel data
   */
  async buildHotelPOI() {
    return await this.cache.wrap(`hoteldata`, async () => {
      const rideData = await this.getPOIS();
      const rides = {};
      Object.keys(rideData).forEach((ride) => {
        // We only want hotels
        if (rideData[ride].meta.type !== 'PHANTASIALAND_HOTELS') return undefined;

        const id = rideData[ride].id;

        rides[id] = {
          name: rideData[ride].name,
          id: `Phantasialand_${id}`,
          location: {
            area: rideData[ride].location.area,
            longitude: rideData[ride].location.longitude,
            latitude: rideData[ride].location.latitude,
          },
          meta: {
            category: rideData[ride].meta.category,
            type: entityType.ride,
            descriptions: {
              description: rideData[ride].meta.descriptions.description,
              short_description: rideData[ride].meta.descriptions.short_description,
            },
            tags: rideData[ride].meta.tags,
          },
        };
      });
      return Promise.resolve(rides);
    }, 1000 * 60 * 60 * this.config.cachepoistime /* cache for 12 hours */);
  }

  /**
   * Fetch hotel bar data
   */
  async buildHotelBarPOI() {
    return await this.cache.wrap(`hotelbardata`, async () => {
      const rideData = await this.getPOIS();
      const rides = {};
      Object.keys(rideData).forEach((ride) => {
        // We only want hotel bars
        if (rideData[ride].meta.type !== 'PHANTASIALAND_HOTELS_BARS') return undefined;

        const id = rideData[ride].id;

        rides[id] = {
          name: rideData[ride].name,
          id: `Phantasialand_${id}`,
          location: {
            area: rideData[ride].location.area,
            longitude: rideData[ride].location.longitude,
            latitude: rideData[ride].location.latitude,
          },
          meta: {
            category: rideData[ride].meta.category,
            type: entityType.ride,
            descriptions: {
              description: rideData[ride].meta.descriptions.description,
              short_description: rideData[ride].meta.descriptions.short_description,
            },
            tags: rideData[ride].meta.tags,
          },
        };
      });
      return Promise.resolve(rides);
    }, 1000 * 60 * 60 * this.config.cachepoistime /* cache for 12 hours */);
  }

  /**
   * Fetch restaurant data
   */
  async buildRestaurantPOI() {
    return await this.cache.wrap(`restdata`, async () => {
      const rideData = await this.getPOIS();
      const rides = {};
      Object.keys(rideData).forEach((ride) => {
        // We only want restaurants
        if (rideData[ride].meta.type !== 'RESTAURANTS_AND_SNACKS') return undefined;

        const id = rideData[ride].id;

        rides[id] = {
          name: rideData[ride].name,
          id: `Phantasialand_${id}`,
          location: {
            area: rideData[ride].location.area,
            longitude: rideData[ride].location.longitude,
            latitude: rideData[ride].location.latitude,
          },
          meta: {
            category: rideData[ride].meta.category,
            type: entityType.ride,
            descriptions: {
              description: rideData[ride].meta.descriptions.description,
              short_description: rideData[ride].meta.descriptions.short_description,
            },
            tags: rideData[ride].meta.tags,
          },
        };
      });
      return Promise.resolve(rides);
    }, 1000 * 60 * 60 * this.config.cachepoistime /* cache for 12 hours */);
  }

  /**
   * Fetch ride data
   */
  async buildRidePOI() {
    return await this.cache.wrap(`ridedata`, async () => {
      const rideData = await this.getPOIS();
      const rides = {};
      Object.keys(rideData).forEach((ride) => {
        // We only want rides
        if (rideData[ride].meta.type !== 'ATTRACTIONS') return undefined;

        const id = rideData[ride].id;

        rides[id] = {
          name: rideData[ride].name,
          id: `Phantasialand_${id}`,
          location: {
            area: rideData[ride].location.area,
            longitude: rideData[ride].location.longitude,
            latitude: rideData[ride].location.latitude,
          },
          meta: {
            category: rideData[ride].meta.category,
            type: entityType.ride,
            descriptions: {
              description: rideData[ride].meta.descriptions.description,
              short_description: rideData[ride].meta.descriptions.short_description,
            },
            tags: rideData[ride].meta.tags,
            restrictions: rideData[ride].meta.restrictions,
          },
        };
      });
      return Promise.resolve(rides);
    }, 1000 * 60 * 60 * this.config.cachepoistime /* cache for 12 hours */);
  }

  /**
   * Fetch wait times
   * @return {string} ride wait times of Phantasialand
   */
  async getQueue() {
    return this.buildRidePOI().then((poi) => {
      // We'll pretend we're actually in Phantasialand, cause we're cool
      const RandomLocation = Location.randomBetween(this.config.longitudeMin, this.config.latitudeMin, this.config.longitudeMax, this.config.latitudeMax);

      return fetch(`${this.config.waitTimesURL}?loc=${RandomLocation.latitude},${RandomLocation.longitude}&compact=true&access_token=${this.config.apiKey}`,
          {
            method: 'GET',
          },
      )
          .then((res) => res.json())
          .then((ride) => {
            const rides = [];
            // console.log(ride);
            if (!ride) throw new Error('No queuedata found!');
            Object.keys(ride).forEach((rideData) => {
              console.log(ride[rideData].waitTime);
              let waitTime = '0';
              let active = false;
              let state = queueType.closed;

              // Check if ride is open and if it actually has a queuetime attached
              if (ride[rideData].open && ride[rideData].waitTime !== null) {
                waitTime = ride[rideData].waitTime;
                active = true;
                state = queueType.operating;
              }

              // Attach schedule if known, api communicates closingtimes at least (Shown on park signage)
              let openTime = undefined;
              let closingTime = undefined;
              let opType = undefined;

              if (ride[rideData].opening && ride[rideData].closing) { // We got ridetimes so display them
                openTime = moment(ride[rideData].opening).format();
                closingTime = moment(ride[rideData].closing).format();
                opType = scheduleType.operating;
              } else { // Park is probably closed || Before park opening & after opening hours are purged automatically...
                openTime = moment('23:59', 'HH:mm a').format();
                closingTime = moment('23:59', 'HH:mm a').format();
                opType = scheduleType.closed;
              }

              const schedule = {
                openingtTime: openTime,
                closingTime: closingTime,
                type: opType,
              };

              if (poi[ride[rideData].poiId]) {
                poi[ride[rideData].poiId].meta.schedule = schedule; // Attach the schedule data to the meta object.

                const rideobj = {
                  name: poi[ride[rideData].poiId].name,
                  id: poi[ride[rideData].poiId].id,
                  status: state,
                  waitTime: waitTime,
                  active: active,
                  changedAt: ride[rideData].updatedAt,
                  location: poi[ride[rideData].poiId].location,
                  meta: poi[ride[rideData].poiId].meta,
                };
                rides.push(rideobj);
              }
            });
            return Promise.resolve(rides);
          });
    });
  }

  /**
   * Get operating hours of phantasialand
   * @return {string} calendar data
   */
  async getOpHours() {
    const hours = [];
    return fetch(`https://www.phantasialand.de/en/theme-park/opening-hours/`)
        .then((res) => res.text())
        .then((text) => {
          const $ = cheerio.load(text);
          const calendarData = JSON.parse($('.phl-date-picker').attr('data-calendar'));
          calendarData.forEach((calendar) => {
            if (calendar.title === 'Geschlossen') {
              Object.keys(calendar.days_selected).forEach((time) => {
                const cal = {
                  date: calendar.days_selected[time],
                  openingTime: moment('23:59', 'HH:mm a').format(),
                  closingTime: moment('23:59', 'HH:mm a').format(),
                  type: scheduleType.closed,
                  special: [],
                };
                hours.push(cal);
              });
            } else if (calendar.title === 'Park\u00f6ffnung: 09:00 \u2013 18:00 Uhr') {
              Object.keys(calendar.days_selected).forEach((time) => {
                const cal = {
                  date: calendar.days_selected[time],
                  openingTime: moment('09:00', 'HH:mm a').format(),
                  closingTime: moment('18:00', 'HH:mm a').format(),
                  type: scheduleType.operating,
                  special: [],
                };
                hours.push(cal);
              });
            } else if (calendar.title === 'Park\u00f6ffnung: 09:00 \u2013 19:00 Uhr') {
              Object.keys(calendar.days_selected).forEach((time) => {
                const cal = {
                  date: calendar.days_selected[time],
                  openingTime: moment('09:00', 'HH:mm a').format(),
                  closingTime: moment('19:00', 'HH:mm a').format(),
                  type: scheduleType.operating,
                  special: [],
                };
                hours.push(cal);
              });
            } else if (calendar.title === 'Silvester: 11:00 \u2013 18:00 Uhr') {
              Object.keys(calendar.days_selected).forEach((time) => {
                const cal = {
                  date: calendar.days_selected[time],
                  openingTime: moment('11:00', 'HH:mm a').format(),
                  closingTime: moment('18:00', 'HH:mm a').format(),
                  type: scheduleType.operating,
                  special: [],
                };
                hours.push(cal);
              });
            } else if (calendar.title === 'Wintertraum: 11:00 \u2013 20:00 Uhr') {
              Object.keys(calendar.days_selected).forEach((time) => {
                const cal = {
                  date: calendar.days_selected[time],
                  openingTime: moment('09:00', 'HH:mm a').format(),
                  closingTime: moment('20:00', 'HH:mm a').format(),
                  type: scheduleType.operating,
                  special: [],
                };
                hours.push(cal);
              });
            }
          });
          return Promise.resolve(hours);
        });
  }
};

export default Phantasialand;