Compare commits

..

20 Commits

Author SHA1 Message Date
map-per
c09831caaa Use list form SC
Signed-off-by: map-per <map-per@gmx.de>
2025-11-14 21:58:42 +01:00
map-per
005f731faa clang-format
Signed-off-by: map-per <map-per@gmx.de>
2025-11-13 17:46:05 +01:00
map-per
237894cd45 switch to boost::regex and only construct regex once
Signed-off-by: map-per <map-per@gmx.de>
2025-11-13 17:27:21 +01:00
Yannik Bloscheck
323b49388d [ios] Added file to Xcode
Signed-off-by: Yannik Bloscheck <git@yannikbloscheck.com>
2025-11-13 14:02:28 +01:00
map-per
da9f2af963 use full regex match
Signed-off-by: map-per <map-per@gmx.de>
2025-11-13 11:53:15 +01:00
map-per
f292298c4e use own kes_to_remove list
Signed-off-by: map-per <map-per@gmx.de>
2025-11-13 11:16:07 +01:00
map-per
be15215b83 fixes
Signed-off-by: map-per <map-per@gmx.de>
2025-11-13 10:25:35 +01:00
map-per
4ae64791ff [editor] Mark businesse as disused/vacant
Signed-off-by: map-per <map-per@gmx.de>
2025-11-13 10:13:59 +01:00
NoelClick
1de35bb5f8 [android] Adjust phrasing to "Opens on ... / Closes on ..." for clarity
- Update i18n strings following review suggestion.

Signed-off-by: NoelClick <dev@noel.click>
2025-11-12 23:07:53 +01:00
NoelClick
7b7df6ff2e [android] Remove unused full "minutes" plural resource
- Keep only `minutes_short` (`%d min`) for compact display.
- Ensures consistency with search UI and avoids redundant i18n strings.

Signed-off-by: NoelClick <dev@noel.click>
2025-11-12 23:07:53 +01:00
NoelClick
33e2f4854e [android] Use 60/15 min short-horizon windows (yellow) for closes/opens
- Closes: Yellow when <= 60 min
- Opens: Yellow when <= 15 min
- Keep compact unit via `plurals/minutes_short`
- Long-horizon cases continue to show "Open / Closed now • Opens / Closes ... at ..."
- Use full weekday names for clarity

Signed-off-by: NoelClick <dev@noel.click>
2025-11-12 23:07:53 +01:00
NoelClick
5b4fa55e83 [android] Use compact min unit for short opening-hours label
* Replace "minutes" plural with new "minutes_short" (`%d min`) for concise
  display on the place page.

Signed-off-by: NoelClick <dev@noel.click>
(cherry picked from commit 5468927a285466a5c5614328a4400abb5182d302)
Signed-off-by: NoelClick <dev@noel.click>
2025-11-12 23:07:53 +01:00
NoelClick
83256c4895 [android] Show "Opens / Closes X at Y" using formatter + add i18n strings
- Wire `PlacePageView.refreshOpenState()` to `OpenStateTextFormatter`.
- Keep <= 60 min branch with plurals (“Closes in %d minutes • at HH:mm”).
- Add day hint when next change is not today (“Opens Sat at 09:00”).
- Add localized strings with positional placeholders:
  - `opens_at` / `closes_at` (... `%s`).
  - `opens_day_at` / `closes_day_at` (`%1$s=%day`, `%2$s=%time`).

Refs: #2303

Signed-off-by: NoelClick <dev@noel.click>
(cherry picked from commit be80c7486882ab64a64efc30d0979d3674bbcc29)
Signed-off-by: NoelClick <dev@noel.click>
2025-11-12 23:07:53 +01:00
NoelClick
94542456a2 [android] Add OpenStateTextFormatter and JVM tests
- Introduce a tiny, pure formatter for opening-hours labels:
	- `formatHoursMinutes(12/24h)`, `isSameLocalDate()`, `dayShort()`
	- `buildAtLabel(...)` that accepts already-localized templates
- Add JVM unit tests for hour formatting and label selection.

Signed-off-by: NoelClick <dev@noel.click>
(cherry picked from commit df4b5f2281607e5a35b98b1007fb34eabd4aa657)
Signed-off-by: NoelClick <dev@noel.click>
2025-11-12 23:07:53 +01:00
patepelo
dd620c3f0c Add wiki debug command to docs
Signed-off-by: patepelo <developing.anton@gmail.com>
2025-11-13 02:01:27 +07:00
Jean-Baptiste
a42db17858 [android] Add icon before some settings
Signed-off-by: Jean-Baptiste <jeanbaptiste.charron@outlook.fr>
2025-11-12 19:13:37 +01:00
patepelo
738d0641ca Add Myanmar Burma synonyms
Signed-off-by: patepelo <developing.anton@gmail.com>
2025-11-12 21:16:34 +07:00
Yannik Bloscheck
4f5f8782fe [types] Adding lifebuoy
Signed-off-by: Yannik Bloscheck <git@yannikbloscheck.com>
2025-11-12 12:25:57 +01:00
Yannik Bloscheck
a886270dda [types] Add emergency access points
Signed-off-by: Yannik Bloscheck <git@yannikbloscheck.com>
2025-11-12 12:25:32 +01:00
x7z4w
66609ff08b [styles] Fix winery label
Signed-off-by: x7z4w <x7z4w@noreply.codeberg.org>
2025-11-12 10:49:38 +01:00
83 changed files with 944 additions and 410 deletions

View File

@@ -153,6 +153,7 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe
private final Map<Metadata.MetadataType, View> mDetailsBlocks = new HashMap<>();
private final Map<Metadata.MetadataType, View> mSocialMediaBlocks = new HashMap<>();
private MaterialButton mReset;
private MaterialButton mDisused;
private EditorHostFragment mParent;
@@ -827,6 +828,8 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe
osmInfo.setMovementMethod(LinkMovementMethod.getInstance());
mReset = view.findViewById(R.id.reset);
mReset.setOnClickListener(this);
mDisused = view.findViewById(R.id.disused);
mDisused.setOnClickListener(this);
mDetailsBlocks.put(Metadata.MetadataType.FMD_OPEN_HOURS, blockOpeningHours);
mDetailsBlocks.put(Metadata.MetadataType.FMD_PHONE_NUMBER, blockPhone);
@@ -894,6 +897,8 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe
mParent.addLanguage();
else if (id == R.id.reset)
reset();
else if (id == R.id.disused)
placeDisused();
else if (id == R.id.block_outdoor_seating)
mOutdoorSeating.toggle();
}
@@ -939,9 +944,12 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe
if (mParent.addingNewObject())
{
UiUtils.hide(mReset);
UiUtils.hide(mDisused);
return;
}
mDisused.setVisibility(Editor.nativeCanMarkPlaceAsDisused() ? View.VISIBLE : View.GONE);
if (Editor.nativeIsMapObjectUploaded())
{
mReset.setText(R.string.editor_place_doesnt_exist);
@@ -1014,6 +1022,19 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe
dialogFragment.setTextSaveListener(this::commitPlaceDoesntExists);
}
private void placeDisused()
{
new MaterialAlertDialogBuilder(requireActivity(), R.style.MwmTheme_AlertDialog)
.setTitle(R.string.editor_mark_business_vacant_title)
.setMessage(R.string.editor_mark_business_vacant_description)
.setPositiveButton(R.string.editor_submit, (dlg, which) -> {
Editor.nativeMarkPlaceAsDisused();
mParent.processEditedFeatures();
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
private void commitPlaceDoesntExists(@NonNull String text)
{
Editor.nativePlaceDoesNotExist(text);

View File

@@ -358,7 +358,7 @@ public class EditorHostFragment
.show();
}
private void processEditedFeatures()
public void processEditedFeatures()
{
if (OsmOAuth.isAuthorized())
{

View File

@@ -0,0 +1,49 @@
package app.organicmaps.widget.placepage;
import java.time.ZonedDateTime;
import java.time.format.TextStyle;
import java.util.Locale;
public class OpenStateTextFormatter
{
private OpenStateTextFormatter() {}
static String formatHoursMinutes(int hour, int minute, boolean use24h)
{
if (use24h)
return String.format(Locale.ROOT, "%02d:%02d", hour, minute);
int h = hour % 12;
if (h == 0) h = 12;
String ampm = (hour < 12) ? "AM" : "PM";
return String.format(Locale.ROOT, "%d:%02d %s", h, minute, ampm);
}
static boolean isSameLocalDate(ZonedDateTime a, ZonedDateTime b)
{
return a.toLocalDate().isEqual(b.toLocalDate());
}
static String dayShort(ZonedDateTime t, Locale locale)
{
return t.getDayOfWeek().getDisplayName(TextStyle.SHORT, locale);
}
static String buildAtLabel(
boolean opens,
boolean isToday,
String dayShort,
String time,
String opensAtLocalized,
String closesAtLocalized,
String opensDayAtLocalized,
String closesDayAtLocalized
)
{
if (isToday)
return opens ? String.format(Locale.ROOT, opensAtLocalized, time) // Opens at %s
: String.format(Locale.ROOT, closesAtLocalized, time); // Closes at %s
return opens ? String.format(Locale.ROOT, opensDayAtLocalized, dayShort, time) // Opens %s at %s
: String.format(Locale.ROOT, closesDayAtLocalized, dayShort, time); // Closes %s at %s
}
}

View File

@@ -85,9 +85,11 @@ import com.google.android.material.textview.MaterialTextView;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.TextStyle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
public class PlacePageView extends Fragment
implements View.OnClickListener, View.OnLongClickListener, LocationListener, SensorListener, Observer<MapObject>,
@@ -105,6 +107,10 @@ public class PlacePageView extends Fragment
private static final String LINKS_FRAGMENT_TAG = "LINKS_FRAGMENT_TAG";
private static final String TRACK_SHARE_MENU_ID = "TRACK_SHARE_MENU_ID";
private static final int SHORT_HORIZON_CLOSE_MIN = 60;
private static final int SHORT_HORIZON_OPEN_MIN = 15;
private static final List<CoordinatesFormat> visibleCoordsFormat =
Arrays.asList(CoordinatesFormat.LatLonDMS, CoordinatesFormat.LatLonDecimal, CoordinatesFormat.OLCFull,
CoordinatesFormat.UTM, CoordinatesFormat.MGRS, CoordinatesFormat.OSMLink);
@@ -797,57 +803,95 @@ public class PlacePageView extends Fragment
final String ohStr = mMapObject.getMetadata(Metadata.MetadataType.FMD_OPEN_HOURS);
final Timetable[] timetables = OpeningHours.nativeTimetablesFromString(ohStr);
if (timetables != null && timetables.length != 0)
// No valid timetable
if (timetables == null || timetables.length == 0)
{
final Context context = requireContext();
final OhState poiState = OpeningHours.nativeCurrentState(timetables);
// Ignore unknown rule state
if (poiState.state == OhState.State.Unknown)
{
UiUtils.hide(mTvOpenState);
return;
}
// Get colours
final ForegroundColorSpan colorGreen =
new ForegroundColorSpan(ContextCompat.getColor(context, R.color.base_green));
final ForegroundColorSpan colorYellow =
new ForegroundColorSpan(ContextCompat.getColor(context, R.color.base_yellow));
final ForegroundColorSpan colorRed = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.base_red));
// Get next state info
final SpannableStringBuilder openStateString = new SpannableStringBuilder();
final boolean isOpen = (poiState.state == OhState.State.Open); // False == Closed due to early exit for Unknown
final long nextStateTime = isOpen ? poiState.nextTimeClosed : poiState.nextTimeOpen; // Unix time (seconds)
final int minsToNextState = (int) ((nextStateTime - (System.currentTimeMillis() / 1000)) / 60);
if (minsToNextState <= 60) // POI opens/closes in 60 mins
{
final String minsToChangeStr = minsToNextState + " " + getString(R.string.minute);
final String nextChangeFormatted = getString(isOpen ? R.string.closes_in : R.string.opens_in, minsToChangeStr);
final ForegroundColorSpan nextChangeColor = isOpen ? colorYellow : colorRed;
// TODO: We should check closed/open time for specific feature's timezone.
ZonedDateTime time = ZonedDateTime.ofInstant(Instant.ofEpochSecond(nextStateTime), ZoneId.systemDefault());
String localizedTime =
new HoursMinutes(time.getHour(), time.getMinute(), DateUtils.is24HourFormat(context)).toString();
openStateString.append(nextChangeFormatted, nextChangeColor, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
.append("") // Add spacer
.append(getString(R.string.at, localizedTime));
}
else if (isOpen)
openStateString.append(getString(R.string.open_now), colorGreen, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// TODO: Add "Closes at 18:00" etc
else // Closed
openStateString.append(getString(R.string.closed_now), colorRed, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// TODO: Add "Opens at 18:00" etc
UiUtils.setTextAndHideIfEmpty(mTvOpenState, openStateString);
UiUtils.hide(mTvOpenState);
return;
}
// No valid timetable
UiUtils.hide(mTvOpenState);
final Context context = requireContext();
final OhState poiState = OpeningHours.nativeCurrentState(timetables);
// Ignore unknown rule state
if (poiState.state == OhState.State.Unknown)
{
UiUtils.hide(mTvOpenState);
return;
}
// Get colours
final ForegroundColorSpan colorGreen =
new ForegroundColorSpan(ContextCompat.getColor(context, R.color.base_green));
final ForegroundColorSpan colorYellow =
new ForegroundColorSpan(ContextCompat.getColor(context, R.color.base_yellow));
final ForegroundColorSpan colorRed = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.base_red));
// Get next state info
final SpannableStringBuilder openStateString = new SpannableStringBuilder();
final boolean isOpen = (poiState.state == OhState.State.Open); // False == Closed due to early exit for Unknown
final long nextStateTime = isOpen ? poiState.nextTimeClosed : poiState.nextTimeOpen; // Unix time (seconds)
final long nowSec = System.currentTimeMillis() / 1000;
final int minsToNextState = (int) ((nextStateTime - nowSec) / 60);
// NOTE: Timezone is currently device timezone. TODO: use feature-specific timezone.
final ZonedDateTime nextChangeLocal =
ZonedDateTime.ofInstant(Instant.ofEpochSecond(nextStateTime), ZoneId.systemDefault());
String localizedTimeString = OpenStateTextFormatter.formatHoursMinutes(
nextChangeLocal.getHour(), nextChangeLocal.getMinute(), DateUtils.is24HourFormat(context));
final boolean shortHorizonClosing = isOpen && minsToNextState >= 0 && minsToNextState <= SHORT_HORIZON_CLOSE_MIN;
final boolean shortHorizonOpening = !isOpen && minsToNextState >= 0 && minsToNextState <= SHORT_HORIZON_OPEN_MIN;
if (shortHorizonClosing || shortHorizonOpening) // POI Opens/Closes in 60 mins • at 18:00
{
final String minsToChangeStr = getResources().getQuantityString(
R.plurals.minutes_short, Math.max(minsToNextState, 1), Math.max(minsToNextState, 1));
final String nextChangeFormatted = getString(isOpen ? R.string.closes_in : R.string.opens_in, minsToChangeStr);
openStateString.append(nextChangeFormatted, colorYellow, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
.append("") // Add spacer
.append(getString(R.string.at, localizedTimeString));
}
else
{
final String opensAtStr = getString(R.string.opens_at); // "Opens at %s"
final String closesAtStr = getString(R.string.closes_at); // "Closes at %s"
final String opensDayAtStr = getString(R.string.opens_day_at); // "Opens %1$s at %2$s"
final String closesDayAtStr = getString(R.string.closes_day_at); // "Closes %1$s at %2$s"
final boolean isToday =
OpenStateTextFormatter.isSameLocalDate(nextChangeLocal, ZonedDateTime.now(nextChangeLocal.getZone()));
// Full weekday name per design feedback.
final String dayName =
nextChangeLocal.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.getDefault());
if (isOpen) // > 60 minutes OR negative (safety). Show “Open now • Closes at 18:00”
{
openStateString.append(getString(R.string.open_now), colorGreen, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
final String atLabel =
OpenStateTextFormatter.buildAtLabel(false, isToday, dayName, localizedTimeString,
opensAtStr, closesAtStr, opensDayAtStr, closesDayAtStr);
if (!TextUtils.isEmpty(atLabel))
openStateString.append("").append(atLabel);
}
else // Closed
{
openStateString.append(getString(R.string.closed_now), colorRed, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
final String atLabel =
OpenStateTextFormatter.buildAtLabel(true, isToday, dayName, localizedTimeString,
opensAtStr, closesAtStr, opensDayAtStr, closesDayAtStr);
if (!TextUtils.isEmpty(atLabel))
openStateString.append("").append(atLabel);
}
}
UiUtils.setTextAndHideIfEmpty(mTvOpenState, openStateString);
}
private void addPlace()

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?colorControlNormal">
<path
android:pathData="M80,360v-160q0,-33 23.5,-56.5T160,120h640q33,0 56.5,23.5T880,200v160h-80v-160L160,200v160L80,360ZM160,720q-33,0 -56.5,-23.5T80,640v-200h80v200h640v-200h80v200q0,33 -23.5,56.5T800,720L160,720ZM40,840v-80h880v80L40,840ZM480,420ZM80,440v-80h240q11,0 21,6t15,16l47,93 123,-215q5,-9 14,-14.5t20,-5.5q11,0 21,5.5t15,16.5l49,98h235v80L620,440q-11,0 -21,-5.5T584,418l-26,-53 -123,215q-5,10 -15,15t-21,5q-11,0 -20.5,-6T364,578l-69,-138L80,440Z"
android:fillColor="#fff"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?colorControlNormal">
<path
android:pathData="M480,840q-150,0 -255,-105T120,480q0,-150 105,-255t255,-105q14,0 27.5,1t26.5,3q-41,29 -65.5,75.5T444,300q0,90 63,153t153,63q55,0 101,-24.5t75,-65.5q2,13 3,26.5t1,27.5q0,150 -105,255T480,840ZM480,760q88,0 158,-48.5T740,585q-20,5 -40,8t-40,3q-123,0 -209.5,-86.5T364,300q0,-20 3,-40t8,-40q-78,32 -126.5,102T200,480q0,116 82,198t198,82ZM470,490Z"
android:fillColor="#fff"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?colorControlNormal">
<path
android:pathData="M19,9h-4v-6h-6v6h-4l7,7 7,-7ZM5,18v2h14v-2h-14Z"
android:fillColor="#FFF"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?colorControlNormal">
<path
android:pathData="M216,784q-45,-45 -70.5,-104T120,558q0,-63 24,-124.5T222,318q35,-35 86.5,-60t122,-39.5Q501,204 591.5,201t202.5,7q8,106 5,195t-16.5,160.5q-13.5,71.5 -38,125T684,778q-53,53 -112.5,77.5T450,880q-65,0 -127,-25.5T216,784ZM328,768q29,17 59.5,24.5T450,800q46,0 91,-18.5t86,-59.5q18,-18 36.5,-50.5t32,-85Q709,534 716,459.5t2,-177.5q-49,-2 -110.5,-1.5T485,290q-61,9 -116,29t-90,55q-45,45 -62,89t-17,85q0,59 22.5,103.5T262,714q42,-80 111,-153.5T534,440q-72,63 -125.5,142.5T328,768ZM328,768ZM328,768Z"
android:fillColor="#fff"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?colorControlNormal">
<path
android:pathData="M200,800v-280h-80v-80h240v80h-80v280h-80ZM200,360v-200h80v200h-80ZM360,360v-80h80v-120h80v120h80v80L360,360ZM440,800v-360h80v360h-80ZM680,800v-120h-80v-80h240v80h-80v120h-80ZM680,520v-360h80v360h-80Z"
android:fillColor="#fff"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?colorControlNormal">
<path
android:pathData="M280,920q-33,0 -56.5,-23.5T200,840v-720q0,-33 23.5,-56.5T280,40h400q33,0 56.5,23.5T760,120v124q18,7 29,22t11,34v80q0,19 -11,34t-29,22v404q0,33 -23.5,56.5T680,920L280,920ZM280,840h400v-720L280,120v720ZM280,840v-720,720ZM394,640h172q14,0 24,-10t10,-24v-132q0,-14 -10,-24t-24,-10h-6v-40q0,-33 -23.5,-56.5T480,320q-33,0 -56.5,23.5T400,400v40h-6q-14,0 -24,10t-10,24v132q0,14 10,24t24,10ZM440,440v-40q0,-17 11.5,-28.5T480,360q17,0 28.5,11.5T520,400v40h-80Z"
android:fillColor="#fff"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?colorControlNormal">
<path
android:pathData="M339,698q22,-22 49.5,-36t58.5,-19q-5,19 -8,38t-3,39q0,24 3.5,47t11.5,44L339,698ZM254,614 L170,528q62,-62 142,-95t168,-33q49,0 96,10.5t90,30.5q-44,8 -81.5,29T517,522q-9,-1 -18.5,-1.5T480,520q-64,0 -122.5,24.5T254,614ZM84,444 L0,360q95,-97 219.5,-148.5T480,160q136,0 260.5,51.5T960,360l-84,84q-79,-79 -181.5,-121.5T480,280q-112,0 -214.5,42.5T84,444ZM760,520 L772,580q12,5 22.5,10.5T816,604l58,-18 40,68 -46,40q2,12 2,26t-2,26l46,40 -40,68 -58,-18q-11,8 -21.5,13.5T772,860l-12,60h-80l-12,-60q-12,-5 -22.5,-10.5T624,836l-58,18 -40,-68 46,-40q-2,-12 -2,-26t2,-26l-46,-40 40,-68 58,18q11,-8 21.5,-13.5T668,580l12,-60h80ZM720,640q-33,0 -56.5,23.5T640,720q0,33 23.5,56.5T720,800q33,0 56.5,-23.5T800,720q0,-33 -23.5,-56.5T720,640Z"
android:fillColor="#fff"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?colorControlNormal">
<path
android:pathData="M480,480q-66,0 -113,-47t-47,-113q0,-66 47,-113t113,-47q66,0 113,47t47,113q0,66 -47,113t-113,47ZM160,800v-112q0,-34 17.5,-62.5T224,582q62,-31 126,-46.5T480,520q66,0 130,15.5T736,582q29,15 46.5,43.5T800,688v112L160,800ZM240,720h480v-32q0,-11 -5.5,-20T700,654q-54,-27 -109,-40.5T480,600q-56,0 -111,13.5T260,654q-9,5 -14.5,14t-5.5,20v32ZM480,400q33,0 56.5,-23.5T560,320q0,-33 -23.5,-56.5T480,240q-33,0 -56.5,23.5T400,320q0,33 23.5,56.5T480,400ZM480,320ZM480,720Z"
android:fillColor="#fff"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?colorControlNormal">
<path
android:pathData="m798,638 l-62,-62q44,-41 69,-97t25,-119q0,-63 -25,-118t-69,-96l62,-64q56,53 89,125t33,153q0,81 -33,153t-89,125ZM670,510l-64,-64q18,-17 29,-38.5t11,-47.5q0,-26 -11,-47.5T606,274l64,-64q32,29 50,67.5t18,82.5q0,44 -18,82.5T670,510ZM360,520q-66,0 -113,-47t-47,-113q0,-66 47,-113t113,-47q66,0 113,47t47,113q0,66 -47,113t-113,47ZM40,840v-112q0,-33 17,-62t47,-44q51,-26 115,-44t141,-18q77,0 141,18t115,44q30,15 47,44t17,62v112L40,840ZM120,760h480v-32q0,-11 -5.5,-20T580,694q-36,-18 -92.5,-36T360,640q-71,0 -127.5,18T140,694q-9,5 -14.5,14t-5.5,20v32ZM360,440q33,0 56.5,-23.5T440,360q0,-33 -23.5,-56.5T360,280q-33,0 -56.5,23.5T280,360q0,33 23.5,56.5T360,440ZM360,360ZM360,760Z"
android:fillColor="#fff"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?colorControlNormal">
<path
android:pathData="M360,440h80v-160h-80v160ZM480,440h80v-160h-80v160ZM600,440h80v-160h-80v160ZM240,880q-33,0 -56.5,-23.5T160,800v-480l240,-240h320q33,0 56.5,23.5T800,160v640q0,33 -23.5,56.5T720,880L240,880ZM240,800h480v-640L434,160L240,354v446ZM240,800h480,-480Z"
android:fillColor="#fff"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M13.26,3C8.17,2.86 4,6.95 4,12L2.21,12c-0.45,0 -0.67,0.54 -0.35,0.85l2.79,2.8c0.2,0.2 0.51,0.2 0.71,0l2.79,-2.8c0.31,-0.31 0.09,-0.85 -0.36,-0.85L6,12c0,-3.9 3.18,-7.05 7.1,-7 3.72,0.05 6.85,3.18 6.9,6.9 0.05,3.91 -3.1,7.1 -7,7.1 -1.61,0 -3.1,-0.55 -4.28,-1.48 -0.4,-0.31 -0.96,-0.28 -1.32,0.08 -0.42,0.42 -0.39,1.13 0.08,1.49C9,20.29 10.91,21 13,21c5.05,0 9.14,-4.17 9,-9.26 -0.13,-4.69 -4.05,-8.61 -8.74,-8.74zM12.75,8c-0.41,0 -0.75,0.34 -0.75,0.75v3.68c0,0.35 0.19,0.68 0.49,0.86l3.12,1.85c0.36,0.21 0.82,0.09 1.03,-0.26 0.21,-0.36 0.09,-0.82 -0.26,-1.03l-2.88,-1.71v-3.4c0,-0.4 -0.34,-0.74 -0.75,-0.74z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?colorControlNormal">
<path
android:pathData="M480,560q-33,0 -56.5,-23.5T400,480q0,-33 23.5,-56.5T480,400q33,0 56.5,23.5T560,480q0,33 -23.5,56.5T480,560ZM480,840q-139,0 -241,-91.5T122,520h82q14,104 92.5,172T480,760q117,0 198.5,-81.5T760,480q0,-117 -81.5,-198.5T480,200q-69,0 -129,32t-101,88h110v80L120,400v-240h80v94q51,-64 124.5,-99T480,120q75,0 140.5,28.5t114,77q48.5,48.5 77,114T840,480q0,75 -28.5,140.5t-77,114q-48.5,48.5 -114,77T480,840Z"
android:fillColor="#fff"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M2,20h20v-4h-20v4ZM4,17h2v2h-2v-2ZM2,4v4h20v-4h-20ZM6,7h-2v-2h2v2ZM2,14h20v-4h-20v4ZM4,11h2v2h-2v-2Z"
android:fillColor="#FFF"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?colorControlNormal">
<path
android:pathData="M160,720q-33,0 -56.5,-23.5T80,640v-320q0,-33 23.5,-56.5T160,240h640q33,0 56.5,23.5T880,320v320q0,33 -23.5,56.5T800,720L160,720ZM160,640h640v-320L680,320v160h-80v-160h-80v160h-80v-160h-80v160h-80v-160L160,320v320ZM280,480h80,-80ZM440,480h80,-80ZM600,480h80,-80ZM480,480Z"
android:fillColor="#fff"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?colorControlNormal">
<path
android:pathData="M280,800v-520L80,280v-120h520v120L400,280v520L280,800ZM640,800v-320L520,480v-120h360v120L760,480v320L640,800Z"
android:fillColor="#fff"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?colorControlNormal">
<path
android:pathData="M480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480h80q0,115 72.5,203T418,794l-58,-58 56,-56L598,862q-29,10 -58.5,14T480,880ZM500,600v-240h120q17,0 28.5,11.5T660,400v160q0,17 -11.5,28.5T620,600L500,600ZM300,600v-60h100v-40h-60v-40h60v-40L300,420v-60h120q17,0 28.5,11.5T460,400v160q0,17 -11.5,28.5T420,600L300,600ZM560,540h40v-120h-40v120ZM800,480q0,-115 -72.5,-203T542,166l58,58 -56,56 -182,-182q29,-10 58.5,-14t59.5,-4q83,0 156,31.5T763,197q54,54 85.5,127T880,480h-80Z"
android:fillColor="#fff"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?colorControlNormal">
<path
android:pathData="m476,880 l182,-480h84L924,880h-84l-43,-122L603,758L560,880h-84ZM160,760l-56,-56 202,-202q-35,-35 -63.5,-80T190,320h84q20,39 40,68t48,58q33,-33 68.5,-92.5T484,240L40,240v-80h280v-80h80v80h280v80L564,240q-21,72 -63,148t-83,116l96,98 -30,82 -122,-125 -202,201ZM628,688h144l-72,-204 -72,204Z"
android:fillColor="#fff"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?colorControlNormal">
<path
android:pathData="m476,880 l182,-480h84L924,880h-84l-43,-122L603,758L560,880h-84ZM628,688h144l-72,-204 -72,204ZM254,640q-66,0 -123.5,-38.5T44,498l72,-36q21,42 58,70t79,28q38,0 62.5,-23.5T340,480q0,-33 -23.5,-56.5T260,400h-60v-80h60q25,0 42.5,-17.5T320,260q0,-25 -17,-42.5T261,200q-23,0 -41,15t-32,33l-63,-49q26,-32 60,-55.5t77,-23.5q57,0 97.5,40.5T400,259q0,27 -10,52.5T361,357q10,10 18.5,20.5T396,400h124v-200h-80v-80h240v80h-80v116l-61,164L420,480v4q0,63 -46,109.5T254,640Z"
android:fillColor="#fff"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?colorControlNormal">
<path
android:pathData="M480,840 L0,359q93,-93 215.5,-146T480,160q142,0 264.5,53T960,359l-56,57q-81,-81 -190,-128.5T480,240q-103,0 -195,32.5T117,363l419,420 -56,57ZM864,800L761,698q-18,11 -38,16.5t-43,5.5q-68,0 -114,-46t-46,-114q0,-68 46,-114t114,-46q68,0 114,46t46,114q0,23 -5.5,43T818,641l102,103 -56,56ZM680,640q34,0 57,-23t23,-57q0,-34 -23,-57t-57,-23q-34,0 -57,23t-23,57q0,34 23,57t57,23ZM480,783Z"
android:fillColor="#fff"/>
</vector>

View File

@@ -394,7 +394,8 @@
<com.google.android.material.card.MaterialCardView
android:id="@+id/cv__more"
style="@style/MwmWidget.Editor.CardView">
style="@style/MwmWidget.Editor.CardView"
android:layout_marginBottom="@dimen/margin_base">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -421,6 +422,17 @@
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.button.MaterialButton
android:id="@+id/disused"
style="@style/MwmWidget.M3.Button.Secondary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginBottom="@dimen/margin_quarter"
app:backgroundTint="?cardBackground"
android:textColor="@color/base_red"
app:strokeColor="@color/base_red"
android:text="@string/editor_business_vacant_button"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/reset"
style="@style/MwmWidget.M3.Button.Secondary"

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Items in the Preferences can have space reserved for an icon. As none of the
settings currently have associated icons, this leads to items that seem indented
without purpose. This wastes space and can lead to truncation of the item names
and descriptions. See https://github.com/organicmaps/organicmaps/issues/1872
To rectify this, the iconSpaceReserved property needs to be set to false.
According to https://developer.android.com/reference/android/preference/Preference#attr_android:iconSpaceReserved
false should be the default. However, according to
https://material.io/design/platform-guidance/android-settings.html this goes
against the material design guidelines and the default was overridden to true in
https://cs.android.com/android/platform/superproject/+/android-9.0.0_r1:prebuilts/sdk/current/support/v7/preference/res/values-sw360dp-v13/values-sw360dp-v13.xml
This file sets the default value back to false (i.e. no space reserved for icons).
See also the discussion at https://github.com/organicmaps/organicmaps/pull/1924
-->
<resources xmlns:tools="http://schemas.android.com/tools">
<bool name="config_materialPreferenceIconSpaceReserved" tools:ignore="MissingDefaultResource">false</bool>
</resources>

View File

@@ -441,6 +441,14 @@
<string name="opens_in">Opens in %s</string>
<string name="closes_in">Closes in %s</string>
<string name="closed">Closed</string>
<string name="opens_at">Opens at %s</string>
<string name="closes_at">Closes at %s</string>
<string name="opens_day_at">Opens on %1$s at %2$s</string>
<string name="closes_day_at">Closes on %1$s at %2$s</string>
<plurals name="minutes_short">
<item quantity="one">%d min</item>
<item quantity="other">%d min</item>
</plurals>
<!-- Used in the opening_hours fragment for the last checked date, eg. "Confirmed two weeks ago" -->
<string name="hours_confirmed_time_ago">Confirmed %s</string>
<!-- Used on the place page for the last checked date, eg. "Existence confirmed two weeks ago" -->
@@ -543,6 +551,14 @@
<string name="editor_place_doesnt_exist_description">Describe what the place looks like now to send an error note to the OpenStreetMap community</string>
<!-- Error message for "Place doesn't exist" dialog when comment is empty -->
<string name="delete_place_empty_comment_error">Please indicate the reason for deleting the place</string>
<!-- Button in the editor to mark business as vacant -->
<string name="editor_business_vacant_button">Business is vacant</string>
<!-- Title of confirmation dialog before marking business as vacant -->
<string name="editor_mark_business_vacant_title">Mark business as vacant</string>
<!-- Description in confirmation dialog before marking business as vacant -->
<string name="editor_mark_business_vacant_description">Use this if the business has moved out and the space is empty and ready for a new tenant.</string>
<!-- Submit change to OSM in the editor -->
<string name="editor_submit">Submit</string>
<!-- Phone number error message -->
<string name="error_enter_correct_phone">Enter a valid phone number</string>
<string name="error_enter_correct_web">Enter a valid web address</string>

View File

@@ -1,24 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
android:key="backup_location"
android:summary="@string/pref_backup_location_summary_initial"
android:title="@string/pref_backup_location_title" />
android:title="@string/pref_backup_location_title"
app:iconSpaceReserved="false" />
<Preference
android:key="backup_now"
android:summary="@string/pref_backup_now_summary"
android:title="@string/pref_backup_now_title" />
android:title="@string/pref_backup_now_title"
app:iconSpaceReserved="false" />
<ListPreference
android:defaultValue="86400000"
android:entries="@array/backup_interval_entries"
android:entryValues="@array/backup_interval_values"
android:key="backup_history_interval"
android:title="@string/pref_backup_interval_title" />
android:title="@string/pref_backup_interval_title"
app:iconSpaceReserved="false" />
<ListPreference
android:defaultValue="10"
android:entries="@array/backup_history_entries"
android:entryValues="@array/backup_history_values"
android:key="backup_history_count"
android:title="@string/pref_backup_history_title" />
android:title="@string/pref_backup_history_title"
app:iconSpaceReserved="false" />
</PreferenceScreen>

View File

@@ -7,6 +7,7 @@
<Preference
android:key="@string/pref_osm_profile"
android:title="@string/profile"
app:icon="@drawable/ic_profile"
tools:summary="LongLongUsernameHere"
app:singleLineTitle="false"
android:order="1"/>
@@ -17,6 +18,7 @@
<ListPreference
android:key="@string/pref_munits"
android:title="@string/measurement_units"
app:icon="@drawable/ic_straighten"
app:singleLineTitle="false"
android:summary="@string/measurement_units_summary"
android:entries="@array/measument_units"
@@ -42,6 +44,7 @@
<SwitchPreferenceCompat
android:key="@string/pref_autodownload"
android:title="@string/autodownload"
app:icon="@drawable/ic_download_st"
app:singleLineTitle="false"
android:order="5"/>
<SwitchPreferenceCompat
@@ -49,22 +52,26 @@
android:title="@string/big_font"
app:singleLineTitle="false"
android:defaultValue="false"
app:icon="@drawable/ic_text_fields"
android:order="6"/>
<SwitchPreferenceCompat
android:key="@string/pref_transliteration"
android:title="@string/transliteration_title"
app:singleLineTitle="false"
app:icon="@drawable/ic_translate_indic"
android:defaultValue="false"
android:order="7"/>
<Preference
android:key="@string/pref_storage"
android:title="@string/maps_storage"
app:icon="@drawable/ic_sd_card"
app:singleLineTitle="false"
android:summary="@string/maps_storage_summary"
android:order="8"/>
<SwitchPreferenceCompat
android:key="@string/pref_enable_logging"
android:title="@string/enable_logging"
app:icon="@drawable/ic_browse_activity"
app:singleLineTitle="false"
android:summary="@string/enable_logging_warning_message"
android:defaultValue="false"
@@ -80,6 +87,7 @@
android:key="@string/pref_use_mobile_data"
android:title="@string/mobile_data"
app:singleLineTitle="false"
app:icon="@drawable/ic_network_manage"
android:summary="@string/mobile_data_description"
android:entries="@array/mobile_data_options"
android:entryValues="@array/mobile_data_options_values"
@@ -91,6 +99,7 @@
android:summary="@string/power_managment_description"
android:entries="@array/power_management_scheme"
android:entryValues="@array/power_management_scheme_values"
app:icon="@drawable/ic_eco"
android:order="15"/>
<SwitchPreferenceCompat
android:key="@string/pref_keep_screen_on"
@@ -102,6 +111,7 @@
<SwitchPreferenceCompat
android:key="@string/pref_show_on_lock_screen"
android:title="@string/enable_show_on_lock_screen"
app:icon="@drawable/ic_mobile_lock_portrait"
app:singleLineTitle="false"
android:summary="@string/enable_show_on_lock_screen_description"
android:defaultValue="true"
@@ -109,6 +119,7 @@
<Preference
android:key="@string/pref_map_locale"
android:title="@string/change_map_locale"
app:icon="@drawable/ic_translate"
app:singleLineTitle="false"
android:persistent="false"
android:order="18"/>
@@ -116,6 +127,7 @@
android:key="@string/pref_backup"
android:title="@string/pref_backup_title"
android:summary="@string/pref_backup_summary"
app:icon="@drawable/ic_settings_backup_restore"
app:singleLineTitle="false"
android:persistent="false"
android:order="19"/>
@@ -128,12 +140,14 @@
<ListPreference
android:key="@string/pref_map_style"
android:title="@string/pref_map_style_title"
app:icon="@drawable/ic_dark_mode"
app:singleLineTitle="false"
android:entries="@array/map_style"
android:order="1"/>
<SwitchPreferenceCompat
android:key="@string/pref_3d"
android:title="@string/pref_map_3d_title"
app:icon="@drawable/ic_three_d_rotation"
app:singleLineTitle="false"
android:order="2"/>
<SwitchPreferenceCompat
@@ -146,12 +160,14 @@
android:title="@string/pref_tts_enable_title"
app:singleLineTitle="false"
android:persistent="false"
app:icon="@drawable/ic_record_voice_over"
android:order="4">
</Preference>
<PreferenceScreen
android:key="@string/prefs_routing"
android:order="5"
android:title="@string/driving_options_title">
android:title="@string/driving_options_title"
app:icon="@drawable/ic_instant_mix">
<intent
android:targetClass="app.organicmaps.settings.DrivingOptionsActivity"
android:targetPackage="@string/app_id">
@@ -167,12 +183,14 @@
android:key="@string/pref_play_services"
android:title="@string/google_play_services"
app:singleLineTitle="false"
app:icon="@drawable/ic_wifi_find"
android:summary="@string/pref_use_google_play"
android:defaultValue="true"
android:order="1"/>
<SwitchPreferenceCompat
android:key="@string/pref_search_history"
android:title="@string/search_history_title"
app:icon="@drawable/ic_search_recent_st"
app:singleLineTitle="false"
android:defaultValue="true"
android:order="2"/>

View File

@@ -5,45 +5,54 @@
<SwitchPreferenceCompat
android:key="@string/pref_tts_enabled"
android:title="@string/pref_tts_enable_title" />
android:title="@string/pref_tts_enable_title"
app:iconSpaceReserved="false"/>
<SwitchPreferenceCompat
android:key="@string/pref_tts_street_names"
android:title="@string/pref_tts_street_names_title"
app:isPreferenceVisible="false"
android:summary="@string/pref_tts_street_names_description"
android:defaultValue="false" />
android:defaultValue="false"
app:iconSpaceReserved="false"/>
<ListPreference
android:key="@string/pref_tts_language"
app:isPreferenceVisible="false"
android:title="@string/pref_tts_language_title" />
android:title="@string/pref_tts_language_title"
app:iconSpaceReserved="false"/>
<SeekBarPreference
android:key="@string/pref_tts_volume"
app:isPreferenceVisible="false"
android:title="@string/volume" />
android:title="@string/volume"
app:iconSpaceReserved="false"/>
<Preference
android:key="@string/pref_tts_test_voice"
app:isPreferenceVisible="false"
android:title="@string/pref_tts_test_voice_title" />
android:title="@string/pref_tts_test_voice_title"
app:iconSpaceReserved="false"/>
<Preference
android:key="@string/pref_tts_open_system_settings"
android:title="@string/pref_tts_open_system_settings" />
android:title="@string/pref_tts_open_system_settings"
app:iconSpaceReserved="false"/>
<Preference
android:enabled="false"
android:key="@string/pref_tts_info"
android:persistent="false"
android:selectable="false"
android:summary="@string/prefs_languages_information" />
android:summary="@string/prefs_languages_information"
app:iconSpaceReserved="false" />
<Preference
android:enabled="true"
android:key="@string/pref_tts_info_link"
android:persistent="false"
android:selectable="true"
android:summary="@string/prefs_languages_information_off_link" />
android:summary="@string/prefs_languages_information_off_link"
app:iconSpaceReserved="false" />
<ListPreference
android:key="@string/pref_tts_speed_cameras"
android:title="@string/speedcams_alert_title"
app:singleLineTitle="false"
android:entries="@array/speed_cameras"
android:entryValues="@array/speed_cameras_values"
android:defaultValue="@string/auto_enum_value" />
android:defaultValue="@string/auto_enum_value"
app:iconSpaceReserved="false" />
</androidx.preference.PreferenceScreen>

View File

@@ -0,0 +1,72 @@
package app.organicmaps.widget.placepage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Locale;
public class OpenStateTextFormatterTest
{
private static final String OPENS_AT = "Opens at %s";
private static final String CLOSES_AT = "Closes at %s";
private static final String OPENS_DAY_AT = "Opens %1$s at %2$s";
private static final String CLOSES_DAY_AT = "Closes %1$s at %2$s";
@Test
public void formatHoursMinutes_24h()
{
assertEquals("09:00", OpenStateTextFormatter.formatHoursMinutes(9, 0, true));
assertEquals("18:05", OpenStateTextFormatter.formatHoursMinutes(18, 5, true));
}
@Test
public void formatHoursMinutes_12h()
{
assertEquals("9:00 AM", OpenStateTextFormatter.formatHoursMinutes(9, 0, false));
assertEquals("6:05 PM", OpenStateTextFormatter.formatHoursMinutes(18, 5, false));
assertEquals("12:00 PM", OpenStateTextFormatter.formatHoursMinutes(12, 0, false));
assertEquals("12:00 AM", OpenStateTextFormatter.formatHoursMinutes(0, 0, false));
}
@Test
public void buildAtLabel_today_open_close()
{
String open = OpenStateTextFormatter.buildAtLabel(true, true, "Sat", "09:00",
OPENS_AT, CLOSES_AT, OPENS_DAY_AT, CLOSES_DAY_AT);
String close = OpenStateTextFormatter.buildAtLabel(false, true, "Sat", "18:00",
OPENS_AT, CLOSES_AT, OPENS_DAY_AT, CLOSES_DAY_AT);
assertEquals("Opens at 09:00", open);
assertEquals("Closes at 18:00", close);
}
@Test
public void buildAtLabel_other_day()
{
String open = OpenStateTextFormatter.buildAtLabel(true, false, "Sat", "09:00",
OPENS_AT, CLOSES_AT, OPENS_DAY_AT, CLOSES_DAY_AT);
String close = OpenStateTextFormatter.buildAtLabel(false, false, "Tue", "18:00",
OPENS_AT, CLOSES_AT, OPENS_DAY_AT, CLOSES_DAY_AT);
assertEquals("Opens Sat at 09:00", open);
assertEquals("Closes Tue at 18:00", close);
}
@Test
public void isSameLocalDate_and_dayShort_helpers()
{
ZonedDateTime a = ZonedDateTime.of(2025, 3, 1, 10, 0, 0, 0, ZoneId.of("Europe/Paris"));
ZonedDateTime b = ZonedDateTime.of(2025, 3, 1, 22, 0, 0, 0, ZoneId.of("Europe/Paris"));
ZonedDateTime c = a.plusDays(1);
assertTrue(OpenStateTextFormatter.isSameLocalDate(a, b));
assertFalse(OpenStateTextFormatter.isSameLocalDate(a, c));
String day = OpenStateTextFormatter.dayShort(c, Locale.US);
// March 2, 2025 is a Sunday; "Sun" in US locale
assertEquals("Sun", day);
}
}

View File

@@ -277,6 +277,12 @@ JNIEXPORT jboolean JNICALL Java_app_organicmaps_sdk_editor_Editor_nativeIsNameEd
return g_editableMapObject.IsNameEditable();
}
JNIEXPORT jboolean JNICALL Java_app_organicmaps_sdk_editor_Editor_nativeCanMarkPlaceAsDisused(JNIEnv * env,
jclass clazz)
{
return g_editableMapObject.CanMarkPlaceAsDisused();
}
JNIEXPORT jboolean JNICALL Java_app_organicmaps_sdk_editor_Editor_nativeIsPointType(JNIEnv * env, jclass clazz)
{
return g_editableMapObject.IsPointType();
@@ -434,6 +440,11 @@ JNIEXPORT void JNICALL Java_app_organicmaps_sdk_editor_Editor_nativeRollbackMapO
g_framework->NativeFramework()->RollBackChanges(g_editableMapObject.GetID());
}
JNIEXPORT void JNICALL Java_app_organicmaps_sdk_editor_Editor_nativeMarkPlaceAsDisused(JNIEnv * env, jclass clazz)
{
g_framework->NativeFramework()->MarkPlaceAsDisused(g_editableMapObject);
}
JNIEXPORT jobjectArray JNICALL Java_app_organicmaps_sdk_editor_Editor_nativeGetAllCreatableFeatureTypes(JNIEnv * env,
jclass clazz,
jstring jLang)

View File

@@ -99,6 +99,7 @@ public final class Editor
public static native boolean nativeIsAddressEditable();
public static native boolean nativeIsNameEditable();
public static native boolean nativeCanMarkPlaceAsDisused();
public static native boolean nativeIsPointType();
public static native boolean nativeIsBuilding();
@@ -164,6 +165,7 @@ public final class Editor
public static native void nativeCreateNote(String text);
public static native void nativePlaceDoesNotExist(@NonNull String comment);
public static native void nativeRollbackMapObject();
public static native void nativeMarkPlaceAsDisused();
public static native void nativeCreateStandaloneNote(double lat, double lon, String text);
/**

View File

@@ -337,7 +337,9 @@
<string name="type.cuisine.vegetarian">Vegetarisch</string>
<string name="type.cuisine.vietnamese">Vietnamesisch</string>
<string name="type.emergency">Notfall</string>
<string name="type.emergency.access_point">Rettungspunkt</string>
<string name="type.emergency.assembly_point">Notfall-Sammelpunkt</string>
<string name="type.emergency.life_ring">Rettungsring</string>
<string name="type.emergency.defibrillator">Defibrillator</string>
<string name="type.emergency.fire_hydrant">Hydrant</string>
<string name="type.emergency.phone">Notruftelefon</string>

View File

@@ -371,7 +371,9 @@
<string name="type.cuisine.vegetarian">Vegetarian</string>
<string name="type.cuisine.vietnamese">Vietnamese</string>
<string name="type.emergency">Emergency</string>
<string name="type.emergency.access_point">Emergency Rescue Point</string>
<string name="type.emergency.assembly_point">Emergency Assembly Point</string>
<string name="type.emergency.life_ring">Lifebuoy</string>
<string name="type.emergency.defibrillator">Defibrillator</string>
<string name="type.emergency.fire_hydrant">Fire Hydrant</string>
<string name="type.emergency.phone">Emergency Phone</string>

View File

@@ -397,7 +397,9 @@
"amenity-payment_terminal": "Bezahlterminal",
"amenity-public_bath": "Öffentliches Bad",
"amenity-shower": "Dusche",
"emergency-access_point": "4Rettungspunkt|Notfallpunkt|Notfall-Rettungspunkt|Notfall-Treffpunkt",
"emergency-assembly_point": "Notfall-Sammelpunkt",
"emergency-life_ring": "4Rettungsring",
"emergency-defibrillator": "4Defibrillator",
"emergency-fire_hydrant": "4Hydrant",
"emergency-lifeguard": "Notfall-Rettungsschwimmer|Rettungsschwimmer",

View File

@@ -427,7 +427,9 @@
"amenity-payment_centre": "Payment Centre",
"amenity-public_bath": "Public Bath",
"amenity-shower": "Shower",
"emergency-access_point": "5Emergency Rescue Point|Emergency Location|Emergency Marker|Emergency Access Point",
"emergency-assembly_point": "Emergency Assembly Point",
"emergency-life_ring": "5Lifebuoy|6Life Ring|life-ring|lifering|flotation device|floatation device",
"emergency-defibrillator": "4Defibrillator|AED",
"emergency-fire_hydrant": "4Fire Hydrant|Fire Plug",
"emergency-lifeguard": "Lifeguard|Lifesaver",

View File

@@ -24,3 +24,4 @@ China_Guangdong Hong Kong
US_Guam Guam
Macedonia North Macedonia
Czech Republic Czechia
Myanmar Burma
1 Samoa American Samoa
24 US_Guam Guam
25 Macedonia North Macedonia
26 Czech Republic Czechia
27 Myanmar Burma

View File

@@ -169,7 +169,7 @@ highway|residential|bridge;[highway=residential][bridge?];;name;int_name;81;
# railway|rail|service|bridge;[railway=rail][service?][service!=spur][bridge?];...
railway|rail|bridge;[railway=rail][bridge?][dont=match];;name;int_name;82;
deprecated:boundary|administrative|10:04.2024;[boundary=administrative][admin_level=10];x;name;int_name;83;
deprecated:boundary|administrative|6:04.2024;[boundary=administrative][admin_level=6];x;name;int_name;84;
emergency|access_point;[emergency=access_point];;name;;84;
highway|secondary|bridge;[highway=secondary][bridge?];;name;int_name;85;
highway|tertiary|bridge;[highway=tertiary][bridge?];;name;int_name;86;
barrier|bollard;87;
@@ -225,7 +225,7 @@ place|suburb;128;
landuse|allotments;129;
landuse|forest|coniferous;[landuse=forest][wood=coniferous],[landuse=forest][leaf_type=coniferous],[natural=wood][wood=coniferous],[natural=wood][leaf_type=coniferous];;name;int_name;130;
landuse|forest|mixed;[landuse=forest][wood=mixed],[landuse=forest][leaf_type=mixed],[landuse=forest][leaf_cycle=mixed],[natural=wood][wood=mixed],[natural=wood][leaf_type=mixed],[natural=wood][leaf_cycle=mixed];;name;int_name;131;
deprecated:natural|wood|mixed:01.2020;[natural=wood][wood=mixed],[natural=wood][leaf_type=mixed],[natural=wood][leaf_cycle=mixed];x;name;int_name;132;landuse|forest|mixed
emergency|life_ring;132;
sport|tennis;133;
# ~730k usages.
landuse|vineyard;134;
Can't render this file because it contains an unexpected character in line 7 and column 16.

View File

@@ -77,6 +77,8 @@ vending=water : vending=drinks
vending=milk : vending=drinks
vending=bread : vending=food
highway=emergency_access_point : emergency=access_point
building=entrance : entrance=yes
ice_road=yes : highway=ice_road

View File

@@ -242,6 +242,7 @@
@neutral_label: #51585E;
@healthcare_label: #983E44;
@public_transport_label: #2F6499;
@emergency_label: #247F52;
/* 6.4 Road labels */

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1 @@
<svg height="12" viewBox="0 0 12 12" width="12" xmlns="http://www.w3.org/2000/svg"><path d="m6 0c3.23839694 0 5.8775718 2.56557489 5.9958615 5.77506174l.0041385.22493826c0 3.23839694-2.56557489 5.8775718-5.77506174 5.9958615l-.22493826.0041385c-3.3137085 0-6-2.6862915-6-6s2.6862915-6 6-6zm-3 6h-2.4c0 2.98233765 2.41766235 5.4 5.4 5.4v-2.4c-1.65685425 0-3-1.34314575-3-3zm3-2.4c-1.3254834 0-2.4 1.0745166-2.4 2.4s1.0745166 2.4 2.4 2.4 2.4-1.0745166 2.4-2.4-1.0745166-2.4-2.4-2.4zm0-3v2.4c1.59768088 0 2.90366088 1.24891996 2.99490731 2.82372721l.00509269.17627279h2.4c0-2.98233765-2.41766235-5.4-5.4-5.4z" fill="#983E44" opacity=".6"/></svg>

After

Width:  |  Height:  |  Size: 642 B

View File

@@ -927,7 +927,9 @@ node|z16-[man_made=observatory],
node|z17-[amenity=fire_station],
node|z18-[amenity=internet_cafe],
node|z18-[emergency=defibrillator],
node|z18-[emergency=access_point],
node|z18-[emergency=assembly_point],
node|z19-[emergency=life_ring],
node|z18-[emergency=lifeguard],
node|z18-[amenity=toilets],
node|z18-[amenity=atm],
@@ -1525,9 +1527,15 @@ node|z19-[emergency=fire_hydrant],
node|z18-[emergency=defibrillator],
{icon-image: defibrillator-m.svg;font-size: 11;}
node|z18-[emergency=access_point],
{icon-image: access_point-m.svg;font-size: 11;}
node|z18-[emergency=assembly_point],
{icon-image: assembly_point-m.svg;font-size: 11;}
node|z19-[emergency=life_ring],
{icon-image: lifebuoy-m.svg; font-size: 11;}
node|z17-[emergency=lifeguard],
{icon-image: lifeguard-m.svg; font-size: 11;}

View File

@@ -34,6 +34,7 @@ node[shop=wholesale],
node[craft=photographer],
node[craft=tailor],
node[craft=winery],
{text-color: @shop_label}
node[amenity=bar],
@@ -157,3 +158,6 @@ node[healthcare],
node[amenity=bicycle_rental],
node[amenity=taxi],
{text-color: @public_transport_label;text-halo-radius: 0.1;text-halo-opacity: 0.7;text-halo-color: @label_halo_light;}
node[emergency=access_point],
{text-color: @emergency_label;text-halo-radius: 0.1;text-halo-opacity: 0.7;text-halo-color: @label_halo_light;}

View File

@@ -2360,6 +2360,7 @@ amenity-bench # icon z18- (also has captio
amenity-bench-backless # icon z18- (also has caption(optional) z19-)
amenity-lounger # icon z18- (also has caption(optional) z19-)
amenity-waste_disposal # icon z18- (also has caption(optional) z18-)
emergency-access_point # icon z18- (also has caption(optional) z18-)
emergency-assembly_point # icon z18- (also has caption(optional) z18-)
emergency-defibrillator # icon z18- (also has caption(optional) z18-)
emergency-phone # icon z17-
@@ -2368,6 +2369,7 @@ man_made-telescope # icon z18- (also has captio
amenity-waste_basket # icon z18- (also has caption(optional) z19-)
emergency-fire_hydrant # icon z19- (also has caption(optional) z19-)
emergency-life_ring # icon z19- (also has caption(optional) z19-)
power-substation # icon z17- (also has caption(optional) z18-, area z13-)
=== -9990
@@ -2391,9 +2393,11 @@ amenity-loading_dock # icon z18- (also has captio
# amenity-vending_machine-sweets # caption(optional) z18- (also has icon z18-)
# amenity-waste_basket # caption(optional) z19- (also has icon z18-)
# amenity-waste_disposal # caption(optional) z18- (also has icon z18-)
# emergency-access_point # caption(optional) z18- (also has icon z18-)
# emergency-assembly_point # caption(optional) z18- (also has icon z18-)
# emergency-defibrillator # caption(optional) z18- (also has icon z18-)
# emergency-fire_hydrant # caption(optional) z19- (also has icon z19-)
# emergency-life_ring # caption(optional) z19- (also has icon z19-)
# entrance # caption(optional) z19- (also has icon z17-)
entrance-exit # icon z17- (also has caption(optional) z19-)
# entrance-exit # caption(optional) z19- (also has icon z17-)

View File

@@ -240,6 +240,7 @@
@neutral_label: #494F54;
@healthcare_label: #A6454B;
@public_transport_label: #234B73;
@emergency_label: #247F52;
/* 6.4 Road labels */

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1 @@
<svg height="12" viewBox="0 0 12 12" width="12" xmlns="http://www.w3.org/2000/svg"><path d="m6 0c3.23839694 0 5.8775718 2.56557489 5.9958615 5.77506174l.0041385.22493826c0 3.23839694-2.56557489 5.8775718-5.77506174 5.9958615l-.22493826.0041385c-3.3137085 0-6-2.6862915-6-6s2.6862915-6 6-6zm-3 6h-2.4c0 2.98233765 2.41766235 5.4 5.4 5.4v-2.4c-1.65685425 0-3-1.34314575-3-3zm3-2.4c-1.3254834 0-2.4 1.0745166-2.4 2.4s1.0745166 2.4 2.4 2.4 2.4-1.0745166 2.4-2.4-1.0745166-2.4-2.4-2.4zm0-3v2.4c1.59768088 0 2.90366088 1.24891996 2.99490731 2.82372721l.00509269.17627279h2.4c0-2.98233765-2.41766235-5.4-5.4-5.4z" fill="#d85961"/></svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@@ -2366,6 +2366,7 @@ amenity-bench # icon z18- (also has captio
amenity-bench-backless # icon z18- (also has caption(optional) z19-)
amenity-lounger # icon z18- (also has caption(optional) z19-)
amenity-waste_disposal # icon z18- (also has caption(optional) z18-)
emergency-access_point # icon z18- (also has caption(optional) z18-)
emergency-assembly_point # icon z18- (also has caption(optional) z18-)
emergency-defibrillator # icon z18- (also has caption(optional) z18-)
emergency-phone # icon z17-
@@ -2374,6 +2375,7 @@ man_made-telescope # icon z18- (also has captio
amenity-waste_basket # icon z18- (also has caption(optional) z19-)
emergency-fire_hydrant # icon z19- (also has caption(optional) z19-)
emergency-life_ring # icon z19- (also has caption(optional) z19-)
power-substation # icon z17- (also has caption(optional) z18-, area z13-)
=== -9990
@@ -2397,9 +2399,11 @@ amenity-loading_dock # icon z18- (also has captio
# amenity-vending_machine-sweets # caption(optional) z18- (also has icon z18-)
# amenity-waste_basket # caption(optional) z19- (also has icon z18-)
# amenity-waste_disposal # caption(optional) z18- (also has icon z18-)
# emergency-access_point # caption(optional) z18- (also has icon z18-)
# emergency-assembly_point # caption(optional) z18- (also has icon z18-)
# emergency-defibrillator # caption(optional) z18- (also has icon z18-)
# emergency-fire_hydrant # caption(optional) z19- (also has icon z19-)
# emergency-life_ring # caption(optional) z19- (also has icon z19-)
# entrance # caption(optional) z19- (also has icon z17-)
entrance-exit # icon z17- (also has caption(optional) z19-)
# entrance-exit # caption(optional) z19- (also has icon z17-)

View File

@@ -180,6 +180,7 @@
@neutral_label: #51585E;
@healthcare_label: #983E44;
@public_transport_label: #2F6499;
@emergency_label: #247F52;
/* 6.4 Road labels */

View File

@@ -151,6 +151,7 @@
@neutral_label: #494F54;
@healthcare_label: #A6454B;
@public_transport_label: #234B73;
@emergency_label: #247F52;
/* ROADS LABELS */
@shield_text: #000000;

View File

@@ -70,3 +70,7 @@ All the following commands require an app restart:
- `?all-types`: Shows all internal types in place page
- `?no-all-types`: Disables showing all internal types in place page
## Search
- `?wiki`: Search results for all elements with Wikipedia links

View File

@@ -147,8 +147,6 @@ set(SRC
osm_o5m_source.hpp
osm_source.cpp
osm_xml_source.hpp
panoramax_collector.cpp
panoramax_collector.hpp
place_processor.cpp
place_processor.hpp
platform_helpers.cpp

View File

@@ -225,6 +225,11 @@ bool FeatureBuilder::PreSerialize()
});
m_params.name = std::move(nameWithRef);
}
else if (ftypes::IsEmergencyAccessPointChecker::Instance()(types))
{
m_params.name.Clear();
m_params.name.AddString(StringUtf8Multilang::kDefaultCode, m_params.ref);
}
m_params.ref.clear();
}

View File

@@ -10,7 +10,6 @@
#include "generator/mini_roundabout_transformer.hpp"
#include "generator/node_mixer.hpp"
#include "generator/osm2type.hpp"
#include "generator/panoramax_collector.hpp"
#include "generator/region_meta.hpp"
#include "routing/speed_camera_prohibition.hpp"
@@ -69,9 +68,6 @@ void CountryFinalProcessor::Process()
if (!m_isolinesPath.empty())
AddIsolines();
LOG(LINFO, ("Enriching with Panoramax data..."));
EnrichPanoramax();
// DropProhibitedSpeedCameras();
LOG(LINFO, ("Processing building parts..."));
ProcessBuildingParts();
@@ -297,50 +293,6 @@ void CountryFinalProcessor::AddAddresses()
LOG(LINFO, ("Total addresses:", totalStats));
}
void CountryFinalProcessor::EnrichPanoramax()
{
if (m_panoramaxFilename.empty() || !Platform::IsFileExistsByFullPath(m_panoramaxFilename))
{
LOG(LINFO, ("Panoramax data not available, skipping enrichment"));
return;
}
LOG(LINFO, ("Enriching roads with Panoramax imagery data from:", m_panoramaxFilename));
// Load Panoramax imagery data
PanoramaxCollector collector;
if (!collector.LoadImageryData(m_panoramaxFilename, ""))
{
LOG(LWARNING, ("Failed to load Panoramax data"));
return;
}
// Enrich roads in each MWM
ForEachMwmTmp(m_temporaryMwmPath, [&](auto const & name, auto const & path)
{
if (!IsCountry(name))
return;
LOG(LINFO, ("Enriching Panoramax for:", name));
std::vector<FeatureBuilder> features;
ForEachFeatureRawFormat<serialization_policy::MaxAccuracy>(
path, [&](FeatureBuilder && fb, uint64_t) {
collector.EnrichRoad(fb);
features.emplace_back(std::move(fb));
});
// Rewrite the file with enriched features
FeatureBuilderWriter<serialization_policy::MaxAccuracy> writer(path, FileWriter::Op::OP_WRITE_TRUNCATE);
for (auto & fb : features)
writer.Write(fb);
LOG(LINFO, (name, "done"));
});
LOG(LINFO, ("Panoramax enrichment complete:", DebugPrint(collector.GetStats())));
}
void CountryFinalProcessor::ProcessCoastline()
{
/// @todo We can remove MinSize at all.

View File

@@ -26,7 +26,6 @@ public:
void SetAddressesDir(std::string const & dir) { m_addressPath = dir; }
void SetCityBoundariesFiles(std::string const & collectorFile) { m_boundariesCollectorFile = collectorFile; }
void SetPanoramaxFile(std::string const & filename) { m_panoramaxFilename = filename; }
// FinalProcessorIntermediateMwmInterface overrides:
void Process() override;
@@ -40,7 +39,6 @@ private:
void AddFakeNodes();
void AddIsolines();
void AddAddresses();
void EnrichPanoramax();
void DropProhibitedSpeedCameras();
// void Finish();
@@ -56,7 +54,6 @@ private:
std::string m_fakeNodesFilename;
std::string m_miniRoundaboutsFilename;
std::string m_addrInterpolFilename;
std::string m_panoramaxFilename;
std::string m_hierarchySrcFilename;

View File

@@ -42,9 +42,6 @@ struct GenerateInfo
// External folders with additional preprocessed data (isolines, addresses).
std::string m_isolinesDir, m_addressesDir;
// Panoramax imagery data file
std::string m_panoramaxFilename;
// Current generated file name if --output option is defined.
std::string m_fileName;

View File

@@ -107,7 +107,6 @@ DEFINE_string(nodes_list_path, "",
DEFINE_bool(generate_isolines_info, false, "Generate the isolines info section");
DEFINE_string(isolines_path, "", "Path to isolines directory. If set, adds isolines linear features.");
DEFINE_string(addresses_path, "", "Path to addresses directory. If set, adds addr:interpolation features.");
DEFINE_string(panoramax_file, "", "Path to Panoramax imagery coords file. If set, enriches roads with street-level imagery availability.");
// Routing.
DEFINE_bool(make_routing_index, false, "Make sections with the routing information.");
@@ -244,7 +243,6 @@ MAIN_WITH_ERROR_HANDLING([](int argc, char ** argv)
genInfo.m_complexHierarchyFilename = FLAGS_complex_hierarchy_data;
genInfo.m_isolinesDir = FLAGS_isolines_path;
genInfo.m_addressesDir = FLAGS_addresses_path;
genInfo.m_panoramaxFilename = FLAGS_panoramax_file;
// Use merged style.
GetStyleReader().SetCurrentStyle(MapStyleMerged);

View File

@@ -1,157 +0,0 @@
#include "generator/panoramax_collector.hpp"
#include "indexer/feature_meta.hpp"
#include "indexer/ftypes_matcher.hpp"
#include "geometry/mercator.hpp"
#include "base/assert.hpp"
#include "base/logging.hpp"
#include <cstdint>
#include <fstream>
#include <sstream>
namespace generator
{
PanoramaxCollector::PanoramaxCollector() = default;
bool PanoramaxCollector::LoadImageryData(std::string const & coordsFilePath, std::string const & /* tilesDir */)
{
LOG(LINFO, ("Loading Panoramax imagery from:", coordsFilePath));
std::ifstream file(coordsFilePath, std::ios::binary);
if (!file.is_open())
{
LOG(LWARNING, ("Failed to open Panoramax coords file:", coordsFilePath));
return false;
}
// Read header
uint32_t version;
uint64_t count;
file.read(reinterpret_cast<char*>(&version), sizeof(version));
file.read(reinterpret_cast<char*>(&count), sizeof(count));
if (version != 1)
{
LOG(LWARNING, ("Unsupported Panoramax coords file version:", version));
return false;
}
LOG(LINFO, ("Loading", count, "Panoramax imagery points..."));
// Read coordinates and build spatial index
std::vector<ImageryPoint> points;
points.reserve(count);
for (uint64_t i = 0; i < count; ++i)
{
double lat, lon;
file.read(reinterpret_cast<char*>(&lat), sizeof(lat));
file.read(reinterpret_cast<char*>(&lon), sizeof(lon));
// Convert to Mercator coordinates
m2::PointD mercPt = mercator::FromLatLon(lat, lon);
points.emplace_back(mercPt);
if ((i + 1) % 500000 == 0)
LOG(LINFO, ("Loaded", i + 1, "/", count, "points"));
}
if (!file)
{
LOG(LWARNING, ("Error reading Panoramax coords file"));
return false;
}
file.close();
LOG(LINFO, ("Building spatial index for", points.size(), "points..."));
// Build spatial index
for (auto const & pt : points)
m_imageryTree.Add(pt);
m_stats.m_totalImageryPoints = static_cast<uint32_t>(points.size());
LOG(LINFO, ("Panoramax data loaded:", m_stats.m_totalImageryPoints, "imagery points"));
return true;
}
bool PanoramaxCollector::EnrichRoad(feature::FeatureBuilder & fb)
{
// Only process roads
static auto const & isRoad = ftypes::IsWayChecker::Instance();
if (!isRoad(fb.GetTypes()))
return false;
m_stats.m_totalRoads++;
// Check if road already has Panoramax flag
if (fb.GetMetadata().Has(feature::Metadata::FMD_PANORAMAX))
return false;
// Get road geometry
std::vector<m2::PointD> roadPoints;
fb.ForEachPoint([&roadPoints](m2::PointD const & pt) {
roadPoints.push_back(pt);
});
if (roadPoints.empty())
return false;
// Calculate road bounding box
m2::RectD roadBBox;
for (auto const & pt : roadPoints)
roadBBox.Add(pt);
// Expand bounding box by threshold distance in Mercator units
auto const center = roadBBox.Center();
auto const inflateRect = mercator::RectByCenterXYAndSizeInMeters(center, kDistanceThresholdM);
roadBBox.Inflate(inflateRect.SizeX() / 2.0, inflateRect.SizeY() / 2.0);
// Query imagery tree for nearby points
bool hasImagery = false;
m_imageryTree.ForEachInRect(roadBBox, [&](ImageryPoint const & imgPt) {
if (hasImagery)
return; // Already found, early exit
// Check distance to each road point
for (auto const & roadPt : roadPoints)
{
double const distM = mercator::DistanceOnEarth(roadPt, imgPt.m_point);
if (distM <= kDistanceThresholdM)
{
hasImagery = true;
return;
}
}
});
if (hasImagery)
{
fb.GetMetadata().Set(feature::Metadata::FMD_PANORAMAX, "yes");
m_stats.m_roadsWithImagery++;
return true;
}
return false;
}
std::string DebugPrint(PanoramaxCollector::Stats const & s)
{
std::ostringstream out;
out << "PanoramaxCollector::Stats {"
<< " totalRoads: " << s.m_totalRoads
<< ", roadsWithImagery: " << s.m_roadsWithImagery
<< " (" << (s.m_totalRoads > 0 ? (s.m_roadsWithImagery * 100.0 / s.m_totalRoads) : 0) << "%)"
<< ", totalImageryPoints: " << s.m_totalImageryPoints
<< " }";
return out.str();
}
} // namespace generator

View File

@@ -1,72 +0,0 @@
#pragma once
#include "generator/feature_builder.hpp"
#include "geometry/point2d.hpp"
#include "geometry/tree4d.hpp"
#include "base/geo_object_id.hpp"
#include <string>
#include <vector>
namespace generator
{
/// Enriches road features with Panoramax street-level imagery availability flag.
/// Loads Panoramax imagery coordinates from tile index and marks roads within threshold distance.
class PanoramaxCollector
{
public:
// Distance threshold for matching roads to imagery (meters)
static double constexpr kDistanceThresholdM = 20.0;
/// ImageryPoint represents a single point where street-level imagery exists
struct ImageryPoint
{
m2::PointD m_point; // Mercator coordinates
ImageryPoint() = default;
explicit ImageryPoint(m2::PointD const & pt) : m_point(pt) {}
m2::RectD GetLimitRect() const { return m2::RectD(m_point, m_point); }
};
/// Statistics for debugging/logging
struct Stats
{
uint32_t m_totalRoads = 0;
uint32_t m_roadsWithImagery = 0;
uint32_t m_totalImageryPoints = 0;
uint32_t m_tilesProcessed = 0;
friend std::string DebugPrint(Stats const & s);
};
PanoramaxCollector();
/// Load Panoramax imagery data from tile directory
/// @param tileIndexPath Path to tile_index.json
/// @param tilesDir Directory containing .mvt tiles
/// @return true if data loaded successfully
bool LoadImageryData(std::string const & tileIndexPath, std::string const & tilesDir);
/// Check if a road has nearby imagery and enrich it
/// @param fb FeatureBuilder for a road segment
/// @return true if imagery was found within threshold
bool EnrichRoad(feature::FeatureBuilder & fb);
Stats const & GetStats() const { return m_stats; }
private:
/// Spatial index of imagery points
m4::Tree<ImageryPoint> m_imageryTree;
/// Statistics
Stats m_stats;
/// Decode an MVT tile and extract imagery coordinates
std::vector<m2::PointD> DecodeMVTTile(std::string const & tilePath,
int tileX, int tileY, int zoom);
};
} // namespace generator

View File

@@ -182,7 +182,6 @@ RawGenerator::FinalProcessorPtr RawGenerator::CreateCountryFinalProcessor(Affili
auto finalProcessor = std::make_shared<CountryFinalProcessor>(affiliations, m_genInfo.m_tmpDir, m_threadsCount);
finalProcessor->SetIsolinesDir(m_genInfo.m_isolinesDir);
finalProcessor->SetAddressesDir(m_genInfo.m_addressesDir);
finalProcessor->SetPanoramaxFile(m_genInfo.m_panoramaxFilename);
finalProcessor->SetMiniRoundabouts(m_genInfo.GetIntermediateFileName(MINI_ROUNDABOUTS_FILENAME));
finalProcessor->SetAddrInterpolation(m_genInfo.GetIntermediateFileName(ADDR_INTERPOL_FILENAME));
if (addAds)

View File

@@ -359,7 +359,9 @@
"type.cuisine.vegetarian" = "Vegetarisch";
"type.cuisine.vietnamese" = "Vietnamesisch";
"type.emergency" = "Notfall";
"type.emergency.access_point" = "Rettungspunkt";
"type.emergency.assembly_point" = "Notfall-Sammelpunkt";
"type.emergency.life_ring" = "Rettungsring";
"type.emergency.defibrillator" = "Defibrillator";
"type.emergency.fire_hydrant" = "Hydrant";
"type.emergency.phone" = "Notruftelefon";

View File

@@ -367,7 +367,9 @@
"type.cuisine.vegetarian" = "Vegetarian";
"type.cuisine.vietnamese" = "Vietnamese";
"type.emergency" = "Emergency";
"type.emergency.access_point" = "Emergency Rescue Point";
"type.emergency.assembly_point" = "Emergency Assembly Point";
"type.emergency.life_ring" = "Lifebuoy";
"type.emergency.defibrillator" = "Defibrillator";
"type.emergency.fire_hydrant" = "Fire Hydrant";
"type.emergency.phone" = "Emergency Phone";

View File

@@ -386,7 +386,9 @@
"type.cuisine.vegetarian" = "Vegetarian";
"type.cuisine.vietnamese" = "Vietnamese";
"type.emergency" = "Emergency";
"type.emergency.access_point" = "Emergency Rescue Point";
"type.emergency.assembly_point" = "Emergency Assembly Point";
"type.emergency.life_ring" = "Lifebuoy";
"type.emergency.defibrillator" = "Defibrillator";
"type.emergency.fire_hydrant" = "Fire Hydrant";
"type.emergency.phone" = "Emergency Phone";

View File

@@ -33,6 +33,7 @@ set(SRC
xml_feature.cpp
xml_feature.hpp
yes_no_unknown.hpp
keys_to_remove.hpp
)
omim_add_library(${PROJECT_NAME} ${SRC})

View File

@@ -57,6 +57,9 @@ std::string GetTypeForFeature(editor::XMLFeature const & node)
}
}
if (node.HasTag("disused:shop") || node.HasTag("disused:amenity"))
return "vacant business";
if (node.HasTag("addr:housenumber") || node.HasTag("addr:street") || node.HasTag("addr:postcode"))
return "address";

View File

@@ -0,0 +1,135 @@
#pragma once
#include <string_view>
// Keys that should be removed when a place in OSM is replaced, copied from
// https://github.com/mnalis/StreetComplete-taginfo-categorize/blob/master/sc_to_remove.txt
// Changes to the list: don't remove 'wheelchair' and addresses in the 'contact:' style
inline constexpr std::string_view kKeysToRemove[] = {
"shop_?[1-9]?(:.*)?", "craft_?[1-9]?", "amenity_?[1-9]?", "club_?[1-9]?", "old_amenity",
"old_shop", "information", "leisure", "office_?[1-9]?", "tourism",
// popular shop=* / craft=* subkeys
"marketplace", "household", "swimming_pool", "laundry", "golf", "sports", "ice_cream",
"scooter", "music", "retail", "yes", "ticket", "newsagent", "lighting", "truck", "car_repair",
"car_parts", "video", "fuel", "farm", "car", "tractor", "hgv", "ski", "sculptor",
"hearing_aids", "surf", "photo", "boat", "gas", "kitchen", "anime", "builder", "hairdresser",
"security", "bakery", "bakehouse", "fishing", "doors", "kiosk", "market", "bathroom", "lamps",
"vacant", "insurance(:.*)?", "caravan", "gift", "bicycle", "bicycle_rental", "insulation",
"communication", "mall", "model", "empty", "wood", "hunting", "motorcycle", "trailer",
"camera", "water", "fireplace", "outdoor", "blacksmith", "electronics", "fan", "piercing",
"stationery", "sensory_friendly(:.*)?", "street_vendor", "sells(:.*)?", "safety_equipment",
// obsoleted information
"(demolished|abandoned|disused)(:(?!bui).+)?", "was:.*", "not:.*", "damage", "created_by",
"check_date", "opening_date", "last_checked", "checked_exists:date", "pharmacy_survey",
"old_ref", "update", "import_uuid", "review", "fixme:atp",
// classifications / links to external databases
"fhrs:.*", "old_fhrs:.*", "fvst:.*", "ncat", "nat_ref", "gnis:.*", "winkelnummer",
"type:FR:FINESS", "type:FR:APE", "kvl_hro:amenity", "ref:DK:cvr(:.*)?", "certifications?",
"transiscope", "opendata:type", "local_ref", "official_ref",
// names and identifications
"name_?[1-9]?(:.*)?", ".*_name_?[1-9]?(:.*)?", "noname", "branch(:.*)?", "brand(:.*)?",
"not:brand(:.*)?", "network(:.*)?", "operator(:.*)?", "operator_type", "ref", "ref:vatin",
"designation", "SEP:CLAVEESC", "identifier", "ref:FR:SIRET", "ref:FR:SIREN", "ref:FR:NAF",
"(old_)?ref:FR:prix-carburants",
// contacts
"contact_person", "phone(:.*)?", "phone_?[1-9]?", "emergency:phone", "emergency_telephone_code",
"contact:(?!housenumber$|street$|place$|postcode$|city$|country$|pobox$|unit$).*",
"mobile", "fax", "facebook", "instagram", "twitter", "youtube", "telegram", "tiktok", "email",
"website_?[1-9]?(:.*)?", "app:.*", "ownership",
"url", "url:official", "source_ref:url", "owner",
// payments
"payment(:.*)?", "payment_multi_fee", "currency(:.*)?", "cash_withdrawal(:.*)?", "fee",
"charge", "charge_fee", "money_transfer", "donation:compensation", "paypoint",
// generic shop/craft attributes
"seasonal", "time", "opening_hours(:.*)?", "check_(in|out)", "wifi", "internet",
"internet_access(:.*)?", "second_hand", "self_service", "automated", "license:.*",
"bulk_purchase", ".*:covid19", "language:.*", "baby_feeding", "description(:.*)?",
"description[0-9]", "min_age", "max_age", "supermarket(:.*)?", "social_facility(:.*)?",
"functional", "trade", "wholesale", "sale", "smoking(:outside)?", "zero_waste", "origin",
"attraction", "strapline", "dog", "showroom", "toilets?(:.*)?", "sanitary_dump_station",
"changing_table(:.*)?", "blind", "company(:.*)?", "stroller", "walk-in",
"webshop", "operational_status.*", "status", "drive_through", "surveillance(:.*)?",
"outdoor_seating", "indoor_seating", "colour", "access_simple", "floor", "product_category",
"guide", "source_url", "category", "kids_area", "kids_area:indoor", "resort", "since", "state",
"temporary", "self_checkout", "audio_loop", "related_law(:.*)?", "official_status(:.*)?",
// food and drink details
"bar", "cafe", "coffee", "microroasting", "microbrewery", "brewery", "real_ale", "taproom",
"training", "distillery", "drink(:.*)?", "cocktails", "alcohol", "wine([:_].*)?",
"happy_hours", "diet:.*", "cuisine", "ethnic", "tasting", "breakfast", "lunch", "organic",
"produced_on_site", "restaurant", "food", "pastry", "pastry_shop", "product", "produce",
"chocolate", "fair_trade", "butcher", "reservation(:.*)?", "takeaway(:.*)?", "delivery(:.*)?",
"caterer", "real_fire", "flour_fortified", "highchair", "fast_food", "pub", "snack",
"confectionery", "drinking_water:refill",
// related to repair shops/crafts
"service(:.*)?", "motorcycle:.*", "repair", ".*:repair", "electronics_repair(:.*)?",
"workshop",
// shop=hairdresser, shop=clothes
"unisex", "male", "female", "gender", "gender_simple", "lgbtq(:.*)?", "gay", "female:signed",
"male:signed",
// healthcare
"healthcare(:.*)?", "healthcare_.*", "health", "health_.*", "speciality", "medical_.*",
"emergency_ward", "facility(:.*)?", "activities", "healthcare_facility(:.*)?",
"laboratory(:.*)?", "blood(:.*)?", "blood_components", "infection(:.*)?", "disease(:.*)?",
"covid19(:.*)?", "COVID_.*", "CovidVaccineCenterId", "coronaquarantine", "hospital(:.*)?",
"hospital_type_id", "emergency_room", "sample_collection(:.*)?", "bed_count", "capacity:beds",
"part_time_beds", "personnel:count", "staff_count(:.*)?", "admin_staff", "doctors",
"doctors_num", "nurses_num", "counselling_type", "testing_centres", "toilets_number",
"urgent_care", "vaccination", "clinic", "hospital", "pharmacy", "alternative", "laboratory",
"sample_collection", "provided_for(:.*)?", "social_facility_for", "ambulance", "ward",
"HSE_(code|hgid|hgroup|region)", "collection_centre", "design", "AUTORIZATIE", "reg_id",
"post_addr", "scope", "ESTADO", "NIVSOCIO", "NO", "EMP_EST", "COD_HAB", "CLA_PERS", "CLA_PRES",
"snis_code:.*", "hfac_bed", "hfac_type", "nature", "moph_code", "IJSN:.*", "massgis:id",
"OGD-Stmk:.*", "paho:.*", "panchayath", "pbf_contract", "pcode", "pe:minsa:.*", "who:.*",
"pharmacy:category", "tactile_paving", "HF_(ID|TYPE|N_EN)", "RoadConn", "bin", "hiv(:.*)?",
// accommodation & layout
"rooms", "stars", "accommodation", "beds", "capacity(:persons)?", "laundry_service",
"guest_house",
// amenity=place_of_worship
"deanery", "subject:(wikidata|wikipedia|wikimedia_commons)", "church", "church:type",
// schools
"capacity:(pupils|teachers)", "grades", "population:pupils(:.*)?",
"school:(FR|gender|trust|type|type_idn|group:type)", "primary",
// clubs
"animal(_breeding|_training)?", "billiards(:.*)?", "board_game", "sport_1", "sport:boating",
"boat:type", "canoe(_rental|:service)?", "kayak(_rental|:service)?",
"sailboat(_rental|:service)?", "horse_riding", "rugby", "boules", "callsign", "card_games",
"car_service", "catastro:ref", "chess(:.*)?", "children", "climbing(:.*)?", "club(:.*)?",
"communication(:amateur_radio.*)", "community_centre:for", "dffr:network", "dormitory",
"education_for:ages", "electrified", "esperanto", "events_venue", "family", "federation",
"free_flying(:.*)?", "freemasonry(:.*)?", "free_refill", "gaelic_games(:.*)?", "membership",
"military_service", "model_aerodrome(:.*)?", "mode_of_organisation(:.*)?", "snowmobile",
"social_centre(:for)?", "source_dat", "tennis", "old_website", "organisation", "school_type",
"scout(:type)?", "fraternity", "live_music", "lockable", "playground(:theme)?", "nudism",
"music_genre", "length", "fire_station:type:FR", "cadet", "observatory:type", "tower:type",
"zoo", "shooting", "commons", "groomer", "group_only", "hazard", "identity", "interaction",
"logo", "maxheight", "provides", "regional", "scale", "site", "plots", "allotments",
"local_food", "monitoring:pedestrian", "recording:automated", "yacht", "background_music",
"url:spaceapi", "openfire", "fraternity(:.*)?",
// misc specific attributes
"clothes", "shoes", "tailor", "beauty", "tobacco", "carpenter", "furniture", "lottery",
"sport", "dispensing", "tailor:.*", "gambling", "material", "raw_material", "stonemason",
"studio", "scuba_diving(:.*)?", "polling_station", "collector", "books", "agrarian",
"musical_instrument", "massage", "parts", "post_office(:.*)?", "religion", "denomination",
"rental", ".*:rental", "tickets:.*", "public_transport", "goods_supply", "pet", "appliance",
"artwork_type", "charity", "company", "crop", "dry_cleaning", "factory", "feature",
"air_conditioning", "atm", "vending", "vending_machine", "recycling_type", "museum",
"license_classes", "dance:.*", "isced:level", "school", "preschool", "university",
"research_institution", "research", "member_of", "topic", "townhall:type", "parish", "police",
"government", "thw:(lv|rb|ltg)", "office", "administration", "administrative", "association",
"transport", "utility", "consulting", "Commercial", "commercial", "private", "taxi",
"admin_level", "official_status", "target", "liaison", "diplomatic(:.*)?", "embassy",
"consulate", "aeroway", "department", "faculty", "aerospace:product", "boundary", "population",
"diocese", "depot", "cargo", "function", "game", "party", "political_party.*",
"telecom(munication)?", "service_times", "kitchen:facilities", "it:(type|sales)",
"cannabis:cbd", "bath:type", "bath:(open_air|sand_bath)", "animal_boarding", "animal_shelter",
"mattress", "screen", "monitoring:weather", "public", "theatre", "culture", "library",
"cooperative(:.*)?", "winery", "curtain", "lawyer(:.*)?", "local_authority(:.*)?", "equipment",
"hackerspace",
"camp_site", "camping", "bbq", "static_caravans", "emergency(:.*)?", "evacuation_cent(er|re)",
"education", "engineering", "forestry", "foundation", "lawyer", "logistics", "military",
"community_centre", "bank", "operational", "users_(PLWD|boy|elderly|female|girl|men)",
"Comments?", "comments?", "entrance:(width|step_count|kerb:height)", "fenced", "motor_vehicle",
"shelter",
};

View File

@@ -668,7 +668,7 @@ void Editor::UploadChanges(string const & oauthToken, ChangesetTags tags, Finish
{}
// Add tags to XMLFeature
UpdateXMLFeatureTags(feature, journal);
UpdateXMLFeatureTags(feature, journal, changeset);
// Upload XMLFeature to OSM
LOG(LDEBUG, ("CREATE Feature (newEditor)", feature));
@@ -686,7 +686,7 @@ void Editor::UploadChanges(string const & oauthToken, ChangesetTags tags, Finish
XMLFeature feature = GetMatchingFeatureFromOSM(changeset, fti.m_object);
// Update tags of XMLFeature
UpdateXMLFeatureTags(feature, journal);
UpdateXMLFeatureTags(feature, journal, changeset);
// Upload XMLFeature to OSM
LOG(LDEBUG, ("MODIFIED Feature (newEditor)", feature));
@@ -1321,7 +1321,8 @@ bool Editor::IsFeatureUploadedImpl(FeaturesContainer const & features, MwmId con
return info && info->m_uploadStatus == kUploaded;
}
void Editor::UpdateXMLFeatureTags(editor::XMLFeature & feature, std::list<JournalEntry> const & journal)
void Editor::UpdateXMLFeatureTags(editor::XMLFeature & feature, std::list<JournalEntry> const & journal,
ChangesetWrapper & changeset)
{
for (JournalEntry const & entry : journal)
{
@@ -1335,6 +1336,13 @@ void Editor::UpdateXMLFeatureTags(editor::XMLFeature & feature, std::list<Journa
}
case JournalEntryType::ObjectCreated: break;
case JournalEntryType::LegacyObject: ASSERT_FAIL(("Legacy Objects can not be edited with the new editor")); break;
case JournalEntryType::BusinessReplacement:
{
BusinessReplacementData const & businessReplacementData = std::get<BusinessReplacementData>(entry.data);
feature.OSMBusinessReplacement(businessReplacementData.old_type, businessReplacementData.new_type);
changeset.AddChangesetTag("info:place_marked_as_disused", "yes");
break;
}
}
}
}

View File

@@ -1,5 +1,6 @@
#pragma once
#include "editor/changeset_wrapper.hpp"
#include "editor/config_loader.hpp"
#include "editor/editor_config.hpp"
#include "editor/editor_notes.hpp"
@@ -241,7 +242,8 @@ private:
static bool IsFeatureUploadedImpl(FeaturesContainer const & features, MwmId const & mwmId, uint32_t index);
void UpdateXMLFeatureTags(editor::XMLFeature & feature, std::list<JournalEntry> const & journal);
static void UpdateXMLFeatureTags(editor::XMLFeature & feature, std::list<JournalEntry> const & journal,
ChangesetWrapper & changeset);
/// Deleted, edited and created features.
base::AtomicSharedPtr<FeaturesContainer> m_features;

View File

@@ -1,4 +1,5 @@
#include "editor/xml_feature.hpp"
#include "editor/keys_to_remove.hpp"
#include "indexer/classificator.hpp"
#include "indexer/editable_map_object.hpp"
@@ -15,6 +16,7 @@
#include "base/timer.hpp"
#include <array>
#include <boost/regex.hpp>
#include <sstream>
#include <string>
@@ -502,6 +504,29 @@ osm::EditJournal XMLFeature::GetEditJournal() const
entry.data = legacyObjData;
break;
}
case osm::JournalEntryType::BusinessReplacement:
{
osm::BusinessReplacementData businessReplacementData;
// Old Feature Type
std::string old_strType = getAttribute(xmlData, "old_type");
if (old_strType.empty())
MYTHROW(editor::InvalidJournalEntry, ("Old Feature type is empty"));
businessReplacementData.old_type = classif().GetTypeByReadableObjectName(old_strType);
if (businessReplacementData.old_type == IndexAndTypeMapping::INVALID_TYPE)
MYTHROW(editor::InvalidJournalEntry, ("Invalid old Feature Type:", old_strType));
// New Feature Type
std::string new_strType = getAttribute(xmlData, "new_type");
if (new_strType.empty())
MYTHROW(editor::InvalidJournalEntry, ("New Feature type is empty"));
businessReplacementData.new_type = classif().GetTypeByReadableObjectName(new_strType);
if (businessReplacementData.new_type == IndexAndTypeMapping::INVALID_TYPE)
MYTHROW(editor::InvalidJournalEntry, ("Invalid new Feature Type:", new_strType));
entry.data = businessReplacementData;
break;
}
}
if (isHistory)
journal.AddJournalHistoryEntry(entry);
@@ -572,6 +597,14 @@ void XMLFeature::SetEditJournal(osm::EditJournal const & journal)
xmlData.append_attribute("version") = legacyObjData.version.data();
break;
}
case osm::JournalEntryType::BusinessReplacement:
{
osm::BusinessReplacementData const & businessReplacementData =
std::get<osm::BusinessReplacementData>(entry.data);
xmlData.append_attribute("old_type") = classif().GetReadableObjectName(businessReplacementData.old_type).data();
xmlData.append_attribute("new_type") = classif().GetReadableObjectName(businessReplacementData.new_type).data();
break;
}
}
}
};
@@ -675,6 +708,52 @@ void XMLFeature::UpdateOSMTag(std::string_view key, std::string_view value)
}
}
void XMLFeature::OSMBusinessReplacement(uint32_t old_type, uint32_t new_type)
{
std::string name = GetTagValue("name");
// Remove OSM tags using the list from keys_to_remove.hpp
static boost::regex const regex([]
{
std::string regexPattern;
for (auto const & key : kKeysToRemove)
{
if (!regexPattern.empty())
regexPattern.append("|");
regexPattern.append(key);
}
return regexPattern;
}());
ForEachTag([this](std::string_view key, std::string_view /*value*/)
{
if (boost::regex_match(key.begin(), key.end(), regex))
RemoveTag(key);
});
if (classif().GetReadableObjectName(new_type) == "disusedbusiness")
{
// Mark as 'disused'
string const strOldType = classif().GetReadableObjectName(old_type);
strings::SimpleTokenizer iter(strOldType, "-");
string_view const key = *iter;
if (++iter)
SetTagValue("disused:" + std::string(key), *iter);
else
SetTagValue("disused:" + std::string(key), "yes");
if (!name.empty())
SetTagValue("old_name", name);
}
else
{
// Add new category tag
ASSERT_FAIL("Only marking places as 'disused' is implemented yet. Wrong new_type: " +
classif().GetReadableObjectName(new_type));
}
}
string XMLFeature::GetAttribute(string const & key) const
{
return GetRootNode().attribute(key.data()).value();

View File

@@ -187,6 +187,8 @@ public:
/// Wrapper for SetTagValue and RemoveTag, avoids duplication for similar alternative osm tags
void UpdateOSMTag(std::string_view key, std::string_view value);
/// Replace an old business with a new business
void OSMBusinessReplacement(uint32_t old_type, uint32_t new_type);
std::string GetAttribute(std::string const & key) const;
void SetAttribute(std::string const & key, std::string const & value);

View File

@@ -41,6 +41,14 @@ void EditJournal::MarkAsCreated(uint32_t type, feature::GeomType geomType, m2::P
AddJournalEntry({JournalEntryType::ObjectCreated, time(nullptr), osm::ObjCreateData{type, geomType, mercator}});
}
void EditJournal::AddBusinessReplacement(uint32_t old_type, uint32_t new_type)
{
LOG(LDEBUG, ("Business of type ", classif().GetReadableObjectName(old_type), " was replaced by a ",
classif().GetReadableObjectName(new_type)));
AddJournalEntry(
{JournalEntryType::BusinessReplacement, time(nullptr), osm::BusinessReplacementData{old_type, new_type}});
}
void EditJournal::AddJournalEntry(JournalEntry entry)
{
m_journal.push_back(std::move(entry));
@@ -103,6 +111,15 @@ std::string EditJournal::ToString(osm::JournalEntry const & journalEntry)
LegacyObjData const & legacyObjData = std::get<LegacyObjData>(journalEntry.data);
return ToString(journalEntry.journalEntryType).append(": version=\"").append(legacyObjData.version).append("\"");
}
case osm::JournalEntryType::BusinessReplacement:
{
BusinessReplacementData const & businessReplacementData = std::get<BusinessReplacementData>(journalEntry.data);
return ToString(journalEntry.journalEntryType)
.append(": Category changed from ")
.append(classif().GetReadableObjectName(businessReplacementData.old_type))
.append(" to ")
.append(classif().GetReadableObjectName(businessReplacementData.new_type));
}
default: UNREACHABLE();
}
}
@@ -114,6 +131,7 @@ std::string EditJournal::ToString(osm::JournalEntryType journalEntryType)
case osm::JournalEntryType::TagModification: return "TagModification";
case osm::JournalEntryType::ObjectCreated: return "ObjectCreated";
case osm::JournalEntryType::LegacyObject: return "LegacyObject";
case osm::JournalEntryType::BusinessReplacement: return "BusinessReplacement";
default: UNREACHABLE();
}
}
@@ -126,6 +144,8 @@ std::optional<JournalEntryType> EditJournal::TypeFromString(std::string const &
return JournalEntryType::ObjectCreated;
else if (entryType == "LegacyObject")
return JournalEntryType::LegacyObject;
else if (entryType == "BusinessReplacement")
return JournalEntryType::BusinessReplacement;
else
return {};
}

View File

@@ -16,6 +16,7 @@ enum class JournalEntryType
TagModification,
ObjectCreated,
LegacyObject, // object without full journal history, used for transition to new editor
BusinessReplacement,
// Possible future values: ObjectDeleted, ObjectDisused, ObjectNotDisused, LocationChanged, FeatureTypeChanged
};
@@ -38,11 +39,17 @@ struct LegacyObjData
std::string version;
};
struct BusinessReplacementData
{
uint32_t old_type;
uint32_t new_type;
};
struct JournalEntry
{
JournalEntryType journalEntryType = JournalEntryType::TagModification;
time_t timestamp;
std::variant<TagModData, ObjCreateData, LegacyObjData> data;
std::variant<TagModData, ObjCreateData, LegacyObjData, BusinessReplacementData> data;
};
/// Used to determine whether existing OSM object should be updated or new one created
@@ -69,6 +76,9 @@ public:
/// Log object creation in the journal
void MarkAsCreated(uint32_t type, feature::GeomType geomType, m2::PointD mercator);
/// Log business replacement in the journal
void AddBusinessReplacement(uint32_t old_type, uint32_t new_type);
void AddJournalEntry(JournalEntry entry);
/// Clear Journal and move content to journalHistory, used after upload to OSM

View File

@@ -87,6 +87,32 @@ vector<MapObject::MetadataID> EditableMapObject::GetEditableProperties() const
return props;
}
bool EditableMapObject::CanMarkPlaceAsDisused() const
{
if (GetEditingLifecycle() == EditingLifecycle::CREATED)
return false;
auto types = GetTypes();
types.SortBySpec();
uint32_t mainType = *types.begin();
std::string mainTypeStr = classif().GetReadableObjectName(mainType);
constexpr string_view typePrefixes[] = {
"shop",
"amenity-restaurant",
"amenity-fast_food",
"amenity-cafe",
"amenity-pub",
"amenity-bar",
};
for (auto const & typePrefix : typePrefixes)
if (mainTypeStr.starts_with(typePrefix))
return true;
return false;
}
NamesDataSource EditableMapObject::GetNamesDataSource()
{
auto const mwmInfo = GetID().m_mwmId.GetInfo();
@@ -656,6 +682,16 @@ void EditableMapObject::MarkAsCreated(uint32_t type, feature::GeomType geomType,
m_journal.MarkAsCreated(type, geomType, std::move(mercator));
}
void EditableMapObject::MarkAsDisused()
{
auto types = GetTypes();
types.SortBySpec();
uint32_t old_type = *types.begin();
uint32_t new_type = classif().GetTypeByReadableObjectName("disusedbusiness");
ApplyBusinessReplacement(new_type);
m_journal.AddBusinessReplacement(old_type, new_type);
}
void EditableMapObject::ClearJournal()
{
m_journal.Clear();
@@ -673,7 +709,7 @@ void EditableMapObject::ApplyEditsFromJournal(EditJournal const & editJournal)
void EditableMapObject::ApplyJournalEntry(JournalEntry const & entry)
{
LOG(LDEBUG, ("Applying Journal Entry: ", osm::EditJournal::ToString(entry)));
// Todo
switch (entry.journalEntryType)
{
case JournalEntryType::TagModification:
@@ -760,6 +796,12 @@ void EditableMapObject::ApplyJournalEntry(JournalEntry const & entry)
ASSERT_FAIL(("Legacy Objects can not be loaded from Journal"));
break;
}
case JournalEntryType::BusinessReplacement:
{
BusinessReplacementData const & businessReplacementData = std::get<BusinessReplacementData>(entry.data);
ApplyBusinessReplacement(businessReplacementData.new_type);
break;
}
}
}
@@ -859,6 +901,47 @@ void EditableMapObject::LogDiffInJournal(EditableMapObject const & unedited_emo)
}
}
void EditableMapObject::ApplyBusinessReplacement(uint32_t new_type)
{
// Types
feature::TypesHolder new_feature_types;
new_feature_types.Add(new_type); // Update feature type
std::string wheelchairType = feature::GetReadableWheelchairType(m_types);
if (!wheelchairType.empty())
new_feature_types.SafeAdd(classif().GetTypeByReadableObjectName(wheelchairType));
std::vector<uint32_t> const buildingTypes = ftypes::IsBuildingChecker::Instance().GetTypes();
for (uint32_t const & type : buildingTypes)
if (m_types.Has(type))
new_feature_types.SafeAdd(type);
m_types = new_feature_types;
// Names
m_name.Clear();
// Metadata
feature::Metadata new_metadata;
constexpr MetadataID metadataToKeep[] = {
MetadataID::FMD_WHEELCHAIR,
MetadataID::FMD_POSTCODE,
MetadataID::FMD_LEVEL,
MetadataID::FMD_ELE,
MetadataID::FMD_HEIGHT,
MetadataID::FMD_MIN_HEIGHT,
MetadataID::FMD_BUILDING_LEVELS,
MetadataID::FMD_BUILDING_MIN_LEVEL
};
for (MetadataID const & metadataID : metadataToKeep)
new_metadata.Set(metadataID, std::string(m_metadata.Get(metadataID)));
m_metadata = new_metadata;
}
bool AreObjectsEqualIgnoringStreet(EditableMapObject const & lhs, EditableMapObject const & rhs)
{
feature::TypesHolder const & lhsTypes = lhs.GetTypes();

View File

@@ -78,6 +78,8 @@ public:
/// All store/load/valid operations will be via MetadataEntryIFace interface instead of switch-case.
std::vector<MetadataID> GetEditableProperties() const;
bool CanMarkPlaceAsDisused() const;
/// See comment for NamesDataSource class.
NamesDataSource GetNamesDataSource();
LocalizedStreet const & GetStreet() const;
@@ -141,11 +143,16 @@ public:
void SetJournal(EditJournal && editJournal);
EditingLifecycle GetEditingLifecycle() const;
void MarkAsCreated(uint32_t type, feature::GeomType geomType, m2::PointD mercator);
void MarkAsDisused();
void ClearJournal();
void ApplyEditsFromJournal(EditJournal const & journal);
void ApplyJournalEntry(JournalEntry const & entry);
void LogDiffInJournal(EditableMapObject const & unedited_emo);
private:
void ApplyBusinessReplacement(uint32_t new_type);
public:
/// Check whether langCode can be used as default name.
static bool CanUseAsDefaultName(int8_t const langCode, std::vector<int8_t> const & nativeMwmLanguages);

View File

@@ -854,6 +854,12 @@ IsPlatformChecker::IsPlatformChecker()
m_types.push_back(c.GetTypeByPath({"public_transport", "platform"}));
}
IsEmergencyAccessPointChecker::IsEmergencyAccessPointChecker()
{
Classificator const & c = classif();
m_types.push_back(c.GetTypeByPath({"emergency", "access_point"}));
}
IsAddressInterpolChecker::IsAddressInterpolChecker() : BaseChecker(1 /* level */)
{
Classificator const & c = classif();

View File

@@ -629,6 +629,14 @@ public:
DECLARE_CHECKER_INSTANCE(IsPlatformChecker);
};
class IsEmergencyAccessPointChecker : public BaseChecker
{
IsEmergencyAccessPointChecker();
public:
DECLARE_CHECKER_INSTANCE(IsEmergencyAccessPointChecker);
};
class IsAddressInterpolChecker : public BaseChecker
{
IsAddressInterpolChecker();

View File

@@ -3091,6 +3091,13 @@ void Framework::DeleteFeature(FeatureID const & fid)
UpdatePlacePageInfoForCurrentSelection();
}
void Framework::MarkPlaceAsDisused(osm::EditableMapObject emo)
{
emo.MarkAsDisused();
osm::Editor::Instance().SaveEditedFeature(emo);
UpdatePlacePageInfoForCurrentSelection();
}
osm::NewFeatureCategories Framework::GetEditorCategories() const
{
return osm::Editor::Instance().GetNewFeatureCategories();

View File

@@ -755,6 +755,7 @@ public:
bool GetEditableMapObject(FeatureID const & fid, osm::EditableMapObject & emo) const;
osm::Editor::SaveResult SaveEditedMapObject(osm::EditableMapObject emo);
void DeleteFeature(FeatureID const & fid);
void MarkPlaceAsDisused(osm::EditableMapObject emo);
osm::NewFeatureCategories GetEditorCategories() const;
bool RollBackChanges(FeatureID const & fid);
void CreateNote(osm::MapObject const & mapObject, osm::Editor::NoteProblemType const type, std::string const & note);

View File

@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
271DC2172EC60C0C00442D94 /* keys_to_remove.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 271DC2162EC60C0C00442D94 /* keys_to_remove.hpp */; };
340C20DE1C3E4DFD00111D22 /* osm_auth.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 340C20DC1C3E4DFD00111D22 /* osm_auth.cpp */; };
340C20DF1C3E4DFD00111D22 /* osm_auth.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 340C20DD1C3E4DFD00111D22 /* osm_auth.hpp */; };
340DC8291C4E71E500EAA2CC /* changeset_wrapper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 340DC8271C4E71E500EAA2CC /* changeset_wrapper.cpp */; };
@@ -75,6 +76,7 @@
/* Begin PBXFileReference section */
270C9C212E16AABF00ABA688 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; name = module.modulemap; path = ../../libs/editor/module.modulemap; sourceTree = SOURCE_ROOT; };
271DC2162EC60C0C00442D94 /* keys_to_remove.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = keys_to_remove.hpp; sourceTree = "<group>"; };
340C20DC1C3E4DFD00111D22 /* osm_auth.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = osm_auth.cpp; sourceTree = "<group>"; };
340C20DD1C3E4DFD00111D22 /* osm_auth.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = osm_auth.hpp; sourceTree = "<group>"; };
340DC8271C4E71E500EAA2CC /* changeset_wrapper.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = changeset_wrapper.cpp; sourceTree = "<group>"; };
@@ -240,6 +242,7 @@
6715560420BEC331002BA3B4 /* edits_migration.hpp */,
3D052486200F62ED00F24998 /* feature_matcher.cpp */,
3D052485200F62ED00F24998 /* feature_matcher.hpp */,
271DC2162EC60C0C00442D94 /* keys_to_remove.hpp */,
6715565220BF0F86002BA3B4 /* new_feature_categories.cpp */,
6715565320BF0F87002BA3B4 /* new_feature_categories.hpp */,
341138741C15AE42002E3B3E /* opening_hours_ui.cpp */,
@@ -337,6 +340,7 @@
6715565520BF0F87002BA3B4 /* new_feature_categories.hpp in Headers */,
6715560820BEC332002BA3B4 /* edits_migration.hpp in Headers */,
3D052487200F62EE00F24998 /* feature_matcher.hpp in Headers */,
271DC2172EC60C0C00442D94 /* keys_to_remove.hpp in Headers */,
3411387B1C15AE42002E3B3E /* ui2oh.hpp in Headers */,
341138791C15AE42002E3B3E /* opening_hours_ui.hpp in Headers */,
);