Commit 634cb7f8 authored by YONG-LIN SU's avatar YONG-LIN SU

1. 修改介面

2. 新增sony xperia 10 II 相容
parent fb151307
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="SERIAL_NUMBER" />
<value value="577125220043" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2022-08-29T05:19:03.595558100Z" />
</component>
</project>
\ No newline at end of file
......@@ -25,6 +25,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- android 11 管理所有檔案權限 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
......@@ -35,6 +36,7 @@
android:roundIcon="@mipmap/ic_icon_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:requestLegacyExternalStorage="true"
tools:targetApi="31">
<activity
android:name=".view.T02PlateAndSpaceConfirmActivity"
......
......@@ -723,6 +723,7 @@ public class Common {
}
public static Bitmap decodeFile2ScaledPlateBitmap(String path, BoxInfo boxInfo, Activity activity){
// TODO: 2022/8/30 調整壓縮方案
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap bitmap = BitmapFactory.decodeFile(path, options);
......@@ -733,15 +734,23 @@ public class Common {
size = options.outWidth / DM.widthPixels;
}
// 固定縮放比例
size = 4;
//size = 1;
options.inSampleSize = size;
options.inJustDecodeBounds = false;
bitmap = BitmapFactory.decodeFile(path, options);
// 確保方向正確
bitmap = turnPictureDegree(bitmap,path);
// int x0 = (int)(boxInfo.x0/size);
// int y0 = (int)(boxInfo.y0/size);
// int w = (int)(boxInfo.getRect().width()/size);
// int h = (int)(boxInfo.getRect().height()/size);
int[] maxXYWH = boxInfo.getMaxXYWH();
int x0 = maxXYWH[0]/size;
int y0 = maxXYWH[1]/size;
int w = maxXYWH[2]/size;
int h = maxXYWH[3]/size;
int x0 = (int)(boxInfo.x0/size);
int y0 = (int)(boxInfo.y0/size);
int w = (int)(boxInfo.getRect().width()/size);
int h = (int)(boxInfo.getRect().height()/size);
Bitmap plateBitmap = null;
try {
plateBitmap = Bitmap.createBitmap(bitmap, x0, y0, w, h);
......@@ -751,6 +760,29 @@ public class Common {
return plateBitmap;
}
private static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
public static void moveFile(String inputPath, String outputPath) {
InputStream in = null;
......
package ecom.android.newparkapp;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import androidx.recyclerview.widget.RecyclerView;
public class HorizontalLayoutManager extends RecyclerView.LayoutManager {
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
super.onLayoutChildren(recycler, state);
detachAndScrapAttachedViews(recycler);
int itemCount = getItemCount();
if ( itemCount == 0){
return;
}
int offsetX = 0;
for(int i = 0; i < itemCount; i++){
View view = recycler.getViewForPosition(i);
Button button = view.findViewById(R.id.btn_select_space);
button.setPadding(50,0,50,0);
addView(view);
measureChildWithMargins(view, 0, 0);
int viewWidth = getDecoratedMeasuredWidth(view);
int viewHeight = getDecoratedMeasuredHeight(view);
layoutDecorated(view, offsetX, 0, offsetX + viewWidth, viewHeight);
offsetX += viewWidth;
}
}
@Override
public boolean canScrollHorizontally() {
super.canScrollHorizontally();
return true;
}
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
super.scrollHorizontallyBy(dx, recycler, state);
offsetChildrenHorizontal(dx * -1);
return dx;
}
private void fill(int dx, RecyclerView.Recycler recycler){
if (dx > 0){
// 左滑
}else {
// 右滑
}
}
@Override
public void onScrollStateChanged(int state) {
super.onScrollStateChanged(state);
if (state == RecyclerView.SCROLL_STATE_IDLE){
}
}
}
package ecom.android.newparkapp.adapter;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
......@@ -52,6 +53,9 @@ public class SelectSpaceAdapter extends ListAdapter<Space, SelectSpaceAdapter.Se
this.dataBinding = dataBinding;
}
void setBinding(Space space, OnItemClickListener onItemClickListener){
if (space.isNearSpace){
dataBinding.btnSelectSpace.setTextColor(Color.RED);
}
dataBinding.btnSelectSpace.setText(space.id);
dataBinding.btnSelectSpace.setOnClickListener(view -> {
onItemClickListener.onSpaceClick(space);
......
......@@ -26,6 +26,15 @@ public interface SpaceDao {
@Query("SELECT * FROM space WHERE latitude>:latitudeSmaller AND latitude < :latitudeBigger AND longitude > :longitudeSmaller AND longitude < :longitudeBigger")
List<Space> getAllByLocation(double latitudeBigger, double latitudeSmaller, double longitudeBigger , double longitudeSmaller);
@Query("SELECT * FROM space WHERE road_id IN (:roadIds) AND id>:spaceId AND id NOT IN (:spaceIds)")
List<Space> getAllByInRoadIdsAndBigThanSpaceIdAndNotInSpaces(String[] roadIds, String spaceId, String[] spaceIds);
@Query("SELECT * FROM space WHERE road_id IN (:roadIds) AND id<:spaceId AND id NOT IN (:spaceIds)")
List<Space> getAllByInRoadIdsAndSmallThanSpaceIdAndNotInSpaces(String[] roadIds, String spaceId, String[] spaceIds);
@Query("SELECT * FROM space WHERE road_id IN (:roadIds) AND id NOT IN (:spaceIds)")
List<Space> getAllByInRoadIdsAndNotInSpaceIds(String[] roadIds,String[] spaceIds);
@Query("SELECT * FROM space WHERE id IN (:Ids)")
List<Space> loadAllByIds(String [] Ids);
......
......@@ -50,6 +50,37 @@ public class BoxInfo implements Serializable {
return new RectF(x0, y0, x1, y1);
}
public int[] getMaxXYWH(){
int[] xywh = new int[4];
float x0 = this.x0;
float y0 = this.y0;
float x1 = this.x1;
float y1 = this.y1;
if (landmarks != null){
//左上 x y 取小
x0 = Math.min(landmarks[0], x0);
y0 = Math.min(landmarks[1], y0);
//右上 x 取小 y 取大
x1 = Math.max(landmarks[2], x1);
y0 = Math.min(landmarks[3], y0);
// 右下
x1 = Math.max(landmarks[4], x1);
y1 = Math.max(landmarks[5], y1);
// 左下
x0 = Math.min(landmarks[6], x0);
y1 = Math.max(landmarks[7], y1);
}
xywh[0] = (int)x0;
xywh[1] = (int)y0;
xywh[2] = (int)(x1 - x0);
xywh[3] = (int)(y1 - y0);
return xywh;
}
public String getLabel(int modelName) {
return plateLabels[label];
}
......
......@@ -8,6 +8,7 @@ import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Embedded;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
/**
......@@ -36,6 +37,9 @@ public class Space implements Parcelable {
@ColumnInfo(name = "space_status")
public SpaceStatus spaceStatus;
@Ignore
public boolean isNearSpace = false;
public Space(String id, Road road, SpaceType spaceType, SpaceRate spaceRate, float latitude, float longitude, SpaceStatus spaceStatus) {
this.id = id;
this.road = road;
......
package ecom.android.newparkapp.view;
import static ecom.android.newparkapp.Common.PREF;
import static ecom.android.newparkapp.Common.decodeFile2ScaledPlateBitmap;
import android.content.Intent;
import android.graphics.Bitmap;
import android.location.Location;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
......@@ -17,6 +21,7 @@ import androidx.recyclerview.widget.GridLayoutManager;
import java.io.File;
import java.util.List;
import ecom.android.newparkapp.HorizontalLayoutManager;
import ecom.android.newparkapp.R;
import ecom.android.newparkapp.adapter.SelectSpaceAdapter;
import ecom.android.newparkapp.databinding.ActivityT02PlateAndSpaceConfirmBinding;
......@@ -47,6 +52,8 @@ public class T02PlateAndSpaceConfirmActivity extends AppCompatActivity {
private T02SelectSpaceViewModel t02SelectSpaceViewModel;
private ViewModelProvider viewModelProvider;
private GridLayoutManager rvSpaceOptionGridLayoutManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
......@@ -73,11 +80,13 @@ public class T02PlateAndSpaceConfirmActivity extends AppCompatActivity {
infoRepository = new InfoRepository(this.getApplication());
selectSpaceAdapter = new SelectSpaceAdapter(space -> nextStage(space));
dataBinding.rvSpaceOption.setLayoutManager(new GridLayoutManager(this, 3));
//dataBinding.rvSpaceOption.setLayoutManager(new GridLayoutManager(this, 2));
rvSpaceOptionGridLayoutManager = new GridLayoutManager(this, 2);
dataBinding.rvSpaceOption.setLayoutManager(rvSpaceOptionGridLayoutManager);
dataBinding.rvSpaceOption.setAdapter(selectSpaceAdapter);
reNewCandidateSpaceAdapter = new SelectSpaceAdapter(space -> nextStage(space));
dataBinding.rvRenewSpaceOption.setLayoutManager(new GridLayoutManager(this, 3));
dataBinding.rvRenewSpaceOption.setLayoutManager(new GridLayoutManager(this, 2));
dataBinding.rvRenewSpaceOption.setAdapter(reNewCandidateSpaceAdapter);
plateImageInit();
......@@ -151,6 +160,12 @@ public class T02PlateAndSpaceConfirmActivity extends AppCompatActivity {
selectSpaceAdapter.submitList(spaces);
}
});
t02SelectSpaceViewModel.getTop1NearSpacePosition().observe(this, index -> {
if (rvSpaceOptionGridLayoutManager != null){
rvSpaceOptionGridLayoutManager.scrollToPosition(index);
}
});
}
private void btnSelectRoadOnClicked(){
......
......@@ -347,6 +347,8 @@ public class T02StartActivity extends AppCompatActivity {
String plateNumber = bundle.getString("PlateNumber");
Space space = bundle.getParcelable("Space");
// TODO: 2022/8/25 接續新單流程
// 新單
t02StartViewModel.initCurrentCase();
// 更新圖片
t02StartViewModel.setNewCasePhoto(photoFile);
// 更新路段
......
......@@ -184,11 +184,13 @@ public class T01SettingViewModel extends AndroidViewModel {
*/
public boolean checkAndMkdirs() {
SimpleDateFormat dFormat = new SimpleDateFormat("yyyyMMdd", Locale.TAIWAN);
String[] Filename = new String[4];
Filename[0] = Environment.getExternalStorageDirectory() + getApplication().getString(R.string.sysDataPhoto_path) + dFormat.format(new Date());
Filename[1] = Environment.getExternalStorageDirectory() + getApplication().getString(R.string.sysDataUpload_path);
Filename[2] = Environment.getExternalStorageDirectory() + getApplication().getString(R.string.sysData_path) + dFormat.format(new Date());
Filename[3] = Environment.getExternalStorageDirectory() + getApplication().getString(R.string.sysDataDB_path);
String[] Filename = new String[5];
// 主目錄
Filename[0] = Environment.getExternalStorageDirectory() + getApplication().getString(R.string.sysData_path);
Filename[1] = Environment.getExternalStorageDirectory() + getApplication().getString(R.string.sysDataPhoto_path) + dFormat.format(new Date());
Filename[2] = Environment.getExternalStorageDirectory() + getApplication().getString(R.string.sysDataUpload_path);
Filename[3] = Environment.getExternalStorageDirectory() + getApplication().getString(R.string.sysData_path) + dFormat.format(new Date());
Filename[4] = Environment.getExternalStorageDirectory() + getApplication().getString(R.string.sysDataDB_path);
boolean ret = true;
for (String s : Filename) {
......@@ -199,7 +201,6 @@ public class T01SettingViewModel extends AndroidViewModel {
}
}
}
String path = Environment.getExternalStorageDirectory() + getApplication().getString(R.string.sysData_path);
return ret;
}
......
......@@ -11,8 +11,12 @@ import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.MutableLiveData;
import java.lang.invoke.MutableCallSite;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import ecom.android.newparkapp.entity.Case;
import ecom.android.newparkapp.entity.Road;
import ecom.android.newparkapp.entity.Space;
import ecom.android.newparkapp.repository.InfoRepository;
......@@ -22,11 +26,11 @@ public class T02SelectSpaceViewModel extends AndroidViewModel {
private MutableLiveData<Road> currentRoad = new MutableLiveData<>();
private MutableLiveData<List<Space>> currentSpace = new MutableLiveData<>();
private MutableLiveData<Integer> top1NearSpacePosition = new MutableLiveData<>();
public T02SelectSpaceViewModel(@NonNull Application application) {
super(application);
infoRepository = new InfoRepository(application);
}
public void updateCurrentRoad(Road road){
......@@ -40,12 +44,104 @@ public class T02SelectSpaceViewModel extends AndroidViewModel {
}
public LiveData<Road> getCurrentRoad(){return currentRoad;}
public LiveData<List<Space>> getCurrentSpace(){return currentSpace;}
public LiveData<Integer> getTop1NearSpacePosition(){return top1NearSpacePosition;}
public void searchNearSpace(Location location) {
double latitude = location.getLatitude();
double longitude = location.getLongitude();
double precision = 0.0002;// 63m
currentRoad.setValue(null);
currentSpace.setValue(infoRepository.spaceDao.getAllByLocation(latitude + precision, latitude - precision, longitude + precision, longitude - precision));
List<Space> nearSpace = infoRepository.spaceDao.getAllByLocation(latitude + precision, latitude - precision, longitude + precision, longitude - precision);
getMoreNearSpace(nearSpace);
}
private void getMoreNearSpace(List<Space> nearSpace){
List<Space> moreNearSpace = new ArrayList<>();
if (nearSpace.size() > 0) {
Map<String, List<Space>> spaceGroupByRoad = nearSpace.stream().collect(Collectors.groupingBy(s -> s.road.id));
String[] roadIds = spaceGroupByRoad.keySet().toArray(new String[0]);
String[] spaceIds = new String[nearSpace.size() - 1];
Space firstSpace = nearSpace.get(0);
// 取得除了第一筆以外的車格id
for (int i = 1; i <nearSpace.size(); i++){
String spaceId = nearSpace.get(i).id;
spaceIds[i - 1] = spaceId;
}
// 取得所有關聯的車格,並包含第一筆車格
List<Space> relateSpaces = infoRepository.spaceDao.getAllByInRoadIdsAndNotInSpaceIds(roadIds, spaceIds);
// 取得第一筆車格的位置
int index = -1;
for (int i = 0; i < relateSpaces.size(); i ++){
if (relateSpaces.get(i).id.equals(firstSpace.id)){
index = i;
break;
}
}
// 插入前段車格
for (int i = 0; i < index; i++){
moreNearSpace.add(relateSpaces.get(i));
}
// 插入鄰近車格
for (Space space : nearSpace){
space.isNearSpace = true;
moreNearSpace.add(space);
}
// 插入後段車格
for (int i = index + 1; i < relateSpaces.size(); i++){
moreNearSpace.add(relateSpaces.get(i));
}
// 取得鄰近車格 index
currentSpace.setValue(moreNearSpace);
top1NearSpacePosition.setValue(index);
}
}
public void nextSpacePage(){
List<Space> currentSpaceList = getCurrentSpace().getValue();
if (currentSpaceList.size() > 0){
Map<String, List<Space>> spaceGroupByRoad = currentSpaceList.stream().collect(Collectors.groupingBy(s -> s.road.id));
String[] roadIds = spaceGroupByRoad.keySet().toArray(new String[0]);
String[] spaceIds = new String[currentSpaceList.size()];
for (int i = 0; i < spaceIds.length; i++){
spaceIds[i] = currentSpaceList.get(i).id;
}
Space firstSpace = currentSpaceList.get(0);
List<Space> nextSpace = infoRepository.spaceDao.getAllByInRoadIdsAndBigThanSpaceIdAndNotInSpaces(roadIds, firstSpace.id, spaceIds);
if (nextSpace.size() != 0) {
for (int i = 0; i < nextSpace.size(); i++){
if (i > nextSpace.size() - 8){
break;
}
nextSpace.remove(i);
}
currentSpace.setValue(nextSpace);
}
}
}
public void previousSpacePage(){
List<Space> currentSpaceList = getCurrentSpace().getValue();
if (currentSpaceList.size() > 0){
Map<String, List<Space>> spaceGroupByRoad = currentSpaceList.stream().collect(Collectors.groupingBy(s -> s.road.id));
String[] roadIds = spaceGroupByRoad.keySet().toArray(new String[0]);
String[] spaceIds = new String[currentSpaceList.size()];
for (int i = 0; i < spaceIds.length; i++){
spaceIds[i] = currentSpaceList.get(i).id;
}
Space firstSpace = currentSpaceList.get(0);
List<Space> previousSpace = infoRepository.spaceDao.getAllByInRoadIdsAndSmallThanSpaceIdAndNotInSpaces(roadIds, firstSpace.id, spaceIds);
if (previousSpace.size() != 0) {
for (int i = 0; i < previousSpace.size(); i++){
if (i > previousSpace.size() - 8){
break;
}
previousSpace.remove(i);
}
currentSpace.setValue(previousSpace);
}
}
}
}
......@@ -330,7 +330,10 @@ public class T02StartViewModel extends AndroidViewModel {
tempCase.caseStatus = CaseStatus.LIST; //saveBills
// 更新當前list結果
cases.set(caseCursor.getValue(), tempCase);
int index = cases.indexOf(caseCursor.getValue());
if (index != -1){
cases.set(index, tempCase);
}
// 更新當前顯示
currentCase.postValue(tempCase);
......
......@@ -10,12 +10,13 @@
android:paddingStart="10dp"
android:paddingTop="10dp"
android:paddingEnd="10dp"
android:paddingBottom="50dp">
android:paddingBottom="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal">
android:layout_height="0dp"
android:layout_weight="50"
android:orientation="horizontal"
android:gravity="bottom">
<ImageView
android:id="@+id/iv_cropped_plate"
......@@ -30,7 +31,7 @@
<Button
android:id="@+id/btn_input_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0.67"
android:text="確認"
android:textSize="24sp"
......
......@@ -12,7 +12,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0.6"
android:layout_weight="0.42"
android:orientation="vertical">
<TextView
......@@ -34,19 +34,28 @@
android:text="車牌辨識結果"
android:textSize="24sp" />
<ImageView
android:id="@+id/iv_cropped_plate"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:scaleType="fitCenter"
/>
<Button
android:id="@+id/btn_keyin_plate_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="8052-LZ"
android:textSize="24sp" />
android:layout_height="100dp"
android:gravity="center"
android:orientation="horizontal"
android:paddingStart="8dp"
android:paddingEnd="8dp">
<ImageView
android:id="@+id/iv_cropped_plate"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_weight="1"
android:scaleType="fitCenter" />
<Button
android:id="@+id/btn_keyin_plate_number"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="30sp" />
</LinearLayout>
<TextView
android:id="@+id/textView28"
......@@ -89,16 +98,16 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0.12"
android:layout_weight="0.1"
android:gravity="center"
android:orientation="horizontal"
android:paddingEnd="5dp"
tools:ignore="RtlSymmetry">
<TextView
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_weight="0.2"
android:gravity="center|center_vertical"
android:text="格號"
android:textSize="30sp"
......@@ -106,9 +115,9 @@
<Button
android:id="@+id/btn_select_road"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.7"
android:layout_weight="0.8"
android:textSize="30sp" />
<Button
......
......@@ -19,12 +19,12 @@
</data>
<androidx.core.widget.NestedScrollView
android:id="@+id/t02_start_border"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:orientation="vertical"
tools:context=".view.T02StartActivity"
tools:layout_editor_absoluteY="25dp">
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="false"
android:orientation="vertical"
tools:context=".view.T02StartActivity"
tools:layout_editor_absoluteY="25dp">
<LinearLayout
android:layout_width="match_parent"
......@@ -581,8 +581,7 @@ tools:layout_editor_absoluteY="25dp">
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="@{`自動扣款: ` + (t02StartViewModel.currentCase.autoPay ? `是`:`否`)}"
android:textSize="21sp" />
android:text="@{`自動扣款: ` + (t02StartViewModel.currentCase.autoPay ? `是`:`否`)}" android:textSize="21sp" />
<TextView
android:id="@+id/latitude_text"
......
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<Button
android:id="@+id/btn_select_space"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="C876"
android:textSize="20dp">
</Button>
<Button
android:id="@+id/btn_select_space"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="C876"
android:textSize="20dp"></Button>
</layout>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment