diff --git a/bun.lockb b/bun.lockb index f79740b0..ddf04e9b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 1347dc39..728434df 100755 --- a/package.json +++ b/package.json @@ -99,9 +99,9 @@ }, "dependencies": { "earclip": "^1.1.1", - "open-vector-tile": "^1.6.0", + "open-vector-tile": "^1.6.1", "s2-tilejson": "^1.7.0", - "s2json-spec": "^1.6.0", + "s2json-spec": "^1.6.2", "sharp": "^0.33.5" } } diff --git a/src/readers/gbfs/index.ts b/src/readers/gbfs/index.ts index ec06fec3..06165198 100644 --- a/src/readers/gbfs/index.ts +++ b/src/readers/gbfs/index.ts @@ -20,7 +20,7 @@ export type GBFSTypess = GBFSV1 | GBFSV2 | GBFSV3; * - v2: https://gbfs.helbiz.com/v2.2/durham/gbfs.json * - v1: https://gbfs.urbansharing.com/gbfs/gbfs.json * @param url - The link to the GBFS feed - * @param locale - The locale to use if provided, otherwise default to en + * @param locale - The locale to use if provided, otherwise default to "en" (e.g., "en", "en-US"). * @returns - a GBFSReader of the appropriate version */ export async function buildGBFSReader(url: string, locale = 'en'): Promise { diff --git a/src/readers/gbfs/schemaV1/index.ts b/src/readers/gbfs/schemaV1/index.ts index 4fba44e0..d720ca42 100644 --- a/src/readers/gbfs/schemaV1/index.ts +++ b/src/readers/gbfs/schemaV1/index.ts @@ -12,6 +12,8 @@ import { GBFSVersionsV1, } from '.'; +import type { Feature, FeatureIterator, PointGeometry, Properties } from '../../..'; + export * from './freeBikeStatus'; export * from './gbfs'; export * from './gbfsVersions'; @@ -24,13 +26,60 @@ export * from './systemInformation'; export * from './systemPricingPlans'; export * from './systemRegions'; +/** Station Information feature properties */ +export interface GBFSStationV1FeaturesV1Properties extends Properties { + station_id: string; + name: string; + short_name?: string; + address?: string; + cross_street?: string; + region_id?: string; + post_code?: string; + rental_methods?: Array< + | 'key' + | 'creditcard' + | 'paypass' + | 'applepay' + | 'androidpay' + | 'transitcard' + | 'accountnumber' + | 'phone' + | 'KEY' + | 'CREDITCARD' + | 'PAYPASS' + | 'APPLEPAY' + | 'ANDROIDPAY' + | 'TRANSITCARD' + | 'ACCOUNTNUMBER' + | 'PHONE' + >; + capacity?: number; +} + +/** Station Information Point Feature */ +export type GBFSStationPointFeatureV1 = Feature< + undefined, + Properties, + GBFSStationV1FeaturesV1Properties, + PointGeometry +>; + +/** All potential feature types in a GBFS V1 specification */ +export type GBFSFeaturesV1 = GBFSStationPointFeatureV1; + +/** All potential feature property types in a GBFS V1 specification */ +export type GBFSFeaturePropertiesV1 = GBFSStationV1FeaturesV1Properties; + /** * GBFS Version 1 Reader */ -export class GBFSReaderV1 { +export class GBFSReaderV1 + implements FeatureIterator +{ + version = 1; freeBikeStatus?: GBFSFreeBikeStatusV1; gbfs: GBFSV1; - versions?: GBFSVersionsV1; + gbfsVersions?: GBFSVersionsV1; stationInformation?: GBFSStationInformationV1; stationStatus?: GBFSStationStatusV1; systemAlerts?: GBFSSystemAlertsV1; @@ -46,7 +95,7 @@ export class GBFSReaderV1 { */ constructor(gbfs: GBFSV1, feeds?: FeedResV1) { this.gbfs = gbfs; - this.versions = feeds?.gbfs_versions; + this.gbfsVersions = feeds?.gbfs_versions; this.systemInformation = feeds?.system_information as GBFSSystemInformationV1; this.stationInformation = feeds?.station_information; this.stationStatus = feeds?.station_status; @@ -57,6 +106,54 @@ export class GBFSReaderV1 { this.systemPricingPlans = feeds?.system_pricing_plans; this.systemRegions = feeds?.system_regions; } + + /** + * Yields all of the shapes + * @yields an iterator that contains shapes, stops, location data, and routes + */ + async *[Symbol.asyncIterator](): AsyncGenerator { + const { stationInformation } = this; + if (stationInformation !== undefined) { + const { + data: { stations }, + } = stationInformation; + for (const station of stations) { + const { + lat, + lon, + station_id, + name, + short_name, + address, + cross_street, + region_id, + post_code, + rental_methods, + capacity, + } = station; + const stationProperties: GBFSStationV1FeaturesV1Properties = { + station_id, + name, + short_name, + address, + cross_street, + region_id, + post_code, + rental_methods, + capacity, + }; + const stationPoint: GBFSStationPointFeatureV1 = { + type: 'Feature', + properties: stationProperties, + geometry: { + type: 'Point', + coordinates: [lon, lat], + }, + }; + yield stationPoint; + } + } + } } /** Parsing all the available feeds */ diff --git a/src/readers/gbfs/schemaV2/geofencingZones.ts b/src/readers/gbfs/schemaV2/geofencingZones.ts index 7ec5a950..9a43fd84 100644 --- a/src/readers/gbfs/schemaV2/geofencingZones.ts +++ b/src/readers/gbfs/schemaV2/geofencingZones.ts @@ -1,3 +1,5 @@ +import type { FeatureCollection, MValue, Properties, ValueArrayObject } from '../../..'; + /** * # GBFS Geofencing Zones Schema V2.3, V2.2, V2.1, OR V2.0 * Describes geofencing zones and their associated rules and attributes (added in v2.1-RC). @@ -168,6 +170,52 @@ export const gbfsGeofencingZonesSchemaV23 = { required: ['last_updated', 'ttl', 'version', 'data'], }; +/** + * GBFS V3: Restrictions that apply within the area of the polygon. + */ +export interface GBFSGeofencingZonesV2PropertiesRule extends ValueArrayObject { + /** + * Array of vehicle type IDs for which these restrictions apply. + */ + // @ts-expect-error - we need to clean this / remove it + vehicle_type_id?: string[]; + /** + * Is the undocked ride allowed to start and end in this zone? + */ + ride_allowed: boolean; + /** + * Is the ride allowed to travel through this zone? + */ + ride_through_allowed: boolean; + /** + * Maximum speed allowed, in kilometers per hour. + * **Minimum**: 0 + */ + maximum_speed_kph?: number; + /** + * Vehicle MUST be parked at stations defined in station_information.json within this geofence zone. + */ + station_parking?: boolean; +} + +/** Properties of a geofencing zone */ +export interface GBFSGeofencingZonesV2Properties extends Properties { + /** Public name of the geofencing zone. */ + name: string; + /** + * Start time of the geofencing zone in RFC3339 format. + * **format** date-time + */ + start?: string; + /** + * End time of the geofencing zone in RFC3339 format. + * **format** date-time + */ + end?: string; + /** Array of rules defining restrictions within the geofence. */ + rules?: GBFSGeofencingZonesV2PropertiesRule[]; +} + /** * # GBFS Geofencing Zones V2.3 * Describes geofencing zones and their associated rules and attributes (added in v2.1-RC). @@ -198,91 +246,7 @@ export interface GBFSGeofencingZonesV23 { * Contains geofencing information for the system. */ data: { - geofencing_zones: { - /** - * FeatureCollection as per IETF RFC 7946. - * **Enum**: "FeatureCollection" - */ - type: 'FeatureCollection'; - - /** - * Array of GeoJSON features. - */ - features: Array<{ - /** - * **Type**: "Feature" - */ - type: 'Feature'; - - /** - * Properties describing travel allowances and limitations. - */ - properties: { - /** - * Public name of the geofencing zone. - */ - name?: string; - - /** - * Start time of the geofencing zone in POSIX time. - * **Minimum**: 1450155600 - */ - start?: number; - - /** - * End time of the geofencing zone in POSIX time. - * **Minimum**: 1450155600 - */ - end?: number; - - /** - * Array of rules for the geofencing zone. - */ - rules?: Array<{ - /** - * Array of vehicle type IDs for which these restrictions apply. - */ - vehicle_type_id?: string[]; - - /** - * Is the undocked ride allowed to start and end in this zone? - */ - ride_allowed: boolean; - - /** - * Is the ride allowed to travel through this zone? - */ - ride_through_allowed: boolean; - - /** - * Maximum speed allowed, in kilometers per hour. - * **Minimum**: 0 - */ - maximum_speed_kph?: number; - - /** - * Vehicle MUST be parked at stations defined in station_information.json within this geofence zone. - */ - station_parking?: boolean; - }>; - }; - - /** - * A polygon that describes geofencing boundaries. - */ - geometry: { - /** - * **Type**: "MultiPolygon" - */ - type: 'MultiPolygon'; - - /** - * Array of coordinates following the right-hand rule. - */ - coordinates: number[][][][]; - }; - }>; - }; + geofencing_zones: FeatureCollection; }; } @@ -464,86 +428,7 @@ export interface GBFSGeofencingZonesV22 { * Contains geofencing information for the system. */ data: { - geofencing_zones: { - /** - * FeatureCollection as per IETF RFC 7946. - * **Enum**: "FeatureCollection" - */ - type: 'FeatureCollection'; - - /** - * Array of GeoJSON features. - */ - features: Array<{ - /** - * **Type**: "Feature" - */ - type: 'Feature'; - - /** - * Properties describing travel allowances and limitations. - */ - properties: { - /** - * Public name of the geofencing zone. - */ - name?: string; - - /** - * Start time of the geofencing zone in POSIX time. - * **Minimum**: 1450155600 - */ - start?: number; - - /** - * End time of the geofencing zone in POSIX time. - * **Minimum**: 1450155600 - */ - end?: number; - - /** - * Array of rules for the geofencing zone. - */ - rules?: Array<{ - /** - * Array of vehicle type IDs for which these restrictions apply. - */ - vehicle_type_id?: string[]; - - /** - * Is the undocked ride allowed to start and end in this zone? - */ - ride_allowed: boolean; - - /** - * Is the ride allowed to travel through this zone? - */ - ride_through_allowed: boolean; - - /** - * Maximum speed allowed, in kilometers per hour. - * **Minimum**: 0 - */ - maximum_speed_kph?: number; - }>; - }; - - /** - * A polygon that describes geofencing boundaries. - */ - geometry: { - /** - * **Type**: "MultiPolygon" - */ - type: 'MultiPolygon'; - - /** - * Array of coordinates following the right-hand rule. - */ - coordinates: number[][][][]; - }; - }>; - }; + geofencing_zones: FeatureCollection; }; } @@ -725,85 +610,6 @@ export interface GBFSGeofencingZonesV21 { * Contains geofencing information for the system. */ data: { - geofencing_zones: { - /** - * FeatureCollection as per IETF RFC 7946. - * **Enum**: "FeatureCollection" - */ - type: 'FeatureCollection'; - - /** - * Array of GeoJSON features. - */ - features: Array<{ - /** - * **Type**: "Feature" - */ - type: 'Feature'; - - /** - * Properties describing travel allowances and limitations. - */ - properties: { - /** - * Public name of the geofencing zone. - */ - name?: string; - - /** - * Start time of the geofencing zone in POSIX time. - * **Minimum**: 1450155600 - */ - start?: number; - - /** - * End time of the geofencing zone in POSIX time. - * **Minimum**: 1450155600 - */ - end?: number; - - /** - * Array of rules for the geofencing zone. - */ - rules?: Array<{ - /** - * Array of vehicle type IDs for which these restrictions apply. - */ - vehicle_type_id?: string[]; - - /** - * Is the undocked ride allowed to start and end in this zone? - */ - ride_allowed: boolean; - - /** - * Is the ride allowed to travel through this zone? - */ - ride_through_allowed: boolean; - - /** - * Maximum speed allowed, in kilometers per hour. - * **Minimum**: 0 - */ - maximum_speed_kph?: number; - }>; - }; - - /** - * A polygon that describes geofencing boundaries. - */ - geometry: { - /** - * **Type**: "MultiPolygon" - */ - type: 'MultiPolygon'; - - /** - * Array of coordinates following the right-hand rule. - */ - coordinates: number[][][][]; - }; - }>; - }; + geofencing_zones: FeatureCollection; }; } diff --git a/src/readers/gbfs/schemaV2/index.ts b/src/readers/gbfs/schemaV2/index.ts index 021e0396..4b3a1412 100644 --- a/src/readers/gbfs/schemaV2/index.ts +++ b/src/readers/gbfs/schemaV2/index.ts @@ -1,6 +1,7 @@ import { GBFSFreeBikeStatusV2, GBFSGeofencingZonesV2, + GBFSGeofencingZonesV2Properties, GBFSStationInformationV2, GBFSStationStatusV2, GBFSSystemAlertsV2, @@ -14,6 +15,14 @@ import { GBFSVersionsV2, } from '.'; +import type { + Feature, + FeatureIterator, + MultiPolygonGeometry, + PointGeometry, + Properties, +} from '../../..'; + export * from './freeBikeStatus'; export * from './gbfs'; export * from './gbfsVersions'; @@ -28,13 +37,97 @@ export * from './systemPricingPlans'; export * from './systemRegions'; export * from './vehicleTypes'; +/** Geofencing Feature */ +export type GBFSGeofencingFeatureV2 = Feature< + undefined, + Properties, + GBFSGeofencingZonesV2Properties, + MultiPolygonGeometry +>; + +/** Station Information feature properties */ +export interface GBFSStationV2FeaturesV2Properties extends Properties { + station_id: string; + name: string; + short_name?: string; + address?: string; + cross_street?: string; + region_id?: string; + post_code?: string; + rental_methods?: Array< + | 'key' + | 'creditcard' + | 'paypass' + | 'applepay' + | 'androidpay' + | 'transitcard' + | 'accountnumber' + | 'phone' + | 'KEY' + | 'CREDITCARD' + | 'PAYPASS' + | 'APPLEPAY' + | 'ANDROIDPAY' + | 'TRANSITCARD' + | 'ACCOUNTNUMBER' + | 'PHONE' + >; + is_virtual_station?: boolean; + parking_type?: + | 'parking_lot' + | 'street_parking' + | 'underground_parking' + | 'sidewalk_parking' + | 'other'; + parking_hoop?: boolean; + contact_phone?: string; + capacity?: number; + is_valet_station?: boolean; + is_charging_station?: boolean; + rental_uris?: { + android?: string; + ios?: string; + web?: string; + }; +} + +/** Station Information Point Feature */ +export type GBFSStationPointFeatureV2 = Feature< + undefined, + Properties, + GBFSStationV2FeaturesV2Properties, + PointGeometry +>; + +/** Station Information Area Feature */ +export type GBFSStationAreaFeatureV2 = Feature< + undefined, + Properties, + GBFSStationV2FeaturesV2Properties, + MultiPolygonGeometry +>; + +/** All potential feature types in a GBFS V2 specification */ +export type GBFSFeaturesV2 = + | GBFSGeofencingFeatureV2 + | GBFSStationPointFeatureV2 + | GBFSStationAreaFeatureV2; + +/** All potential feature property types in a GBFS V2 specification */ +export type GBFSFeaturePropertiesV2 = + | GBFSGeofencingZonesV2Properties + | GBFSStationV2FeaturesV2Properties; + /** * GBFS Version 1 Reader */ -export class GBFSReaderV2 { +export class GBFSReaderV2 + implements FeatureIterator +{ + version = 2; freeBikeStatus?: GBFSFreeBikeStatusV2; gbfs: GBFSV2; - versions?: GBFSVersionsV2; + gbfsVersions?: GBFSVersionsV2; geofencingZones?: GBFSGeofencingZonesV2; stationInformation?: GBFSStationInformationV2; stationStatus?: GBFSStationStatusV2; @@ -52,7 +145,7 @@ export class GBFSReaderV2 { */ constructor(gbfs: GBFSV2, feeds?: FeedResV2) { this.gbfs = gbfs; - this.versions = feeds?.gbfs_versions; + this.gbfsVersions = feeds?.gbfs_versions; this.geofencingZones = feeds?.geofencing_zones; this.systemInformation = feeds?.system_information as GBFSSystemInformationV2; this.stationInformation = feeds?.station_information; @@ -65,6 +158,76 @@ export class GBFSReaderV2 { this.systemRegions = feeds?.system_regions; this.vehicleTypes = feeds?.vehicle_types; } + + /** + * Yields all of the shapes + * @yields an iterator that contains shapes, stops, location data, and routes + */ + async *[Symbol.asyncIterator](): AsyncGenerator { + const { geofencingZones, stationInformation } = this; + if (geofencingZones !== undefined) { + const { + data: { geofencing_zones }, + } = geofencingZones; + for (const feature of geofencing_zones.features) yield feature as GBFSGeofencingFeatureV2; + } + if (stationInformation !== undefined) { + const { + data: { stations }, + } = stationInformation; + for (const station of stations) { + const { + lat, + lon, + station_id, + name, + short_name, + address, + cross_street, + region_id, + post_code, + rental_methods, + capacity, + rental_uris, + } = station; + const stationProperties: GBFSStationV2FeaturesV2Properties = { + station_id, + name, + short_name, + address, + cross_street, + region_id, + post_code, + rental_methods, + is_virtual_station: 'is_virtual_station' in station && station.is_virtual_station, + parking_type: 'parking_type' in station ? station.parking_type : undefined, + parking_hoop: 'parking_hoop' in station && station.parking_hoop, + contact_phone: 'contact_phone' in station ? station.contact_phone : undefined, + capacity, + is_valet_station: 'is_valet_station' in station && station.is_valet_station, + is_charging_station: 'is_charging_station' in station && station.is_charging_station, + rental_uris, + }; + const stationPoint: GBFSStationPointFeatureV2 = { + type: 'Feature', + properties: stationProperties, + geometry: { + type: 'Point', + coordinates: [lon, lat], + }, + }; + yield stationPoint; + if ('station_area' in station && station.station_area !== undefined) { + const stationArea: GBFSStationAreaFeatureV2 = { + type: 'Feature', + properties: stationProperties, + geometry: station.station_area, + }; + yield stationArea; + } + } + } + } } /** Parsing all the available feeds */ diff --git a/src/readers/gbfs/schemaV2/stationInformation.ts b/src/readers/gbfs/schemaV2/stationInformation.ts index 9095b2c5..89855b1a 100644 --- a/src/readers/gbfs/schemaV2/stationInformation.ts +++ b/src/readers/gbfs/schemaV2/stationInformation.ts @@ -1,3 +1,5 @@ +import type { MultiPolygonGeometry } from '../../..'; + /** * # GBFS Station Information Schema V2.3, V2.2, V2.1, OR V2.0 * List of all stations, their capacities, and locations. REQUIRED for systems utilizing docks. @@ -286,10 +288,7 @@ export interface GBFSStationInformationV23 { | 'phone' >; is_virtual_station?: boolean; - station_area?: { - type: 'MultiPolygon'; - coordinates: number[][][][]; - }; + station_area?: MultiPolygonGeometry; parking_type?: | 'parking_lot' | 'street_parking' @@ -560,10 +559,7 @@ export interface GBFSStationInformationV22 { | 'phone' >; is_virtual_station?: boolean; - station_area?: { - type: 'MultiPolygon'; - coordinates: number[][][][]; - }; + station_area?: MultiPolygonGeometry; capacity?: number; vehicle_capacity?: Record; is_valet_station?: boolean; @@ -825,10 +821,7 @@ export interface GBFSStationInformationV21 { | 'phone' >; is_virtual_station?: boolean; - station_area?: { - type: 'MultiPolygon'; - coordinates: number[][][][]; - }; + station_area?: MultiPolygonGeometry; capacity?: number; vehicle_capacity?: Record; is_valet_station?: boolean; diff --git a/src/readers/gbfs/schemaV3/geofencingZones.ts b/src/readers/gbfs/schemaV3/geofencingZones.ts index d09e22d9..8f737ca6 100644 --- a/src/readers/gbfs/schemaV3/geofencingZones.ts +++ b/src/readers/gbfs/schemaV3/geofencingZones.ts @@ -1,4 +1,4 @@ -import type { FeatureCollection, MValue, Properties } from '../../..'; +import type { FeatureCollection, MValue, Properties, ValueArrayObject } from '../../..'; /** * # GBFS Geofencing Zones Schema V3.1-RC & V3.0 @@ -230,7 +230,7 @@ export const gbfsGeofencingZonesSchemaV31RC = { /** * GBFS V3: Public name of the geofencing zone. */ -export interface GBFSGeofencingZonesV3PropertiesName { +export interface GBFSGeofencingZonesV3PropertiesName extends ValueArrayObject { /** * The translated text. */ @@ -245,10 +245,11 @@ export interface GBFSGeofencingZonesV3PropertiesName { /** * GBFS V3: Restrictions that apply within the area of the polygon. */ -export interface GBFSGeofencingZonesV3PropertiesRule { +export interface GBFSGeofencingZonesV3PropertiesRule extends ValueArrayObject { /** * Array of vehicle type IDs for which these restrictions apply. */ + // @ts-expect-error - we need to clean this / remove it vehicle_type_ids?: string[]; /** * Is the ride allowed to start in this zone? @@ -276,22 +277,18 @@ export interface GBFSGeofencingZonesV3PropertiesRule { /** Properties of a geofencing zone */ export interface GBFSGeofencingZonesV3Properties extends Properties { /** Public name of the geofencing zone. */ - // @ts-expect-error - this is ok name: GBFSGeofencingZonesV3PropertiesName[]; /** * Start time of the geofencing zone in RFC3339 format. * **format** date-time */ - // @ts-expect-error - this is ok start?: string; /** * End time of the geofencing zone in RFC3339 format. * **format** date-time */ - // @ts-expect-error - this is ok end?: string; /** Array of rules defining restrictions within the geofence. */ - // @ts-expect-error - this is ok rules?: GBFSGeofencingZonesV3PropertiesRule[]; } diff --git a/src/readers/gbfs/schemaV3/index.ts b/src/readers/gbfs/schemaV3/index.ts index b11a8835..02378db8 100644 --- a/src/readers/gbfs/schemaV3/index.ts +++ b/src/readers/gbfs/schemaV3/index.ts @@ -1,9 +1,9 @@ import { GBFSGeofencingZonesV3, GBFSGeofencingZonesV3Properties, + GBFSManifestV3, GBFSStationInformationV3, GBFSStationStatusV3, - GBFSStationV3, GBFSSystemAlertsV3, GBFSSystemInformationV3, GBFSSystemPricingPlansV3, @@ -37,41 +37,121 @@ export * from './vehicleStatus'; export * from './vehicleTypes'; /** Geofencing Feature */ -export type GeofencingFeature = Feature< +export type GBFSGeofencingFeatureV3 = Feature< undefined, Properties, GBFSGeofencingZonesV3Properties, MultiPolygonGeometry >; -/** StationPoint Feature */ -export type StationPointFeature = Feature< +/** Station Information feature properties */ +export interface GBFSStationV3FeaturesV3Properties extends Properties { + station_id: string; + name: Array<{ + text: string; + language: string; + }>; + short_name?: Array<{ + text: string; + language: string; + }>; + address?: string; + cross_street?: string; + region_id?: string; + post_code?: string; + station_opening_hours?: string; + rental_methods?: Array< + | 'key' + | 'creditcard' + | 'paypass' + | 'applepay' + | 'androidpay' + | 'transitcard' + | 'accountnumber' + | 'phone' + >; + is_virtual_station?: boolean; + parking_type?: + | 'parking_lot' + | 'street_parking' + | 'underground_parking' + | 'sidewalk_parking' + | 'other'; + parking_hoop?: boolean; + contact_phone?: string; + capacity?: number; + is_valet_station?: boolean; + is_charging_station?: boolean; + rental_uris?: { + android?: string; + ios?: string; + web?: string; + }; +} + +/** Station Information Point Feature */ +export type GBFSStationPointFeatureV3 = Feature< undefined, Properties, - GBFSStationV3, + GBFSStationV3FeaturesV3Properties, PointGeometry >; -/** StationArea Feature */ -export type StationAreaFeature = Feature< +/** Station Information Area Feature */ +export type GBFSStationAreaFeatureV3 = Feature< undefined, Properties, - GBFSStationV3, + GBFSStationV3FeaturesV3Properties, MultiPolygonGeometry >; -/** StationPoint Feature */ -export type VehiclePointFeature = Feature< +/** Vehicle Point Feature */ +export type GBFSVehiclePointFeatureV3 = Feature< undefined, Properties, GBFSVehicleV3, - PointGeometry + PointGeometry +>; + +/** Metadata Database Properties */ +export interface GBFSDatasetAreaPropertiesV3 extends Properties { + system_id: string; + versions?: { + version: '1.0' | '1.1' | '2.0' | '2.1' | '2.2' | '2.3' | '3.0' | '3.1-RC'; + url: string; + }[]; + country_code?: string; +} + +/** Metadata Database Feature */ +export type GBFSDatasetAreaFeatureV3 = Feature< + undefined, + Properties, + GBFSDatasetAreaPropertiesV3, + MultiPolygonGeometry >; +/** All potential feature types in a GBFS V3 specification */ +export type GBFSFeaturesV3 = + | GBFSGeofencingFeatureV3 + | GBFSStationPointFeatureV3 + | GBFSVehiclePointFeatureV3 + | GBFSDatasetAreaFeatureV3; + +/** All potential feature property types in a GBFS V3 specification */ +export type GBFSFeaturePropertiesV3 = + | GBFSGeofencingZonesV3Properties + | GBFSStationV3FeaturesV3Properties + | GBFSVehicleV3 + | GBFSDatasetAreaPropertiesV3; + /** * GBFS Version 3 Reader */ -export class GBFSReaderV3 implements FeatureIterator { +export class GBFSReaderV3 + implements FeatureIterator +{ + version = 3; gbfs: GBFSV3; gbfsVersions?: GBFSVersionsV3; systemInformation!: GBFSSystemInformationV3; @@ -82,6 +162,7 @@ export class GBFSReaderV3 implements FeatureIterator { systemRegions?: GBFSSystemRegionsV3; systemPricingPlans?: GBFSSystemPricingPlansV3; geofencingZones?: GBFSGeofencingZonesV3; + manifest?: GBFSManifestV3; /** * @param gbfs - the GBFS schema @@ -98,31 +179,70 @@ export class GBFSReaderV3 implements FeatureIterator { this.systemRegions = feeds?.system_regions; this.systemPricingPlans = feeds?.system_pricing_plans; this.geofencingZones = feeds?.geofencing_zones; + this.manifest = feeds?.manifest; } /** * Yields all of the shapes * @yields an iterator that contains shapes, stops, location data, and routes */ - async *[Symbol.asyncIterator](): AsyncGenerator< - GeofencingFeature | StationPointFeature | VehiclePointFeature - > { - const { geofencingZones, stationInformation, vehicleStatus } = this; + async *[Symbol.asyncIterator](): AsyncGenerator { + const { geofencingZones, stationInformation, vehicleStatus, manifest } = this; if (geofencingZones !== undefined) { const { data: { geofencing_zones }, } = geofencingZones; - for (const feature of geofencing_zones.features) yield feature as GeofencingFeature; + for (const feature of geofencing_zones.features) yield feature as GBFSGeofencingFeatureV3; } if (stationInformation !== undefined) { const { data: { stations }, } = stationInformation; for (const station of stations) { - const { lat, lon, station_area } = station; - const stationPoint: StationPointFeature = { + const { + lat, + lon, + station_area, + station_id, + name, + short_name, + address, + cross_street, + region_id, + post_code, + station_opening_hours, + rental_methods, + is_virtual_station, + parking_type, + parking_hoop, + contact_phone, + capacity, + is_valet_station, + is_charging_station, + rental_uris, + } = station; + const stationProperties = { + station_id, + name, + short_name, + address, + cross_street, + region_id, + post_code, + station_opening_hours, + rental_methods, + is_virtual_station, + parking_type, + parking_hoop, + contact_phone, + capacity, + is_valet_station, + is_charging_station, + rental_uris, + }; + const stationPoint: GBFSStationPointFeatureV3 = { type: 'Feature', - properties: { ...station }, + properties: stationProperties, geometry: { type: 'Point', coordinates: [lon, lat], @@ -130,14 +250,10 @@ export class GBFSReaderV3 implements FeatureIterator { }; yield stationPoint; if (station_area !== undefined) { - const stationArea: StationAreaFeature = { + const stationArea: GBFSStationAreaFeatureV3 = { type: 'Feature', - properties: { ...station }, - geometry: { - type: 'MultiPolygon', - // @ts-expect-error - bug, ignore for now - coordinates: station_area, - }, + properties: stationProperties, + geometry: station_area, }; yield stationArea; } @@ -150,7 +266,7 @@ export class GBFSReaderV3 implements FeatureIterator { for (const vehicle of vehicles) { const { lat, lon } = vehicle; if (lat === undefined || lon === undefined) continue; - const vehiclePoint: VehiclePointFeature = { + const vehiclePoint: GBFSVehiclePointFeatureV3 = { type: 'Feature', properties: { ...vehicle }, geometry: { @@ -161,6 +277,20 @@ export class GBFSReaderV3 implements FeatureIterator { yield vehiclePoint; } } + if (manifest !== undefined) { + const datasets = manifest.data.datasets; + for (const dataset of datasets) { + if ('area' in dataset && dataset.area !== undefined) { + const { system_id, versions, area, country_code } = dataset; + const areaFeature: GBFSDatasetAreaFeatureV3 = { + type: 'Feature', + properties: { system_id, versions, country_code }, + geometry: area, + }; + yield areaFeature; + } + } + } } } @@ -176,6 +306,7 @@ export interface FeedResV3 { system_regions?: GBFSSystemRegionsV3; system_pricing_plans?: GBFSSystemPricingPlansV3; geofencing_zones?: GBFSGeofencingZonesV3; + manifest?: GBFSManifestV3; } /** @@ -198,5 +329,14 @@ export async function buildGBFSReaderV3(gbfs: GBFSV3, path?: string): Promise (await res.json()) as GBFSManifestV3, + ); + } + return new GBFSReaderV3(gbfs, feedData); } diff --git a/src/readers/gbfs/schemaV3/manifest.ts b/src/readers/gbfs/schemaV3/manifest.ts index 26b44256..a210a172 100644 --- a/src/readers/gbfs/schemaV3/manifest.ts +++ b/src/readers/gbfs/schemaV3/manifest.ts @@ -1,3 +1,5 @@ +import type { MultiPolygonGeometry } from '../../..'; + /** * # GBFS Manifest Schema V3.1-RC & V3.0 * An index of gbfs.json URLs for each GBFS data set produced by a publisher. A single instance of @@ -182,18 +184,7 @@ export interface GBFSManifestV31RC { /** * A GeoJSON MultiPolygon that describes the operating area. */ - area?: { - /** - * GeoJSON MultiPolygon type. - * **Enum**: ['MultiPolygon'] - */ - type: 'MultiPolygon'; - - /** - * Coordinates of the MultiPolygon. - */ - coordinates: Array>>; - }; + area?: MultiPolygonGeometry; /** * The ISO 3166-1 alpha-2 country code of the operating area. diff --git a/src/readers/gbfs/schemaV3/stationInformation.ts b/src/readers/gbfs/schemaV3/stationInformation.ts index 6bce0270..7827cdb5 100644 --- a/src/readers/gbfs/schemaV3/stationInformation.ts +++ b/src/readers/gbfs/schemaV3/stationInformation.ts @@ -1,4 +1,4 @@ -import type { MultiPolygonGeometry, Properties } from '../../..'; +import type { MultiPolygonGeometry } from '../../..'; /** * # GBFS Station Information Schema V3.1-RC & V3.0 @@ -299,7 +299,7 @@ export const gbfsStationInformationSchemaV31RC = { /** * Information about a single station. */ -export interface GBFSStationV3 extends Properties { +export interface GBFSStationV3 { /** * Identifier of the station. */ @@ -338,7 +338,6 @@ export interface GBFSStationV3 extends Properties { /** * Short name or alternative identifier for the station. */ - // @ts-expect-error - allow for now short_name?: Array<{ text: string; language: string; @@ -347,38 +346,32 @@ export interface GBFSStationV3 extends Properties { /** * Address where the station is located. */ - // @ts-expect-error - allow for now address?: string; /** * Cross street or landmark where the station is located. */ - // @ts-expect-error - allow for now cross_street?: string; /** * Identifier of the region where the station is located. */ - // @ts-expect-error - allow for now region_id?: string; /** * Postal code where the station is located. */ - // @ts-expect-error - allow for now post_code?: string; /** * Hours of operation for the station in OSM opening_hours format. */ - // @ts-expect-error - allow for now station_opening_hours?: string; /** * Payment methods accepted at the station. * **Enum**: ['key', 'creditcard', 'paypass', 'applepay', 'androidpay', 'transitcard', 'accountnumber', 'phone'] */ - // @ts-expect-error - allow for now rental_methods?: Array< | 'key' | 'creditcard' @@ -393,20 +386,17 @@ export interface GBFSStationV3 extends Properties { /** * Is this station a location with or without physical infrastructure? (added in v2.1-RC) */ - // @ts-expect-error - allow for now is_virtual_station?: boolean; /** * A multipolygon describing the area of a virtual station. (added in v2.1-RC) */ - // @ts-expect-error - allow for now station_area?: MultiPolygonGeometry; /** * Type of parking station. (added in v2.3) * **Enum**: ['parking_lot', 'street_parking', 'underground_parking', 'sidewalk_parking', 'other'] */ - // @ts-expect-error - allow for now parking_type?: | 'parking_lot' | 'street_parking' @@ -417,26 +407,22 @@ export interface GBFSStationV3 extends Properties { /** * Are parking hoops present at this station? (added in v2.3) */ - // @ts-expect-error - allow for now parking_hoop?: boolean; /** * Contact phone of the station. (added in v2.3) */ - // @ts-expect-error - allow for now contact_phone?: string; /** * Total docking points installed at the station, both available and unavailable. * **Minimum**: 0 */ - // @ts-expect-error - allow for now capacity?: number; /** * Parking capacity for virtual stations per vehicle type. */ - // @ts-expect-error - allow for now vehicle_types_capacity?: Array<{ vehicle_type_ids: string[]; count: number; @@ -445,7 +431,6 @@ export interface GBFSStationV3 extends Properties { /** * Docking capacity per vehicle type at the station. */ - // @ts-expect-error - allow for now vehicle_docks_capacity?: Array<{ vehicle_type_ids: string[]; count: number; @@ -454,19 +439,16 @@ export interface GBFSStationV3 extends Properties { /** * Are valet services provided at the station? (added in v2.1-RC) */ - // @ts-expect-error - allow for now is_valet_station?: boolean; /** * Does the station support charging of electric vehicles? (added in v2.3-RC) */ - // @ts-expect-error - allow for now is_charging_station?: boolean; /** * Rental URIs for Android, iOS, and web. */ - // @ts-expect-error - allow for now rental_uris?: { /** * URI for Android apps. (added in v1.1) diff --git a/src/readers/gbfs/schemaV3/vehicleStatus.ts b/src/readers/gbfs/schemaV3/vehicleStatus.ts index d0a2c7b0..2efac826 100644 --- a/src/readers/gbfs/schemaV3/vehicleStatus.ts +++ b/src/readers/gbfs/schemaV3/vehicleStatus.ts @@ -200,14 +200,12 @@ export interface GBFSVehicleV3 extends Properties { * The latitude of the vehicle. * **Range**: [-90, 90] */ - // @ts-expect-error - This is ok for now lat?: number; /** * The longitude of the vehicle. * **Range**: [-180, 180] */ - // @ts-expect-error - This is ok for now lon?: number; /** @@ -223,7 +221,6 @@ export interface GBFSVehicleV3 extends Properties { /** * Contains rental URIs for Android, iOS, and web. */ - // @ts-expect-error - This is ok for now rental_uris?: { android?: string; // **Format**: uri ios?: string; // **Format**: uri @@ -233,53 +230,45 @@ export interface GBFSVehicleV3 extends Properties { /** * The vehicle_type_id of this vehicle (added in v2.1-RC). */ - // @ts-expect-error - This is ok for now vehicle_type_id?: string; /** * The last time this vehicle reported its status to the operator's backend. * **Format**: date-time */ - // @ts-expect-error - This is ok for now last_reported?: string; /** * The furthest distance in meters the vehicle can travel without recharging or refueling. * **Minimum**: 0 */ - // @ts-expect-error - This is ok for now current_range_meters?: number; /** * Current percentage of fuel or battery power remaining in the vehicle. * **Range**: [0, 1] */ - // @ts-expect-error - This is ok for now current_fuel_percent?: number; /** * Identifier referencing the station_id if the vehicle is currently at a station. */ - // @ts-expect-error - This is ok for now station_id?: string; /** * The station_id of the station this vehicle must be returned to. */ - // @ts-expect-error - This is ok for now home_station_id?: string; /** * The plan_id of the pricing plan this vehicle is eligible for. */ - // @ts-expect-error - This is ok for now pricing_plan_id?: string; /** * List of vehicle equipment provided by the operator. * **Enum**: ['child_seat_a', 'child_seat_b', 'child_seat_c', 'winter_tires', 'snow_chains'] */ - // @ts-expect-error - This is ok for now vehicle_equipment?: Array< 'child_seat_a' | 'child_seat_b' | 'child_seat_c' | 'winter_tires' | 'snow_chains' >; @@ -288,7 +277,6 @@ export interface GBFSVehicleV3 extends Properties { * The date and time when any rental of the vehicle must be completed. * **Pattern**: `^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(([+-]([0-9]{2}):([0-9]{2}))|Z)$` */ - // @ts-expect-error - This is ok for now available_until?: string; } diff --git a/tests/readers/gbfs/v1_1.test.ts b/tests/readers/gbfs/v1_1.test.ts index fa627fb3..ccd1d36f 100644 --- a/tests/readers/gbfs/v1_1.test.ts +++ b/tests/readers/gbfs/v1_1.test.ts @@ -23,7 +23,7 @@ import type { GBFSReaderV1 } from '../../../src'; const ajv = new Ajv({ strict: false }); addFormats(ajv); -test('version 1.1', async () => { +test('version 1.1', async (): Promise => { const server = buildServer(); const gbfsReader = (await buildGBFSReader( @@ -32,7 +32,8 @@ test('version 1.1', async () => { const { freeBikeStatus, gbfs, - versions, + version, + gbfsVersions, stationInformation, stationStatus, systemAlerts, @@ -43,6 +44,8 @@ test('version 1.1', async () => { systemRegions, } = gbfsReader; + expect(version).toEqual(1); + // freeBikeStatus expect(freeBikeStatus).toBeDefined(); const validFreeBikeStatus = ajv.compile(gbfsFreeBikeStatusSchemaV11); @@ -52,12 +55,12 @@ test('version 1.1', async () => { const validGBFS = ajv.compile(gbfsSchemaV11); expect(validGBFS(gbfs)).toBeTrue(); // versions - if (versions === undefined) throw new Error('versions is undefined'); - expect(versions).toBeDefined(); + if (gbfsVersions === undefined) throw new Error('versions is undefined'); + expect(gbfsVersions).toBeDefined(); // @ts-expect-error - version 2.2-google is not supported - versions.data.versions = versions.data.versions.filter((v) => v.version !== '2.2-google'); + gbfsVersions.data.versions = gbfsVersions.data.versions.filter((v) => v.version !== '2.2-google'); const validGBFSVersions = ajv.compile(gbfsVersionsSchemaV11); - expect(validGBFSVersions(versions)).toBeTrue(); + expect(validGBFSVersions(gbfsVersions)).toBeTrue(); // systemInformation expect(systemInformation).toBeDefined(); const validSystemInformation = ajv.compile(gbfsSystemInformationSchemaV11); @@ -93,6 +96,6 @@ test('version 1.1', async () => { await server.stop(); - // const features = await Array.fromAsync(gbfsReader); - // expect(features.length).toBe(104); + const features = await Array.fromAsync(gbfsReader); + expect(features.length).toBe(0); }); diff --git a/tests/readers/gbfs/v2_2.test.ts b/tests/readers/gbfs/v2_2.test.ts index 8cba6b20..1d88e13f 100644 --- a/tests/readers/gbfs/v2_2.test.ts +++ b/tests/readers/gbfs/v2_2.test.ts @@ -34,7 +34,8 @@ test('version 2.2', async () => { const { freeBikeStatus, gbfs, - versions, + version, + gbfsVersions, geofencingZones, stationInformation, stationStatus, @@ -47,6 +48,8 @@ test('version 2.2', async () => { vehicleTypes, } = gbfsReader; + expect(version).toEqual(2); + // freeBikeStatus expect(freeBikeStatus).toBeDefined(); const validFreeBikeStatus = ajv.compile(gbfsFreeBikeStatusSchemaV22); @@ -56,12 +59,12 @@ test('version 2.2', async () => { const validGBFS = ajv.compile(gbfsSchemaV22); expect(validGBFS(gbfs)).toBeTrue(); // versions - if (versions === undefined) throw new Error('versions is undefined'); - expect(versions).toBeDefined(); + if (gbfsVersions === undefined) throw new Error('versions is undefined'); + expect(gbfsVersions).toBeDefined(); // @ts-expect-error - version 2.2-google is not supported - versions.data.versions = versions.data.versions.filter((v) => v.version !== '2.2-google'); + gbfsVersions.data.versions = gbfsVersions.data.versions.filter((v) => v.version !== '2.2-google'); const validGBFSVersions = ajv.compile(gbfsVersionsSchemaV22); - expect(validGBFSVersions(versions)).toBeTrue(); + expect(validGBFSVersions(gbfsVersions)).toBeTrue(); // geofencingZones expect(geofencingZones).toBeDefined(); const validGeofencingZones = ajv.compile(gbfsGeofencingZonesSchemaV22); @@ -107,6 +110,6 @@ test('version 2.2', async () => { await server.stop(); - // const features = await Array.fromAsync(gbfsReader); - // expect(features.length).toBe(104); + const features = await Array.fromAsync(gbfsReader); + expect(features.length).toBe(92); }); diff --git a/tests/readers/gbfs/v3_0.test.ts b/tests/readers/gbfs/v3_0.test.ts index c1cd3ef0..9abedcc4 100644 --- a/tests/readers/gbfs/v3_0.test.ts +++ b/tests/readers/gbfs/v3_0.test.ts @@ -30,6 +30,7 @@ test('version 3.0', async () => { )) as GBFSReaderV3; const { gbfs, + version, gbfsVersions, systemInformation, stationInformation, @@ -39,8 +40,11 @@ test('version 3.0', async () => { systemRegions, systemPricingPlans, geofencingZones, + manifest, } = gbfsReader; + expect(version).toEqual(3); + // gbfs expect(gbfs).toBeDefined(); const validGBFS = ajv.compile(gbfsSchemaV30); @@ -81,9 +85,8 @@ test('version 3.0', async () => { expect(geofencingZones).toBeDefined(); const validGeofencingZones = ajv.compile(gbfsGeofencingZonesSchemaV30); expect(validGeofencingZones(geofencingZones)).toBeTrue(); - - // test manifest - const manifest = await Bun.file(`${__dirname}/fixtures/v3.0/manifest.json`).json(); + // manifest + expect(manifest).toBeDefined(); const validManifest = ajv.compile(gbfsManifestSchemaV30); expect(validManifest(manifest)).toBeTrue();