diff --git a/android/app/src/main/java/app/organicmaps/editor/EditorFragment.java b/android/app/src/main/java/app/organicmaps/editor/EditorFragment.java index e4f86fe97..20f6d4ead 100644 --- a/android/app/src/main/java/app/organicmaps/editor/EditorFragment.java +++ b/android/app/src/main/java/app/organicmaps/editor/EditorFragment.java @@ -1,13 +1,19 @@ package app.organicmaps.editor; +import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; +import android.text.Editable; import android.text.InputType; import android.text.TextUtils; +import android.text.TextWatcher; import android.text.method.LinkMovementMethod; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.AutoCompleteTextView; +import android.widget.GridLayout; +import android.widget.Toast; import androidx.annotation.CallSuper; import androidx.annotation.DrawableRes; import androidx.annotation.IdRes; @@ -22,6 +28,7 @@ import app.organicmaps.base.BaseMwmFragment; import app.organicmaps.dialog.EditTextDialogFragment; import app.organicmaps.editor.data.TimeFormatUtils; import app.organicmaps.sdk.Framework; +import app.organicmaps.sdk.bookmarks.data.ChargeSocketDescriptor; import app.organicmaps.sdk.bookmarks.data.Metadata; import app.organicmaps.sdk.editor.Editor; import app.organicmaps.sdk.editor.OpeningHours; @@ -30,26 +37,33 @@ import app.organicmaps.sdk.editor.data.LocalizedStreet; import app.organicmaps.sdk.editor.data.Timetable; import app.organicmaps.sdk.util.StringUtils; import app.organicmaps.sdk.util.Utils; +import app.organicmaps.sdk.util.log.Logger; import app.organicmaps.util.Graphics; import app.organicmaps.util.InputUtils; import app.organicmaps.util.UiUtils; - import com.google.android.material.button.MaterialButton; +import com.google.android.material.card.MaterialCardView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.imageview.ShapeableImageView; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import com.google.android.material.textview.MaterialTextView; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; public class EditorFragment extends BaseMwmFragment implements View.OnClickListener { final static String LAST_INDEX_OF_NAMES_ARRAY = "LastIndexOfNamesArray"; + private static final String CHARGE_SOCKETS_TAG = "CHARGE_SOCKETS_TAG"; private MaterialTextView mCategory; private View mCardName; private View mCardAddress; + private View mCardChargingStation; private View mCardDetails; private View mCardSocialMedia; private View mCardBuilding; @@ -130,6 +144,8 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe private TextInputLayout mInputHouseNumber; private TextInputLayout mInputBuildingLevels; + private View mChargeSockets; + private View mEmptyOpeningHours; private MaterialTextView mOpeningHours; private View mEditOpeningHours; @@ -206,6 +222,7 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe mWifi.setChecked(Editor.nativeHasWifi()); // TODO Reimplement this to avoid https://github.com/organicmaps/organicmaps/issues/9049 // mOutdoorSeating.setChecked(Editor.nativeGetSwitchInput(Metadata.MetadataType.FMD_OUTDOOR_SEATING.toInt(),"yes")); + refreshChargeSockets(); refreshOpeningTime(); refreshEditableFields(); refreshResetButton(); @@ -329,6 +346,14 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe final int[] editableDetails = Editor.nativeGetEditableProperties(); + // charge sockets have their own card; check whether we should display it. + boolean hasChargeSockets = false; + for (int type : editableDetails) + { + hasChargeSockets = hasChargeSockets || (type == Metadata.MetadataType.FMD_CHARGE_SOCKETS.toInt()); + } + UiUtils.showIf(hasChargeSockets, mCardChargingStation); + setCardVisibility(mCardDetails, mDetailsBlocks, editableDetails); setCardVisibility(mCardSocialMedia, mSocialMediaBlocks, editableDetails); } @@ -351,6 +376,283 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe UiUtils.showIf(anyBlockElement, card); } + /** + * Builds a dialog for editing or adding a charge socket. + * + * @param socketIndex The index of the socket to edit, or -1 to add a new socket. + * @param type The current type of the socket (e.g., "type2", "type2_combo"). + * @param count The current number of sockets of this type or 0 for new socket. + * @param power The current power output of the socket in kW or 0 for new socket. + * @return A MaterialAlertDialogBuilder instance for the configured dialog. + */ + private MaterialAlertDialogBuilder buildChargeSocketDialog(int socketIndex, String type, int count, double power) + { + LayoutInflater inflater = LayoutInflater.from(getActivity()); + View dialogView = inflater.inflate(R.layout.dialog_edit_socket, null); + + GridLayout typeBtns = dialogView.findViewById(R.id.edit_socket_type_grid); + typeBtns.removeAllViews(); + + List SOCKET_TYPES = Arrays.stream(getResources().getStringArray(R.array.charge_socket_types)).toList(); + for (String socket : SOCKET_TYPES) + { + MaterialButton btn = (MaterialButton) inflater.inflate(R.layout.button_socket_type, typeBtns, false); + + btn.setTag(R.id.socket_type, socket); + + // load SVG icon converted into VectorDrawable in res/drawable + @SuppressLint("DiscouragedApi") + int resIconId = + getResources().getIdentifier("ic_charge_socket_" + socket, "drawable", requireContext().getPackageName()); + if (resIconId != 0) + { + btn.setIcon(getResources().getDrawable(resIconId)); + } + + @SuppressLint("DiscouragedApi") + int resTypeId = + getResources().getIdentifier("charge_socket_" + socket, "string", requireContext().getPackageName()); + if (resTypeId != 0) + { + btn.setText(getResources().getString(resTypeId)); + } + + if (socket.equals(type)) + { + btn.setChecked(true); + } + + typeBtns.addView(btn); + } + + // manage the grid of socket type buttons as a single 'radio group' + // (this can not be done with a MaterialButtonToggleGroup because it does + // not support GridLayout) + List buttonList = new ArrayList<>(); + + for (int i = 0; i < typeBtns.getChildCount(); i++) + { + View child = typeBtns.getChildAt(i); + if (child instanceof MaterialButton button) + { + buttonList.add(button); + + button.setOnClickListener(view -> { + // deselect all + for (MaterialButton b : buttonList) + { + b.setChecked(false); + } + // select clicked + button.setChecked(true); + }); + } + } + + TextInputLayout countInputLayout = dialogView.findViewById(R.id.edit_socket_count_layout); + AutoCompleteTextView countView = dialogView.findViewById(R.id.edit_socket_count); + if (count > 0) + { + countView.setText(String.valueOf(count)); + } + + // Add a TextWatcher to validate on text change + countView.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + validatePositiveField(s.toString(), countInputLayout); + } + }); + + TextInputLayout powerInputLayout = dialogView.findViewById(R.id.edit_socket_power_layout); + AutoCompleteTextView powerView = dialogView.findViewById(R.id.edit_socket_power); + if (power > 0) + { + powerView.setText(String.valueOf(power)); + } + + // Add a TextWatcher to validate on text change + powerView.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + validatePositiveField(s.toString(), powerInputLayout); + } + }); + + return new MaterialAlertDialogBuilder(requireActivity(), R.style.MwmTheme_AlertDialog) + .setTitle(R.string.editor_socket) + .setView(dialogView) + .setPositiveButton(R.string.save, + (dialog, which) -> { + String socketType = ""; + for (MaterialButton b : buttonList) + { + if (b.isChecked()) + { + socketType = b.getTag(R.id.socket_type).toString(); + break; + } + } + + int countValue = 0; // 0 means 'unknown count' + try + { + countValue = Integer.parseInt(countView.getText().toString()); + } + catch (NumberFormatException ignored) + { + Logger.w(CHARGE_SOCKETS_TAG, "Invalid count value for socket:" + countView.getText().toString()); + } + + if (countValue < 0) + { + countValue = 0; + Logger.w(CHARGE_SOCKETS_TAG, "Invalid count value for socket:" + countView.getText().toString()); + } + + double powerValue = 0; // 0 means 'unknown power' + try + { + powerValue = Double.parseDouble(powerView.getText().toString()); + } + catch (NumberFormatException ignored) + { + Logger.w(CHARGE_SOCKETS_TAG, "Invalid power value for socket:" + powerView.getText().toString()); + } + + if (powerValue < 0) + { + powerValue = 0; + Logger.w(CHARGE_SOCKETS_TAG, "Invalid power value for socket:" + powerView.getText().toString()); + } + + ChargeSocketDescriptor socket = + new ChargeSocketDescriptor(socketType, countValue, powerValue); + + updateChargeSockets(socketIndex, socket); + }) + .setNegativeButton(R.string.cancel, (dialog, which) -> { dialog.dismiss(); }); + } + + // Helper method for validation logic + private boolean validatePositiveField(String text, TextInputLayout layout) { + if (text.isEmpty()) { + layout.setError(null); // No error if empty (assuming 0 is the default) + return true; + } + try { + double value = Double.parseDouble(text); + if (value < 0) { + layout.setError(getString(R.string.error_value_must_be_positive)); + return false; + } else { + layout.setError(null); + return true; + } + } catch (NumberFormatException e) { + layout.setError(getString(R.string.error_invalid_number)); + return false; + } + } + /** + * Updates the list of charge sockets. + * If socketIndex is >=0, it updates the socket at that index. + * Otherwise, it adds the new socket to the list. + * + * @param socketIndex The index of the socket to update, or -1 to add a new socket. + * @param socket The ChargeSocketDescriptor of the socket to add or update. + */ + private void updateChargeSockets(int socketIndex, ChargeSocketDescriptor socket) + { + ChargeSocketDescriptor[] sockets = Editor.nativeGetChargeSockets(); + if (socketIndex >= 0) + { + sockets[socketIndex] = socket; + } + else { + List list = new ArrayList<>(Arrays.asList(sockets)); + list.add(socket); + sockets = list.toArray(new ChargeSocketDescriptor[0]); + } + Editor.nativeSetChargeSockets(sockets); + + refreshChargeSockets(); + } + private void refreshChargeSockets() + { + ChargeSocketDescriptor[] sockets = Editor.nativeGetChargeSockets(); + + LayoutInflater inflater = LayoutInflater.from(requireContext()); + + GridLayout socketsGrid = mChargeSockets.findViewById(R.id.socket_grid_editor); + socketsGrid.removeAllViews(); + + for (int i = 0; i < sockets.length; i++) + { + final int currentIndex = i; + ChargeSocketDescriptor socket = sockets[i]; + + View itemView = inflater.inflate(R.layout.item_charge_socket, socketsGrid, false); + + MaterialTextView type = itemView.findViewById(R.id.socket_type); + ShapeableImageView icon = itemView.findViewById(R.id.socket_icon); + MaterialTextView power = itemView.findViewById(R.id.socket_power); + MaterialTextView count = itemView.findViewById(R.id.socket_count); + + // load SVG icon converted into VectorDrawable in res/drawable + @SuppressLint("DiscouragedApi") + int resIconId = getResources().getIdentifier("ic_charge_socket_" + socket.type(), "drawable", + requireContext().getPackageName()); + if (resIconId != 0) + { + icon.setImageResource(resIconId); + } + + @SuppressLint("DiscouragedApi") + int resTypeId = + getResources().getIdentifier("charge_socket_" + socket.type(), "string", requireContext().getPackageName()); + if (resTypeId != 0) + { + type.setText(resTypeId); + } + + if (socket.power() != 0) + { + DecimalFormat df = new DecimalFormat("#.##"); + power.setText(getString(R.string.kw_label, df.format(socket.power()))); + } + + if (socket.count() != 0) + { + count.setText(getString(R.string.count_label, socket.count())); + } + + itemView.setOnClickListener(v -> { + buildChargeSocketDialog(currentIndex, socket.type(), socket.count(), socket.power()).show(); + }); + socketsGrid.addView(itemView); + } + + // add a 'new item' button at the end, to create new sockets + View btnNewItemView = inflater.inflate(R.layout.button_new_item, socketsGrid, false); + btnNewItemView.setOnClickListener(v -> { + buildChargeSocketDialog(-1, "unknown", -1, -1).show(); + }); + socketsGrid.addView(btnNewItemView); + } + private void refreshOpeningTime() { final String openingHours = Editor.nativeGetOpeningHours(); @@ -435,6 +737,7 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe mCategory = categoryBlock.findViewById(R.id.name); mCardName = view.findViewById(R.id.cv__name); mCardAddress = view.findViewById(R.id.cv__address); + mCardChargingStation = view.findViewById(R.id.cv__charging_station); mCardDetails = view.findViewById(R.id.cv__details); mCardSocialMedia = view.findViewById(R.id.cv__social_media); mCardBuilding = view.findViewById(R.id.cv__building); @@ -507,6 +810,9 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe View blockOutdoorSeating = view.findViewById(R.id.block_outdoor_seating); mOutdoorSeating = view.findViewById(R.id.sw__outdoor_seating); blockOutdoorSeating.setOnClickListener(this); + + mChargeSockets = view.findViewById(R.id.block_charge_sockets); + View blockOpeningHours = view.findViewById(R.id.block_opening_hours); mEditOpeningHours = blockOpeningHours.findViewById(R.id.edit_opening_hours); mEditOpeningHours.setOnClickListener(this); diff --git a/android/app/src/main/java/app/organicmaps/widget/placepage/sections/PlacePageChargeSocketsFragment.java b/android/app/src/main/java/app/organicmaps/widget/placepage/sections/PlacePageChargeSocketsFragment.java index f9664def3..152614d2e 100644 --- a/android/app/src/main/java/app/organicmaps/widget/placepage/sections/PlacePageChargeSocketsFragment.java +++ b/android/app/src/main/java/app/organicmaps/widget/placepage/sections/PlacePageChargeSocketsFragment.java @@ -79,6 +79,9 @@ public class PlacePageChargeSocketsFragment extends Fragment implements Observer { View itemView = inflater.inflate(R.layout.item_charge_socket, mGrid, false); + itemView.setClickable(false); + itemView.setFocusable(false); + MaterialTextView type = itemView.findViewById(R.id.socket_type); ShapeableImageView icon = itemView.findViewById(R.id.socket_icon); MaterialTextView power = itemView.findViewById(R.id.socket_power); diff --git a/android/app/src/main/res/layout-land/dialog_edit_socket.xml b/android/app/src/main/res/layout-land/dialog_edit_socket.xml new file mode 100644 index 000000000..100fe10a1 --- /dev/null +++ b/android/app/src/main/res/layout-land/dialog_edit_socket.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/button_new_item.xml b/android/app/src/main/res/layout/button_new_item.xml new file mode 100644 index 000000000..9a27fc6fc --- /dev/null +++ b/android/app/src/main/res/layout/button_new_item.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/button_socket_type.xml b/android/app/src/main/res/layout/button_socket_type.xml new file mode 100644 index 000000000..3aee3018e --- /dev/null +++ b/android/app/src/main/res/layout/button_socket_type.xml @@ -0,0 +1,18 @@ + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/dialog_edit_socket.xml b/android/app/src/main/res/layout/dialog_edit_socket.xml new file mode 100644 index 000000000..b1c136e1b --- /dev/null +++ b/android/app/src/main/res/layout/dialog_edit_socket.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/fragment_editor.xml b/android/app/src/main/res/layout/fragment_editor.xml index b5bf98837..c21a66bf8 100644 --- a/android/app/src/main/res/layout/fragment_editor.xml +++ b/android/app/src/main/res/layout/fragment_editor.xml @@ -1,18 +1,18 @@ - + android:layout_height="match_parent" + android:clipToPadding="false" + tools:context=".editor.EditorActivity" + tools:ignore="DuplicateIds"> + + android:id="@+id/cv__category" + style="@style/MwmWidget.Editor.CardView"> - + android:layout_marginBottom="0dp" + android:padding="@dimen/margin_half_plus"> + + android:id="@+id/title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_marginStart="@dimen/margin_quarter" + android:layout_toEndOf="@id/icon" + android:text="@string/editor_edit_place_category_title" + android:textAppearance="@style/MwmTextAppearance.Body4" + tools:text="Trololo"/> + android:id="@+id/name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignStart="@id/title" + android:layout_below="@id/title" + android:textAppearance="@style/MwmTextAppearance.Body1" + tools:text="Ololo"/> - - - + android:id="@+id/cv__charging_station" + style="@style/MwmWidget.Editor.CardView" > - - - - + - - - + android:layout_marginTop="@dimen/margin_base" + android:fontFamily="@string/robotoMedium" + android:text="@string/charging_station_available_sockets" + android:textAppearance="@style/MwmTextAppearance.Body3" + tools:ignore="UnusedAttribute"/> - + android:id="@+id/block_charge_sockets" + layout="@layout/item_charging_sockets"/> + android:id="@+id/cv__name" + style="@style/MwmWidget.Editor.CardView"> + + + - + android:orientation="vertical" + android:paddingEnd="@dimen/margin_base" + android:paddingStart="@dimen/margin_base"> + + + + + + + + + + + + + + + + android:id="@+id/block_opening_hours" + layout="@layout/item_opening_hours"/> + android:id="@+id/block_operator" + layout="@layout/item_editor_input"/> + android:id="@+id/block_website" + layout="@layout/item_editor_input"/> + android:id="@+id/block_email" + layout="@layout/item_editor_input"/> + android:id="@+id/block_level" + layout="@layout/item_editor_input"/> + android:id="@+id/cv__social_media" + style="@style/MwmWidget.Editor.CardView"> - + android:orientation="vertical" + android:paddingEnd="@dimen/margin_base" + android:paddingStart="@dimen/margin_base"> + + android:id="@+id/block_facebook" + layout="@layout/item_editor_input"/> + android:id="@+id/block_instagram" + layout="@layout/item_editor_input"/> + android:id="@+id/block_twitter" + layout="@layout/item_editor_input"/> + android:id="@+id/block_vk" + layout="@layout/item_editor_input"/> + android:id="@+id/block_line" + layout="@layout/item_editor_input"/> @@ -357,61 +380,61 @@ + android:id="@+id/cv__building" + style="@style/MwmWidget.Editor.CardView"> - + android:orientation="vertical" + android:paddingEnd="@dimen/margin_base" + android:paddingStart="@dimen/margin_base"> + + android:id="@+id/block_levels" + layout="@layout/item_editor_input"/> + android:id="@+id/cv__more" + style="@style/MwmWidget.Editor.CardView"> - + android:orientation="vertical" + android:padding="@dimen/margin_base"> + + android:id="@+id/custom_input" + style="@style/MwmWidget.Editor.CustomTextInput" + android:gravity="center_vertical" + android:minHeight="74dp" + android:textColorHint="?android:textColorSecondary" + app:hintEnabled="false"> + android:id="@+id/input" + style="@style/MwmWidget.Editor.FieldLayout.EditText" + android:inputType="textMultiLine" + android:hint="@string/editor_note_hint"/> - diff --git a/android/app/src/main/res/layout/item_charging_sockets.xml b/android/app/src/main/res/layout/item_charging_sockets.xml new file mode 100644 index 000000000..5ca42266e --- /dev/null +++ b/android/app/src/main/res/layout/item_charging_sockets.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values-es/strings.xml b/android/app/src/main/res/values-es/strings.xml index 281fcc366..7de51b370 100644 --- a/android/app/src/main/res/values-es/strings.xml +++ b/android/app/src/main/res/values-es/strings.xml @@ -877,11 +877,14 @@ Confirmado %s Compartir traza No se ha encontrado ningún motor de texto-a-voz, comprueba la configuración de la aplicación - potencia desconocida + desconocida Tipo 2 (sin cable) Tipo 2 (con cable) Tipo 2 combo Tipo 1 NACS CHAdeMO + Recuento + Potencia (kW) + desconocido diff --git a/android/app/src/main/res/values-fr/strings.xml b/android/app/src/main/res/values-fr/strings.xml index 33e38033a..08aeeea9b 100644 --- a/android/app/src/main/res/values-fr/strings.xml +++ b/android/app/src/main/res/values-fr/strings.xml @@ -876,11 +876,14 @@ Existence confirmée %s Confirmé %s Impossible de lire ce texte à voix haute, vérifiez les paramètres de l’application - puissance inconnue + inconnue Type 2 (sans câble) Type 2 (avec câble) Type 2 combo Type 1 NACS CHAdeMO + Quantité + Puissance (kW) + inconnu diff --git a/android/app/src/main/res/values/arrays.xml b/android/app/src/main/res/values/arrays.xml index a6f3b36e2..de2c651d7 100644 --- a/android/app/src/main/res/values/arrays.xml +++ b/android/app/src/main/res/values/arrays.xml @@ -22,4 +22,32 @@ @drawable/route_point_19 @drawable/route_point_20 + + + type2_combo + nacs + chademo + type1 + type2_cable + type2 + unknown + + + + 22 + 43 + 50 + 100 + 150 + 200 + 350 + + + + 1 + 2 + 4 + 8 + 10 + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 99b4d4389..1ba4c7944 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -913,7 +913,7 @@ Share Track Delete %s? No text-to-speech engine found, check the app settings - unknown power + unknown Type 2 (no cable) Type 2 (w/ cable) Type 2 combo @@ -921,4 +921,14 @@ NACS CHAdeMO unknown socket + unknown socket + Create new sockets or edit existing ones. + Available sockets + Other or unknown + Count + Power (kW) + Edit socket + unknown + The value must be positive + Invalid number diff --git a/android/sdk/src/main/cpp/app/organicmaps/sdk/editor/Editor.cpp b/android/sdk/src/main/cpp/app/organicmaps/sdk/editor/Editor.cpp index c0f345930..bd0bfea2a 100644 --- a/android/sdk/src/main/cpp/app/organicmaps/sdk/editor/Editor.cpp +++ b/android/sdk/src/main/cpp/app/organicmaps/sdk/editor/Editor.cpp @@ -7,6 +7,7 @@ #include "indexer/cuisines.hpp" #include "indexer/editable_map_object.hpp" +#include "indexer/feature_charge_sockets.hpp" #include "indexer/feature_utils.hpp" #include "indexer/validate_and_format_contacts.hpp" @@ -94,6 +95,64 @@ JNIEXPORT void JNICALL Java_app_organicmaps_sdk_editor_Editor_nativeSetOpeningHo g_editableMapObject.SetOpeningHours(jni::ToNativeString(env, value)); } +JNIEXPORT jobjectArray JNICALL Java_app_organicmaps_sdk_editor_Editor_nativeGetChargeSockets(JNIEnv * env, jclass) +{ + auto sockets = g_editableMapObject.GetChargeSockets(); + + jclass descClass = env->FindClass("app/organicmaps/sdk/bookmarks/data/ChargeSocketDescriptor"); + jmethodID ctor = env->GetMethodID(descClass, "", "(Ljava/lang/String;ID)V"); + + // Create a Java array + jobjectArray result = env->NewObjectArray(sockets.size(), descClass, nullptr); + + for (size_t i = 0; i < sockets.size(); ++i) + { + auto const & s = sockets[i]; + + jstring jType = env->NewStringUTF(s.type.c_str()); + jobject jDesc = env->NewObject(descClass, ctor, jType, (jint)s.count, (jdouble)s.power); + + env->SetObjectArrayElement(result, i, jDesc); + + env->DeleteLocalRef(jType); + env->DeleteLocalRef(jDesc); + } + + return result; +} + +JNIEXPORT void JNICALL Java_app_organicmaps_sdk_editor_Editor_nativeSetChargeSockets(JNIEnv * env, jclass, + jobjectArray jSockets) +{ + ChargeSocketsHelper chargeSockets; + + jsize len = env->GetArrayLength(jSockets); + + jclass descClass = env->FindClass("app/organicmaps/sdk/bookmarks/data/ChargeSocketDescriptor"); + jfieldID fidType = env->GetFieldID(descClass, "type", "Ljava/lang/String;"); + jfieldID fidCount = env->GetFieldID(descClass, "count", "I"); + jfieldID fidPower = env->GetFieldID(descClass, "power", "D"); + + for (jsize i = 0; i < len; ++i) + { + jobject jDesc = env->GetObjectArrayElement(jSockets, i); + + jstring jType = (jstring)env->GetObjectField(jDesc, fidType); + char const * cType = env->GetStringUTFChars(jType, nullptr); + + jint count = env->GetIntField(jDesc, fidCount); + jdouble power = env->GetDoubleField(jDesc, fidPower); + + chargeSockets.AddSocket(cType, static_cast(count), static_cast(power)); + + env->ReleaseStringUTFChars(jType, cType); + env->DeleteLocalRef(jType); + env->DeleteLocalRef(jDesc); + } + + g_editableMapObject.SetChargeSockets(chargeSockets.ToString()); +} + JNIEXPORT jstring JNICALL Java_app_organicmaps_sdk_editor_Editor_nativeGetMetadata(JNIEnv * env, jclass, jint id) { auto const metaID = static_cast(id); diff --git a/android/sdk/src/main/java/app/organicmaps/sdk/editor/Editor.java b/android/sdk/src/main/java/app/organicmaps/sdk/editor/Editor.java index 920cbc8ba..b73198b55 100644 --- a/android/sdk/src/main/java/app/organicmaps/sdk/editor/Editor.java +++ b/android/sdk/src/main/java/app/organicmaps/sdk/editor/Editor.java @@ -5,6 +5,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Size; import androidx.annotation.WorkerThread; import app.organicmaps.sdk.Framework; +import app.organicmaps.sdk.bookmarks.data.ChargeSocketDescriptor; import app.organicmaps.sdk.bookmarks.data.Metadata; import app.organicmaps.sdk.editor.data.FeatureCategory; import app.organicmaps.sdk.editor.data.Language; @@ -62,6 +63,8 @@ public final class Editor public static native void nativeSetMetadata(int id, String value); public static native String nativeGetOpeningHours(); public static native void nativeSetOpeningHours(String openingHours); + public static native ChargeSocketDescriptor[] nativeGetChargeSockets(); + public static native void nativeSetChargeSockets(ChargeSocketDescriptor[] sockets); public static String nativeGetPhone() { return nativeGetMetadata(Metadata.MetadataType.FMD_PHONE_NUMBER.toInt()); diff --git a/data/editor.config b/data/editor.config index fe7403ee0..56c782b2b 100644 --- a/data/editor.config +++ b/data/editor.config @@ -160,7 +160,26 @@ + + + + + + + + + + + + + + + + + + + @@ -463,7 +482,9 @@ - + + + diff --git a/libs/editor/editor_config.cpp b/libs/editor/editor_config.cpp index 53b6c6fc0..4c4cbec3c 100644 --- a/libs/editor/editor_config.cpp +++ b/libs/editor/editor_config.cpp @@ -46,7 +46,12 @@ static std::unordered_map const kNamesToFMD = { {"drive_through", EType::FMD_DRIVE_THROUGH}, {"website_menu", EType::FMD_WEBSITE_MENU}, {"self_service", EType::FMD_SELF_SERVICE}, - {"outdoor_seating", EType::FMD_OUTDOOR_SEATING} + {"outdoor_seating", EType::FMD_OUTDOOR_SEATING}, + // TODO(skadge): this won't work, obv + {"socket_type1_count", EType::FMD_CHARGE_SOCKETS}, + {"socket_type1_output", EType::FMD_CHARGE_SOCKETS}, + {"socket_type2_count", EType::FMD_CHARGE_SOCKETS}, + {"socket_type2_output", EType::FMD_CHARGE_SOCKETS}, /// @todo Add description? }; diff --git a/libs/indexer/editable_map_object.cpp b/libs/indexer/editable_map_object.cpp index c1baa2e3a..c552eb6d7 100644 --- a/libs/indexer/editable_map_object.cpp +++ b/libs/indexer/editable_map_object.cpp @@ -2,6 +2,8 @@ #include "indexer/classificator.hpp" #include "indexer/edit_journal.hpp" +#include "indexer/feature_charge_sockets.hpp" +#include "indexer/feature_meta.hpp" #include "indexer/ftypes_matcher.hpp" #include "indexer/postcodes_matcher.hpp" #include "indexer/validate_and_format_contacts.hpp" @@ -158,6 +160,7 @@ void EditableMapObject::ForEachMetadataItem(function(feature::Metadata::FMD_COUNT); ++i) { auto const type = static_cast(i); + + // CHARGE_SOCKETS have multiple keys/values; handled separately further down + if (type == feature::Metadata::FMD_CHARGE_SOCKETS) + continue; std::string_view const & value = GetMetadata(type); std::string_view const & old_value = unedited_emo.GetMetadata(type); @@ -811,6 +826,15 @@ void EditableMapObject::LogDiffInJournal(EditableMapObject const & unedited_emo) if (cuisinesModified) m_journal.AddTagChange("cuisine", strings::JoinStrings(old_cuisines, ";"), strings::JoinStrings(new_cuisines, ";")); + + // charge sockets + auto chargeSocketsDiff = ChargeSocketsHelper(GetChargeSockets()).Diff(unedited_emo.GetChargeSockets()); + for (auto const & kvdiff : chargeSocketsDiff) + { + std::string key, old_value, new_value; + std::tie(key, old_value, new_value) = kvdiff; + m_journal.AddTagChange(key, old_value, new_value); + } } bool AreObjectsEqualIgnoringStreet(EditableMapObject const & lhs, EditableMapObject const & rhs) diff --git a/libs/indexer/editable_map_object.hpp b/libs/indexer/editable_map_object.hpp index 5717a7f17..90c8170a0 100644 --- a/libs/indexer/editable_map_object.hpp +++ b/libs/indexer/editable_map_object.hpp @@ -108,6 +108,7 @@ public: bool UpdateMetadataValue(std::string_view key, std::string value); void SetOpeningHours(std::string oh); + void SetChargeSockets(std::string sockets); void SetInternet(feature::Internet internet); /// @param[in] cuisine is a vector of osm cuisine ids.