Revision 124
org.gvsig.googlemaps/trunk/org.gvsig.googlemaps/org.gvsig.googlemaps.swing/org.gvsig.googlemaps.swing.impl/src/main/java/org/gvsig/googlemaps/swing/impl/DefaultGoogleMapsParametersPanelController.java | ||
---|---|---|
24 | 24 |
|
25 | 25 |
import java.awt.event.ActionEvent; |
26 | 26 |
import java.awt.event.ActionListener; |
27 |
import java.net.URI; |
|
27 |
import java.awt.event.MouseEvent; |
|
28 |
import java.awt.event.MouseListener; |
|
28 | 29 |
import java.net.URISyntaxException; |
29 | 30 |
import java.net.URL; |
30 |
import java.nio.file.attribute.AclEntry.Builder; |
|
31 | 31 |
|
32 | 32 |
import javax.swing.ComboBoxModel; |
33 | 33 |
import javax.swing.DefaultComboBoxModel; |
... | ... | |
47 | 47 |
import org.gvsig.googlemaps.swing.api.GoogleMapsParametersPanel; |
48 | 48 |
import org.gvsig.tools.ToolsLocator; |
49 | 49 |
import org.gvsig.tools.i18n.I18nManager; |
50 |
|
|
50 |
import org.gvsig.tools.swing.api.ActionListenerSupport; |
|
51 |
import org.gvsig.tools.swing.api.ToolsSwingLocator; |
|
51 | 52 |
import org.slf4j.Logger; |
52 | 53 |
import org.slf4j.LoggerFactory; |
53 | 54 |
|
... | ... | |
68 | 69 |
GoogleMapsManager googleMapsManager=GoogleMapsLocator.getManager(); |
69 | 70 |
DefaultGoogleMapsConfigDataParameters params; |
70 | 71 |
private boolean showCrsWarning = false; |
72 |
private ActionListenerSupport listeners = ToolsSwingLocator.getToolsSwingManager().createActionListenerSupport(); |
|
73 |
private PanelAction selectedAction=null; |
|
71 | 74 |
|
72 | 75 |
/** |
73 | 76 |
* creates a DefaultGoogleMapsParametersPanelController |
... | ... | |
80 | 83 |
translate(); |
81 | 84 |
} |
82 | 85 |
|
86 |
public void addActionListener(ActionListener listener) { |
|
87 |
listeners.addActionListener(listener); |
|
88 |
} |
|
89 |
|
|
90 |
public void removeActionListener(ActionListener listener) { |
|
91 |
listeners.removeActionListener(listener); |
|
92 |
} |
|
93 |
|
|
83 | 94 |
private void translate() { |
84 | 95 |
|
85 | 96 |
lblSelectMapType.setText(i18nManager.getTranslation(lblSelectMapType.getText())); |
... | ... | |
92 | 103 |
lblZoomLvl.setToolTipText(i18nManager.getTranslation(lblZoomLvl.getToolTipText())); |
93 | 104 |
//txtDisclaimer.setText(i18nManager.getTranslation(txtDisclaimer.getText())); |
94 | 105 |
txtDisclaimer.setText(i18nManager.getTranslation("_google_maps_disclaimer")); |
106 |
txtDisclaimer.addMouseListener(new MouseListener() { |
|
107 |
|
|
108 |
|
|
109 |
@Override |
|
110 |
public void mouseReleased(MouseEvent e) { |
|
111 |
// TODO Auto-generated method stub |
|
112 |
|
|
113 |
} |
|
114 |
|
|
115 |
@Override |
|
116 |
public void mousePressed(MouseEvent e) { |
|
117 |
// TODO Auto-generated method stub |
|
118 |
|
|
119 |
} |
|
120 |
|
|
121 |
@Override |
|
122 |
public void mouseExited(MouseEvent e) { |
|
123 |
// TODO Auto-generated method stub |
|
124 |
|
|
125 |
} |
|
126 |
|
|
127 |
@Override |
|
128 |
public void mouseEntered(MouseEvent e) { |
|
129 |
// TODO Auto-generated method stub |
|
130 |
|
|
131 |
} |
|
132 |
|
|
133 |
@Override |
|
134 |
public void mouseClicked(MouseEvent e) { |
|
135 |
selectedAction=GoogleMapsParametersPanel.PanelAction.SHOW_DISCLAIMER; |
|
136 |
listeners.fireActionEvent(new ActionEvent(DefaultGoogleMapsParametersPanelController.this, 0, "disclaimer")); |
|
137 |
} |
|
138 |
}); |
|
139 |
|
|
95 | 140 |
if(showCrsWarning){ |
96 | 141 |
StringBuilder builder = new StringBuilder(); |
97 | 142 |
builder.append("<html><font color=\"red\">"); |
... | ... | |
226 | 271 |
translate(); |
227 | 272 |
} |
228 | 273 |
|
274 |
@Override |
|
275 |
public boolean isSelectedAction(PanelAction action) { |
|
276 |
return action.equals(this.selectedAction); |
|
277 |
} |
|
278 |
|
|
229 | 279 |
} |
org.gvsig.googlemaps/trunk/org.gvsig.googlemaps/org.gvsig.googlemaps.swing/org.gvsig.googlemaps.swing.api/src/main/java/org/gvsig/googlemaps/swing/api/GoogleMapsParametersPanel.java | ||
---|---|---|
22 | 22 |
*/ |
23 | 23 |
package org.gvsig.googlemaps.swing.api; |
24 | 24 |
|
25 |
import java.awt.event.ActionListener; |
|
26 |
|
|
25 | 27 |
import org.gvsig.fmap.dal.DataStoreParameters; |
26 | 28 |
import org.gvsig.fmap.dal.exception.InitializeException; |
27 | 29 |
import org.gvsig.fmap.dal.exception.ProviderNotRegisteredException; |
... | ... | |
33 | 35 |
* |
34 | 36 |
*/ |
35 | 37 |
public interface GoogleMapsParametersPanel extends Component{ |
38 |
enum PanelAction{SHOW_DISCLAIMER}; |
|
36 | 39 |
/** |
37 | 40 |
* Gets the parameters to create a DataStore |
38 | 41 |
* @return DataStoreParameters |
... | ... | |
53 | 56 |
* @param showCrsWarning |
54 | 57 |
*/ |
55 | 58 |
void showCrsWarning(boolean showCrsWarning); |
59 |
|
|
60 |
public void addActionListener(ActionListener listener); |
|
61 |
|
|
62 |
public void removeActionListener(ActionListener listener); |
|
63 |
|
|
64 |
public boolean isSelectedAction(PanelAction action); |
|
56 | 65 |
} |
org.gvsig.googlemaps/trunk/org.gvsig.googlemaps/pom.xml | ||
---|---|---|
3 | 3 |
<parent> |
4 | 4 |
<groupId>org.gvsig</groupId> |
5 | 5 |
<artifactId>org.gvsig.desktop</artifactId> |
6 |
<version>2.0.148</version>
|
|
6 |
<version>2.0.151</version>
|
|
7 | 7 |
</parent> |
8 | 8 |
<artifactId>org.gvsig.googlemaps</artifactId> |
9 | 9 |
<version>1.0.6-SNAPSHOT</version> |
org.gvsig.googlemaps/trunk/org.gvsig.googlemaps/org.gvsig.googlemaps.app/org.gvsig.googlemaps.app.mainplugin/src/main/java/org/gvsig/googlemaps/app/mainplugin/GoogleMapsService.java | ||
---|---|---|
22 | 22 |
*/ |
23 | 23 |
package org.gvsig.googlemaps.app.mainplugin; |
24 | 24 |
|
25 |
import java.awt.event.ActionEvent; |
|
26 |
import java.awt.event.ActionListener; |
|
27 |
import java.net.URI; |
|
28 |
import java.net.URISyntaxException; |
|
29 |
|
|
25 | 30 |
import org.cresques.cts.IProjection; |
26 |
import org.slf4j.Logger; |
|
27 |
import org.slf4j.LoggerFactory; |
|
28 |
|
|
29 | 31 |
import org.gvsig.andami.PluginServices; |
30 | 32 |
import org.gvsig.andami.PluginsLocator; |
31 | 33 |
import org.gvsig.andami.PluginsManager; |
... | ... | |
45 | 47 |
import org.gvsig.tools.swing.api.Component; |
46 | 48 |
import org.gvsig.webmap.lib.api.WebMapService; |
47 | 49 |
import org.gvsig.webmap.lib.api.exceptions.WebMapInvalidParamsException; |
50 |
import org.slf4j.Logger; |
|
51 |
import org.slf4j.LoggerFactory; |
|
48 | 52 |
|
49 | 53 |
|
50 | 54 |
/** |
... | ... | |
76 | 80 |
if (parametersPanel==null){ |
77 | 81 |
GoogleMapsConfigDataParameters configParams= getGoogleMapsConfigDataParameters(); |
78 | 82 |
parametersPanel=GoogleMapsSwingLocator.getManager().createGoogleMapsParametersPanel(configParams); |
83 |
parametersPanel.addActionListener(new ActionListener() { |
|
84 |
|
|
85 |
|
|
86 |
@Override |
|
87 |
public void actionPerformed(ActionEvent event) { |
|
88 |
GoogleMapsParametersPanel panel = (GoogleMapsParametersPanel) event.getSource(); |
|
89 |
if (panel.isSelectedAction(GoogleMapsParametersPanel.PanelAction.SHOW_DISCLAIMER)) { |
|
90 |
PluginsManager pluginManager = PluginsLocator.getManager(); |
|
91 |
I18nManager i18nManager = ToolsLocator.getI18nManager(); |
|
92 |
String disclaimerURL=i18nManager.getTranslation("_disclaimer_url"); |
|
93 |
try { |
|
94 |
pluginManager.desktopBrowse(new URI(disclaimerURL)); |
|
95 |
} catch (URISyntaxException e) { |
|
96 |
log.warn("Couldn't browse to: "+disclaimerURL); |
|
97 |
} |
|
98 |
} |
|
99 |
} |
|
100 |
}); |
|
79 | 101 |
IProjection googleMapsProjection = CRSFactory.getCRS("EPSG:3857"); |
80 | 102 |
if(mapCtrl!=null && !(mapCtrl.getProjection().equals(googleMapsProjection))){ |
81 | 103 |
parametersPanel.showCrsWarning(true); |
82 | 104 |
} |
105 |
|
|
83 | 106 |
} |
84 | 107 |
return parametersPanel; |
85 | 108 |
} |
org.gvsig.googlemaps/trunk/org.gvsig.googlemaps/org.gvsig.googlemaps.app/org.gvsig.googlemaps.app.mainplugin/src/main/resources-plugin/i18n/text_en.properties | ||
---|---|---|
15 | 15 |
_zoom_lvl_not_valid=Not valid zoom level |
16 | 16 |
_api_key_needed=An API key is needed |
17 | 17 |
_google_maps_disclaimer=<html>This tool uses Google Static Maps API.<br> For its proper use, an API key should be requested to Google.<br> More information about its terms of use can be found at: <br><a href="https://developers.google.com/maps/terms#1-your-relationship-with-google">https://developers.google.com/maps/</a></html> |
18 |
_disclaimer_url=https://developers.google.com/maps/terms#1-your-relationship-with-google |
|
18 | 19 |
|
19 | 20 |
_crs_warning=The projection of the view is not appropriate to load layers of this service. |
org.gvsig.googlemaps/trunk/org.gvsig.googlemaps/org.gvsig.googlemaps.app/org.gvsig.googlemaps.app.mainplugin/src/main/resources-plugin/i18n/text.properties | ||
---|---|---|
15 | 15 |
_zoom_lvl_not_valid=Nivel de zoom no v\u00e1lido |
16 | 16 |
_api_key_needed=La clave del API es necesaria |
17 | 17 |
_google_maps_disclaimer=<html>Esta herramienta hace uso de la API de mapas est\u00e1ticos de Google Maps.<br> Es posible que para realizar un uso correcto de ella deba de solicitar una clave de acceso a Google.<br> Puede encontrar m\u00e1s informaci\u00f3n sobre los t\u00e9rminos de uso en: <br><a href="https://developers.google.com/maps/terms#1-your-relationship-with-google">https://developers.google.com/maps/</a></html> |
18 |
_disclaimer_url=https://developers.google.com/maps/terms#1-your-relationship-with-google |
|
18 | 19 |
|
19 | 20 |
_crs_warning=La proyecci\u00f3n de la vista no es la adecuada para cargar capas de este servicio. |
org.gvsig.bingmaps/trunk/org.gvsig.bingmaps/org.gvsig.bingmaps.app/org.gvsig.bingmaps.app.mainplugin/src/main/java/org/gvsig/bingmaps/app/mainplugin/BingMapsService.java | ||
---|---|---|
22 | 22 |
*/ |
23 | 23 |
package org.gvsig.bingmaps.app.mainplugin; |
24 | 24 |
|
25 |
import java.io.File; |
|
26 |
import java.io.FileInputStream; |
|
27 |
import java.io.FileNotFoundException; |
|
28 |
import java.io.IOException; |
|
29 |
import java.util.Properties; |
|
25 |
import java.awt.event.ActionEvent; |
|
26 |
import java.awt.event.ActionListener; |
|
27 |
import java.net.URI; |
|
28 |
import java.net.URISyntaxException; |
|
30 | 29 |
|
31 |
import org.apache.commons.io.IOUtils; |
|
32 | 30 |
import org.cresques.cts.IProjection; |
33 |
|
|
34 | 31 |
import org.gvsig.andami.PluginServices; |
35 | 32 |
import org.gvsig.andami.PluginsLocator; |
36 | 33 |
import org.gvsig.andami.PluginsManager; |
... | ... | |
50 | 47 |
import org.gvsig.tools.swing.api.Component; |
51 | 48 |
import org.gvsig.webmap.lib.api.WebMapService; |
52 | 49 |
import org.gvsig.webmap.lib.api.exceptions.WebMapInvalidParamsException; |
53 |
|
|
54 | 50 |
import org.slf4j.Logger; |
55 | 51 |
import org.slf4j.LoggerFactory; |
56 | 52 |
|
... | ... | |
85 | 81 |
if (parametersPanel==null){ |
86 | 82 |
BingMapsConfigDataParameters configParams= getBingMapsConfigDataParameters(); |
87 | 83 |
parametersPanel=BingMapsSwingLocator.getManager().createBingMapsParametersPanel(configParams); |
84 |
parametersPanel.addActionListener(new ActionListener() { |
|
85 |
|
|
86 |
|
|
87 |
@Override |
|
88 |
public void actionPerformed(ActionEvent event) { |
|
89 |
BingMapsParametersPanel panel = (BingMapsParametersPanel) event.getSource(); |
|
90 |
if (panel.isSelectedAction(BingMapsParametersPanel.PanelAction.SHOW_DISCLAIMER)) { |
|
91 |
PluginsManager pluginManager = PluginsLocator.getManager(); |
|
92 |
I18nManager i18nManager = ToolsLocator.getI18nManager(); |
|
93 |
String disclaimerURL=i18nManager.getTranslation("_disclaimer_url"); |
|
94 |
try { |
|
95 |
pluginManager.desktopBrowse(new URI(disclaimerURL)); |
|
96 |
} catch (URISyntaxException e) { |
|
97 |
log.warn("Couldn't browse to: "+disclaimerURL); |
|
98 |
} |
|
99 |
} |
|
100 |
} |
|
101 |
}); |
|
88 | 102 |
IProjection bingMapsProjection = CRSFactory.getCRS("EPSG:3857"); |
89 | 103 |
if(mapCtrl!=null && !(mapCtrl.getProjection().equals(bingMapsProjection))){ |
90 | 104 |
parametersPanel.showCrsWarning(true); |
org.gvsig.bingmaps/trunk/org.gvsig.bingmaps/org.gvsig.bingmaps.app/org.gvsig.bingmaps.app.mainplugin/src/main/resources-plugin/i18n/text.properties | ||
---|---|---|
11 | 11 |
_zoom_lvl_not_valid=Nivel de zoom no v\u00e1lido |
12 | 12 |
_api_key_needed=La clave del API es necesaria |
13 | 13 |
_bing_maps_disclaimer=<html>Esta herramienta hace uso de la API de Bing Maps.<br> Es posible que para realizar un uso correcto de ella deba de solicitar una clave de acceso a Bing.<br> Puede encontrar m\u00e1s informaci\u00f3n sobre los t\u00e9rminos de uso en: <br><a href="https://www.microsoft.com/maps/product/terms.html">https://www.microsoft.com/maps/product/terms.html</a></html> |
14 |
_disclaimer_url=https://developers.bing.com/maps/terms#1-your-relationship-with-bing |
|
14 | 15 |
|
15 | 16 |
#MapTypes |
16 | 17 |
_aerial=A\u00e9reo |
org.gvsig.bingmaps/trunk/org.gvsig.bingmaps/org.gvsig.bingmaps.app/org.gvsig.bingmaps.app.mainplugin/src/main/resources-plugin/i18n/text_en.properties | ||
---|---|---|
11 | 11 |
_zoom_lvl_not_valid=Not valid zoom level |
12 | 12 |
_api_key_needed=An API key is needed |
13 | 13 |
_bing_maps_disclaimer=<html>This tool uses Bing Maps API.<br> For its proper use, an API key should be requested to Bing.<br> More information about its terms of use can be found at: <br><a href="https://developers.bing.com/maps/terms#1-your-relationship-with-bing">https://developers.bing.com/maps/</a></html> |
14 |
_disclaimer_url=https://developers.bing.com/maps/terms#1-your-relationship-with-bing |
|
14 | 15 |
|
15 | 16 |
#MapTypes |
16 | 17 |
_aerial=Aerial |
org.gvsig.bingmaps/trunk/org.gvsig.bingmaps/org.gvsig.bingmaps.swing/org.gvsig.bingmaps.swing.impl/src/main/java/org/gvsig/bingmaps/swing/impl/DefaultBingMapsParametersPanelController.java | ||
---|---|---|
24 | 24 |
|
25 | 25 |
import java.awt.event.ActionEvent; |
26 | 26 |
import java.awt.event.ActionListener; |
27 |
import java.awt.event.MouseEvent; |
|
28 |
import java.awt.event.MouseListener; |
|
27 | 29 |
|
28 | 30 |
import javax.swing.ComboBoxModel; |
29 | 31 |
import javax.swing.DefaultComboBoxModel; |
30 | 32 |
import javax.swing.JComponent; |
31 | 33 |
|
32 | 34 |
import org.apache.commons.lang3.StringUtils; |
33 |
|
|
34 | 35 |
import org.gvsig.bingmaps.lib.api.BingMapsLocator; |
35 | 36 |
import org.gvsig.bingmaps.lib.api.BingMapsManager; |
36 | 37 |
import org.gvsig.bingmaps.lib.api.MapTypes; |
... | ... | |
46 | 47 |
import org.gvsig.fmap.dal.spi.DataManagerProviderServices; |
47 | 48 |
import org.gvsig.tools.ToolsLocator; |
48 | 49 |
import org.gvsig.tools.i18n.I18nManager; |
49 |
|
|
50 |
import org.gvsig.tools.swing.api.ActionListenerSupport; |
|
51 |
import org.gvsig.tools.swing.api.ToolsSwingLocator; |
|
50 | 52 |
import org.slf4j.Logger; |
51 | 53 |
import org.slf4j.LoggerFactory; |
52 | 54 |
|
... | ... | |
67 | 69 |
BingMapsManager bingMapsManager=BingMapsLocator.getManager(); |
68 | 70 |
BingMapsConfigDataParameters configParams; |
69 | 71 |
private boolean showCrsWarning = false; |
72 |
private ActionListenerSupport listeners = ToolsSwingLocator.getToolsSwingManager().createActionListenerSupport(); |
|
73 |
private PanelAction selectedAction=null; |
|
70 | 74 |
|
71 |
|
|
72 | 75 |
/** |
73 | 76 |
* creates a DefaultBingMapsParametersPanelController |
74 | 77 |
* @param configParams DefaultBingMapsParametersPanelController |
... | ... | |
80 | 83 |
translate(); |
81 | 84 |
} |
82 | 85 |
|
86 |
public void addActionListener(ActionListener listener) { |
|
87 |
listeners.addActionListener(listener); |
|
88 |
} |
|
89 |
|
|
90 |
public void removeActionListener(ActionListener listener) { |
|
91 |
listeners.removeActionListener(listener); |
|
92 |
} |
|
93 |
|
|
83 | 94 |
private void translate() { |
84 | 95 |
|
85 | 96 |
lblSelectMapType.setText(i18nManager.getTranslation(lblSelectMapType.getText())); |
... | ... | |
88 | 99 |
cboMapTypes.setToolTipText(i18nManager.getTranslation(cboMapTypes.getToolTipText())); |
89 | 100 |
txtAPIKey.setToolTipText(i18nManager.getTranslation(txtAPIKey.getToolTipText())); |
90 | 101 |
txtDisclaimer.setText(i18nManager.getTranslation("_bing_maps_disclaimer")); |
102 |
txtDisclaimer.addMouseListener(new MouseListener() { |
|
103 |
|
|
104 |
|
|
105 |
@Override |
|
106 |
public void mouseReleased(MouseEvent e) { |
|
107 |
// TODO Auto-generated method stub |
|
108 |
|
|
109 |
} |
|
110 |
|
|
111 |
@Override |
|
112 |
public void mousePressed(MouseEvent e) { |
|
113 |
// TODO Auto-generated method stub |
|
114 |
|
|
115 |
} |
|
116 |
|
|
117 |
@Override |
|
118 |
public void mouseExited(MouseEvent e) { |
|
119 |
// TODO Auto-generated method stub |
|
120 |
|
|
121 |
} |
|
122 |
|
|
123 |
@Override |
|
124 |
public void mouseEntered(MouseEvent e) { |
|
125 |
// TODO Auto-generated method stub |
|
126 |
|
|
127 |
} |
|
128 |
|
|
129 |
@Override |
|
130 |
public void mouseClicked(MouseEvent e) { |
|
131 |
selectedAction=BingMapsParametersPanel.PanelAction.SHOW_DISCLAIMER; |
|
132 |
listeners.fireActionEvent(new ActionEvent(DefaultBingMapsParametersPanelController.this, 0, "disclaimer")); |
|
133 |
} |
|
134 |
}); |
|
91 | 135 |
if(showCrsWarning){ |
92 | 136 |
StringBuilder builder = new StringBuilder(); |
93 | 137 |
builder.append("<html><font color=\"red\">"); |
... | ... | |
179 | 223 |
this.showCrsWarning = showCrsWarning; |
180 | 224 |
translate(); |
181 | 225 |
} |
226 |
|
|
227 |
@Override |
|
228 |
public boolean isSelectedAction(PanelAction action) { |
|
229 |
return action.equals(selectedAction); |
|
230 |
} |
|
182 | 231 |
} |
org.gvsig.bingmaps/trunk/org.gvsig.bingmaps/org.gvsig.bingmaps.swing/org.gvsig.bingmaps.swing.api/src/main/java/org/gvsig/bingmaps/swing/api/BingMapsParametersPanel.java | ||
---|---|---|
22 | 22 |
*/ |
23 | 23 |
package org.gvsig.bingmaps.swing.api; |
24 | 24 |
|
25 |
import java.awt.event.ActionListener; |
|
26 |
|
|
25 | 27 |
import org.gvsig.fmap.dal.DataStoreParameters; |
26 | 28 |
import org.gvsig.fmap.dal.exception.InitializeException; |
27 | 29 |
import org.gvsig.fmap.dal.exception.ProviderNotRegisteredException; |
... | ... | |
33 | 35 |
* |
34 | 36 |
*/ |
35 | 37 |
public interface BingMapsParametersPanel extends Component{ |
38 |
enum PanelAction{SHOW_DISCLAIMER}; |
|
36 | 39 |
/** |
37 | 40 |
* Gets the parameters to create a DataStore |
38 | 41 |
* @return DataStoreParameters |
... | ... | |
53 | 56 |
* @param showCrsWarning |
54 | 57 |
*/ |
55 | 58 |
void showCrsWarning(boolean showCrsWarning); |
59 |
|
|
60 |
public void addActionListener(ActionListener listener); |
|
61 |
|
|
62 |
public void removeActionListener(ActionListener listener); |
|
63 |
|
|
64 |
public boolean isSelectedAction(PanelAction action); |
|
56 | 65 |
} |
org.gvsig.bingmaps/trunk/org.gvsig.bingmaps/pom.xml | ||
---|---|---|
3 | 3 |
<parent> |
4 | 4 |
<groupId>org.gvsig</groupId> |
5 | 5 |
<artifactId>org.gvsig.desktop</artifactId> |
6 |
<version>2.0.148</version>
|
|
6 |
<version>2.0.151</version>
|
|
7 | 7 |
</parent> |
8 | 8 |
<artifactId>org.gvsig.bingmaps</artifactId> |
9 | 9 |
<version>1.0.6-SNAPSHOT</version> |
Also available in: Unified diff