Skip to content

Commit

Permalink
feat: enable accessing INT ROKAS data (#54)
Browse files Browse the repository at this point in the history
* feat: SBBRokasMapStyler has int switch
* chore: document access to INT data
  • Loading branch information
smallTrogdor authored Nov 27, 2024
1 parent 08e7f7a commit 5aedf40
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 52 deletions.
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ The **SBB Karten** demo application is available both in the SBB Enterprise Play
| Feature | iOS | Android |
|-----------------------------------------| ------------------ | ------------------ |
| Gesture | :white_check_mark: | :white_check_mark: |
| Accessing INT Tiles & POIs | :white_check_mark: | :white_check_mark: |
| Camera | :white_check_mark: | :white_check_mark: |
| Map Styles (including ROKAS Styles) | :white_check_mark: | :white_check_mark: |
| Location (including device tracking) | :white_check_mark: | :white_check_mark: |
Expand Down Expand Up @@ -188,6 +189,24 @@ const SBBMapProperties({
});
```

#### Accessing INT Tiles & POIs

In order to access the INT data from [Journey Maps Tiles INT API], you need to register your application there and receive
a corresponding API Key. API Keys from the PROD API will not work. After that, either set the environment variable
`SBB_MAPS_INT_ENABLED` to `true`:

```bash
SBB_MAPS_INT_ENABLED=true
```

or pass the `useIntegrationData` constructor parameter to a `SBBRokasMapStyler`:

```dart
SBBRokasMapStyler.full(apiKey: Env.MY_INT_API_KEY_NAME, useIntegrationData=true);
```

Using INT data will log to console as an info.

### Gallery and Examples

#### Standard Map
Expand Down Expand Up @@ -272,7 +291,6 @@ See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).


[Journey Maps API]: (https://developer.sbb.ch/apis/journey-maps/information)

[Flutter Maplibre GL plugin]: (https://github.com/maplibre/flutter-maplibre-gl/tree/main)

[Journey Maps Tiles API]: (https://developer.sbb.ch/apis/journey-maps-tiles/information)
[Journey Maps Tiles API]: (https://developer.sbb.ch/apis/journey-maps-tiles/information)
[Journey Maps Tiles INT API]: (https://developer-int.sbb.ch/apis/journey-maps-tiles/information)
2 changes: 2 additions & 0 deletions example/lib/env.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ part 'env.g.dart';
abstract class Env {
@EnviedField(varName: 'JOURNEY_MAPS_TILES_API_KEY', obfuscate: true)
static String journeyMapsTilesApiKey = _Env.journeyMapsTilesApiKey;
@EnviedField(varName: 'JOURNEY_MAPS_TILES_INT_API_KEY', obfuscate: true)
static String journeyMapsTilesIntApiKey = _Env.journeyMapsTilesIntApiKey;
}
2 changes: 2 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:design_system_flutter/design_system_flutter.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sbb_maps_example/routes/integration_data_route.dart';
import 'routes/camera_route.dart';
import 'routes/custom_ui_route.dart';
import 'routes/display_annotations_route.dart';
Expand Down Expand Up @@ -56,6 +57,7 @@ class _MainAppState extends State<MainApp> {
'/poi': (context) => const POIRoute(),
'/routing': (context) => const RoutingRoute(),
'/map_properties': (context) => const MapPropertiesRoute(),
'/integration_data': (context) => const IntegrationDataRoute(),
'/display_annotations': (context) => const DisplayAnnotationsRoute(),
'/track_device_location': (context) => const TrackDeviceLocationRoute(),
},
Expand Down
4 changes: 4 additions & 0 deletions example/lib/routes/features_route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ class _FeaturesRouteState extends State<FeaturesRoute> {
title: 'Map Properties',
routeName: '/map_properties',
),
_FeatureRoute(
title: 'Integration Data',
routeName: '/integration_data',
),
_FeatureRoute(
title: 'Custom UI',
routeName: '/custom_ui',
Expand Down
107 changes: 107 additions & 0 deletions example/lib/routes/integration_data_route.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import 'package:design_system_flutter/design_system_flutter.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sbb_maps_example/env.dart';
import 'package:sbb_maps_example/theme_provider.dart';
import 'package:sbb_maps_flutter/sbb_maps_flutter.dart';

class IntegrationDataRoute extends StatefulWidget {
const IntegrationDataRoute({super.key});

@override
State<IntegrationDataRoute> createState() => _IntegrationDataRouteState();
}

class _IntegrationDataRouteState extends State<IntegrationDataRoute> {
bool useIntegration = false;

@override
Widget build(BuildContext context) {
final mapStyler = SBBRokasMapStyler.full(
apiKey: useIntegration ? Env.journeyMapsTilesIntApiKey : Env.journeyMapsTilesApiKey,
isDarkMode: Provider.of<ThemeProvider>(context).isDark,
useIntegrationData: useIntegration,
);

return Scaffold(
appBar: const SBBHeader(title: 'Integration Data'),
body: SBBMap(
mapStyler: mapStyler,
isMyLocationEnabled: false,
isFloorSwitchingEnabled: true,
builder: (context) => Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.all(sbbDefaultSpacing),
child: SBBMapIconButton(
onPressed: () {
showSBBModalSheet<bool>(
context: context,
title: 'Integration Data',
child: _IntegrationDataModalBody(useIntegration: useIntegration),
).then(_setStateWithProperties);
},
icon: SBBIcons.gears_small,
),
),
),
),
);
}

void _setStateWithProperties(bool? useIntegration) {
setState(
() {
if (useIntegration != null) {
this.useIntegration = useIntegration;
}
},
);
}
}

class _IntegrationDataModalBody extends StatefulWidget {
const _IntegrationDataModalBody({required this.useIntegration});

final bool useIntegration;

@override
State<_IntegrationDataModalBody> createState() => _IntegrationDataModalBodyState();
}

class _IntegrationDataModalBodyState extends State<_IntegrationDataModalBody> {
late bool _useIntegration;

@override
void initState() {
_useIntegration = widget.useIntegration;
super.initState();
}

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: sbbDefaultSpacing,
horizontal: sbbDefaultSpacing,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SBBCheckboxListItem(
value: _useIntegration,
label: 'Use INT Data',
secondaryLabel: 'Accesses developer-int.sbb.ch data.',
onChanged: (v) => setState(() {
_useIntegration = v ?? false;
}),
isLastElement: true,
),
const SizedBox(height: sbbDefaultSpacing),
SBBPrimaryButton(label: 'Apply Changes', onPressed: () => Navigator.pop(context, _useIntegration)),
const SizedBox(height: sbbDefaultSpacing),
],
),
);
}
}
52 changes: 44 additions & 8 deletions lib/src/sbb_map_style/sbb_rokas_map_styler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import 'package:sbb_maps_flutter/src/sbb_map_style/api_key_missing_exception.dar
///
/// The [initialStyleId] is `journey_maps_bright_v1`.
class SBBRokasMapStyler {
static _rokasStyleUrl(String styleId) => 'https://journey-maps-tiles.geocdn.sbb.ch/styles/$styleId/style.json';
static _rokasProdStyleUrl(String styleId) => 'https://journey-maps-tiles.geocdn.sbb.ch/styles/$styleId/style.json';
static _rokasIntStyleUrl(String styleId) => 'https://journey-maps-tiles.geocdn-int.sbb.ch/styles/$styleId/style.json';

static _rokasStyleUrl(String style, {isInt = false}) => isInt ? _rokasIntStyleUrl(style) : _rokasProdStyleUrl(style);

static const _brightV1 = 'journey_maps_bright_v1';
static const _darkV1 = 'journey_maps_dark_v1';
Expand All @@ -36,20 +39,30 @@ class SBBRokasMapStyler {
///
/// Throws an [ApiKeyMissing] exception **during runtime** if neither is given.
///
/// To use integration data for vector tiles and POIs, set
/// [useIntegrationData] to true.
///
/// The [initialStyleId] is `journey_maps_bright_v1`.
static SBBMapStyler full({String? apiKey, bool isDarkMode = false}) {
static SBBMapStyler full({
String? apiKey,
bool isDarkMode = false,
bool useIntegrationData = false,
}) {
final key = _apiKeyElseThrow(apiKey);

final isInt = useIntegrationData || _intEnvVarSet();
_logIfIsInt(isInt);

final rokasDefaultStyle = SBBMapStyle.fromURL(
id: _brightV1,
brightStyleURL: _rokasStyleUrl(_brightV1),
brightStyleURL: _rokasStyleUrl(_brightV1, isInt: isInt),
apiKey: key,
darkStyleURL: _rokasStyleUrl(_darkV1),
darkStyleURL: _rokasStyleUrl(_darkV1, isInt: isInt),
);

final aerialStyle = SBBMapStyle.fromURL(
id: _aerialV1,
brightStyleURL: _rokasStyleUrl(_aerialV1),
brightStyleURL: _rokasStyleUrl(_aerialV1, isInt: isInt),
apiKey: key,
);

Expand All @@ -76,15 +89,25 @@ class SBBRokasMapStyler {
///
/// Throws an [ApiKeyMissing] exception **during runtime** if neither is given.
///
/// To use integration data for vector tiles and POIs, set
/// [useIntegrationData] to true.
///
/// The [initialStyleId] is `journey_maps_bright_v1`.
static SBBMapStyler noAerial({String? apiKey, bool isDarkMode = false}) {
static SBBMapStyler noAerial({
String? apiKey,
bool isDarkMode = false,
bool useIntegrationData = false,
}) {
String key = _apiKeyElseThrow(apiKey);

final isInt = useIntegrationData || _intEnvVarSet();
_logIfIsInt(isInt);

final rokasDefaultStyle = SBBMapStyle.fromURL(
id: _brightV1,
brightStyleURL: _rokasStyleUrl(_brightV1),
brightStyleURL: _rokasStyleUrl(_brightV1, isInt: isInt),
apiKey: key,
darkStyleURL: _rokasStyleUrl(_darkV1),
darkStyleURL: _rokasStyleUrl(_darkV1, isInt: isInt),
);

return SBBCustomMapStyler(
Expand Down Expand Up @@ -118,4 +141,17 @@ class SBBRokasMapStyler {
}
return legacyKey;
}

static bool _intEnvVarSet() {
const intFlag = String.fromEnvironment('SBB_MAPS_INT_ENABLED');
if (intFlag.isNotEmpty) return intFlag == 'true';

return false;
}

static void _logIfIsInt(bool isInt) {
if (!isInt) return;
final logger = Logger();
logger.i('sbb_maps_flutter: You are currently opted in to use integration data.');
}
}
100 changes: 59 additions & 41 deletions test/src/sbb_map_style/sbb_rokas_map_styler_test.dart
Original file line number Diff line number Diff line change
@@ -1,49 +1,67 @@
import 'package:sbb_maps_flutter/sbb_maps_flutter.dart';
import 'package:sbb_maps_flutter/src/sbb_map_style/api_key_missing_exception.dart';
import 'package:test/expect.dart';
import 'package:test/test.dart';

void main() {
group('Unit Test SBBRokasMapStyler', () {
group('initalization', () {
test('whenFull_shouldReturnCustomMapStylerWithAllStyleIds', () {
// act
final actual = SBBRokasMapStyler.full(apiKey: 'key');

// expect
expect(actual, isA<SBBCustomMapStyler>());
expect(actual.getStyleIds().contains('journey_maps_aerial_v1'), equals(true));
expect(actual.getStyleIds().contains('journey_maps_bright_v1'), equals(true));
});

test('whenFull_shouldReturnInBrightMode', () {
// act
final actual = SBBRokasMapStyler.full(apiKey: 'key');

// expect
expect(actual, isA<SBBCustomMapStyler>());
expect(actual.isDarkMode, equals(false));
});

test('whenFull_shouldReturnStyleUriInBrightMode', () {
// arrange
const expectedUri = 'https://journey-maps-tiles.geocdn.sbb.ch'
'/styles/journey_maps_bright_v1/style.json?api_key=key';

// act
final actual = SBBRokasMapStyler.full(apiKey: 'key');

// expect
expect(actual, isA<SBBCustomMapStyler>());
expect(actual.currentStyleURI, equals(expectedUri));
});

test('whenNoAerial_shouldNotHaveAerial', () {
// act
final actual = SBBRokasMapStyler.noAerial(apiKey: 'key');

// expect
expect(actual, isA<SBBCustomMapStyler>());
expect(actual.getStyleIds().contains('journey_maps_aerial_v1'), equals(false));
});
test('whenFull_shouldReturnCustomMapStylerWithAllStyleIds', () {
// act
final actual = SBBRokasMapStyler.full(apiKey: 'key');

// expect
expect(actual, isA<SBBCustomMapStyler>());
expect(actual.getStyleIds().contains('journey_maps_aerial_v1'), equals(true));
expect(actual.getStyleIds().contains('journey_maps_bright_v1'), equals(true));
});

test('whenFull_shouldReturnInBrightMode', () {
// act
final actual = SBBRokasMapStyler.full(apiKey: 'key');

// expect
expect(actual, isA<SBBCustomMapStyler>());
expect(actual.isDarkMode, equals(false));
});

test('whenFull_shouldReturnStyleUriInBrightMode', () {
// arrange
const expectedUri = 'https://journey-maps-tiles.geocdn.sbb.ch'
'/styles/journey_maps_bright_v1/style.json?api_key=key';

// act
final actual = SBBRokasMapStyler.full(apiKey: 'key');

// expect
expect(actual, isA<SBBCustomMapStyler>());
expect(actual.currentStyleURI, equals(expectedUri));
});

test('whenNoAerial_shouldNotHaveAerial', () {
// act
final actual = SBBRokasMapStyler.noAerial(apiKey: 'key');

// expect
expect(actual, isA<SBBCustomMapStyler>());
expect(actual.getStyleIds().contains('journey_maps_aerial_v1'), equals(false));
});

test('whenNoApiKey_shouldThrowApiKeyMissingException', () {
// act + expect
expect(() => SBBRokasMapStyler.full(), throwsA(const TypeMatcher<ApiKeyMissing>()));
});

test('whenUseIntegrationDataIsTrue_uriShouldBeIntPointing', () {
// arrange
const expectedUri = 'https://journey-maps-tiles.geocdn-int.sbb.ch'
'/styles/journey_maps_bright_v1/style.json?api_key=key';

// act
final actual = SBBRokasMapStyler.full(apiKey: 'key', useIntegrationData: true);

// expect
expect(actual, isA<SBBCustomMapStyler>());
expect(actual.currentStyleURI, equals(expectedUri));
});
});
}

0 comments on commit 5aedf40

Please sign in to comment.